[automerger skipped] Import translations. DO NOT MERGE ANYWHERE am: ae62fdcad1 -s ours am: db1564dc30 -s ours

am skip reason: subject contains skip directive

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Launcher3/+/24916778

Change-Id: If1984c83b5d81c5530fe8e845b820808b1e9b5ee
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index a7edf2a..316f9c0 100644
--- a/Android.bp
+++ b/Android.bp
@@ -153,7 +153,8 @@
         "androidx.cardview_cardview",
         "com.google.android.material_material",
         "iconloader_base",
-        "view_capture"
+        "view_capture",
+        "animationlib"
     ],
     manifest: "AndroidManifest-common.xml",
     sdk_version: "current",
@@ -172,6 +173,7 @@
     static_libs: [
         "Launcher3ResLib",
         "launcher-testing-shared",
+        "animationlib"
     ],
     sdk_version: "current",
     min_sdk_version: min_launcher3_sdk_version,
diff --git a/OWNERS b/OWNERS
index b684460..7834396 100644
--- a/OWNERS
+++ b/OWNERS
@@ -13,6 +13,8 @@
 winsonc@google.com
 jonmiranda@google.com
 alexchau@google.com
+patmanning@google.com
+tsuharesu@google.com
 
 per-file FeatureFlags.java, globs = set noparent
 per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com, captaincole@google.com
diff --git a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
index e5b2c67..29b24b7 100644
--- a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
+++ b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
@@ -25,14 +25,13 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.system.Os;
-import android.util.Log;
 
 import androidx.annotation.Keep;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherModel;
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.testing.shared.TestProtocol;
 
@@ -62,7 +61,6 @@
                 public void onActivityCreated(Activity activity, Bundle bundle) {
                     sActivities.put(activity, true);
                     ++sActivitiesCreatedCount;
-                    Log.d(TestProtocol.FLAKY_ACTIVITY_COUNT, "onActivityCreated", new Exception());
                 }
 
                 @Override
@@ -188,13 +186,29 @@
                 return response;
             }
 
+            case TestProtocol.REQUEST_REINITIALIZE_DATA: {
+                final long identity = Binder.clearCallingIdentity();
+                try {
+                    MODEL_EXECUTOR.execute(() -> {
+                        LauncherModel model = LauncherAppState.getInstance(mContext).getModel();
+                        model.getModelDbController().createEmptyDB();
+                        MAIN_EXECUTOR.execute(model::forceReload);
+                    });
+                    return response;
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+            }
+
             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());
+                    MODEL_EXECUTOR.execute(() -> {
+                        LauncherModel model = LauncherAppState.getInstance(mContext).getModel();
+                        model.getModelDbController().createEmptyDB();
+                        model.getModelDbController().clearEmptyDbFlag();
+                        MAIN_EXECUTOR.execute(model::forceReload);
+                    });
                     return response;
                 } finally {
                     Binder.restoreCallingIdentity(identity);
diff --git a/protos/launcher_atom.proto b/protos/launcher_atom.proto
index 63ea20c..f8b08f8 100644
--- a/protos/launcher_atom.proto
+++ b/protos/launcher_atom.proto
@@ -135,7 +135,7 @@
   }
 }
 
-// Next value 51
+// Next value 52
 enum Attribute {
   option allow_alias = true;
 
@@ -187,6 +187,7 @@
   ALL_APPS_SEARCH_RESULT_SYSTEM_POINTER = 42;
   ALL_APPS_SEARCH_RESULT_EDUCARD = 43;
   ALL_APPS_SEARCH_RESULT_LOCATION = 50;
+  ALL_APPS_SEARCH_RESULT_TEXT_HEADER = 51;
 
   // Result sources
   DATA_SOURCE_APPSEARCH_APP_PREVIEW = 45;
diff --git a/quickstep/Android.bp b/quickstep/Android.bp
index f5a8253..638ce27 100644
--- a/quickstep/Android.bp
+++ b/quickstep/Android.bp
@@ -42,5 +42,6 @@
         "tests/src/com/android/quickstep/NavigationModeSwitchRule.java",
         "tests/src/com/android/quickstep/AbstractQuickStepTest.java",
         "tests/src/com/android/quickstep/TaplTestsQuickstep.java",
+        "tests/src/com/android/quickstep/TaplTestsSplitscreen.java",
     ]
 }
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index 7d7054f..c6e2d8c 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -20,7 +20,6 @@
 <manifest
     xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.launcher3">
-    <uses-sdk android:targetSdkVersion="33" android:minSdkVersion="26"/>
     <!--
     Manifest entries specific to Launcher3. This is merged with AndroidManifest-common.xml.
     Refer comments around specific entries on how to extend individual components.
diff --git a/quickstep/protos_overrides/launcher_atom_extension.proto b/quickstep/protos_overrides/launcher_atom_extension.proto
index f5a277b..b3df353 100644
--- a/quickstep/protos_overrides/launcher_atom_extension.proto
+++ b/quickstep/protos_overrides/launcher_atom_extension.proto
@@ -61,6 +61,9 @@
 
       // User entered by tapping on QSB bar on homescreen.
       QSB = 2;
+
+      // User entered by swiping up from overview (using Rocket Gesture).
+      OVERVIEW = 3;
     }
   }
 }
diff --git a/res/interpolator/standard_decelerate.xml b/quickstep/res/drawable/bg_bubble_dismiss_circle.xml
similarity index 65%
rename from res/interpolator/standard_decelerate.xml
rename to quickstep/res/drawable/bg_bubble_dismiss_circle.xml
index 579f4f5..b793eec 100644
--- a/res/interpolator/standard_decelerate.xml
+++ b/quickstep/res/drawable/bg_bubble_dismiss_circle.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2022 The Android Open Source Project
+  ~ Copyright (C) 2023 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -15,8 +15,13 @@
   ~ limitations under the License.
   -->
 
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:controlX1="0"
-    android:controlY1="0"
-    android:controlX2="0"
-    android:controlY2="1"/>
\ No newline at end of file
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+
+    <stroke
+        android:width="2dp"
+        android:color="@android:color/system_accent1_600" />
+
+    <solid android:color="@android:color/system_accent1_600" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_floating_desktop_select.xml b/quickstep/res/drawable/bg_floating_desktop_select.xml
new file mode 100644
index 0000000..d7df338
--- /dev/null
+++ b/quickstep/res/drawable/bg_floating_desktop_select.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+    Copyright (C) 2023 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT 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"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:shape="rectangle">
+
+    <corners android:radius="@dimen/rounded_button_radius" />
+    <solid android:color="?androidprv:attr/materialColorPrimaryContainer" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_bubble_dismiss_white.xml b/quickstep/res/drawable/ic_bubble_dismiss_white.xml
new file mode 100644
index 0000000..b15111b
--- /dev/null
+++ b/quickstep/res/drawable/ic_bubble_dismiss_white.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2023 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="20dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z"
+        android:fillColor="@android:color/system_neutral1_50"/>
+</vector>
diff --git a/quickstep/res/drawable/taskbar_divider_button.xml b/quickstep/res/drawable/taskbar_divider_button.xml
new file mode 100644
index 0000000..cb116cf
--- /dev/null
+++ b/quickstep/res/drawable/taskbar_divider_button.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="52dp"
+    android:height="52dp"
+    android:viewportHeight="52"
+    android:viewportWidth="52">
+    <group>
+        <path
+            android:fillColor="@color/taskbar_divider_background"
+            android:pathData="M26,11L26,41"
+            android:strokeColor="@color/taskbar_divider_background"
+            android:strokeLineCap="round"
+            android:strokeWidth="2" />
+    </group>
+</vector>
\ No newline at end of file
diff --git a/quickstep/res/interpolator/three_point_fast_out_extra_slow_in.xml b/quickstep/res/interpolator/three_point_fast_out_extra_slow_in.xml
deleted file mode 100644
index 70c4231..0000000
--- a/quickstep/res/interpolator/three_point_fast_out_extra_slow_in.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 2021, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:pathData="M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1"/>
diff --git a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
index 4e67629..69e1574 100644
--- a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
+++ b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
@@ -23,7 +23,7 @@
     android:importantForAccessibility="yes"
     android:background="@drawable/keyboard_quick_switch_task_view_background"
     android:clipToOutline="true"
-    launcher:borderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content"
diff --git a/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml b/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml
index 225b4cd..672440f 100644
--- a/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml
+++ b/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml
@@ -13,155 +13,163 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<androidx.constraintlayout.widget.ConstraintLayout
+<FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:theme="@style/GestureTutorialActivity"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:paddingTop="@dimen/gesture_tutorial_menu_padding_top"
-    android:paddingBottom="@dimen/gesture_tutorial_menu_padding_bottom"
-    android:paddingHorizontal="@dimen/gesture_tutorial_menu_padding_horizontal"
-    android:background="?androidprv:attr/materialColorSurfaceContainer">
+    android:background="?androidprv:attr/materialColorSurfaceContainer"
+    android:fitsSystemWindows="true">
 
     <androidx.constraintlayout.widget.ConstraintLayout
-        android:id="@+id/gesture_tutorial_menu_home_button"
-        android:layout_width="0dp"
-        android:layout_height="@dimen/gesture_tutorial_menu_button_height"
-        android:layout_marginEnd="@dimen/gesture_tutorial_menu_button_spacing"
-        android:layout_marginBottom="24dp"
-        android:background="@drawable/gesture_tutorial_menu_home_button_background"
-        android:clipToOutline="true"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:paddingTop="@dimen/gesture_tutorial_menu_padding_top"
+        android:paddingBottom="@dimen/gesture_tutorial_menu_padding_bottom"
+        android:paddingHorizontal="@dimen/gesture_tutorial_menu_padding_horizontal"
+        android:clipToPadding="false">
 
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/guideline"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/gesture_tutorial_menu_back_button">
-
-        <ImageView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:src="@drawable/gesture_tutorial_home_step_shape"
-            android:scaleType="fitXY"
-            android:adjustViewBounds="true"
-
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"/>
-
-        <TextView
-            style="@style/TextAppearance.GestureTutorial.MenuButton.Home"
-            android:id="@+id/gesture_tutorial_menu_home_button_text"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/home_gesture_tutorial_title"
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/gesture_tutorial_menu_home_button"
+            android:layout_width="0dp"
+            android:layout_height="@dimen/gesture_tutorial_menu_button_height"
+            android:layout_marginEnd="@dimen/gesture_tutorial_menu_button_spacing"
+            android:layout_marginBottom="24dp"
+            android:background="@drawable/gesture_tutorial_menu_home_button_background"
+            android:clipToOutline="true"
 
             app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintBottom_toTopOf="@id/guideline"
             app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/gesture_tutorial_menu_back_button">
+
+            <ImageView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/gesture_tutorial_home_step_shape"
+                android:scaleType="fitXY"
+                android:adjustViewBounds="true"
+
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"/>
+
+            <TextView
+                style="@style/TextAppearance.GestureTutorial.MenuButton.Home"
+                android:id="@+id/gesture_tutorial_menu_home_button_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/home_gesture_tutorial_title"
+
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"/>
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/gesture_tutorial_menu_back_button"
+            android:layout_width="0dp"
+            android:layout_height="@dimen/gesture_tutorial_menu_button_height"
+            android:layout_marginEnd="@dimen/gesture_tutorial_menu_button_spacing"
+            android:layout_marginBottom="24dp"
+            android:background="@drawable/gesture_tutorial_menu_back_button_background"
+            android:clipToOutline="true"
+
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toTopOf="@id/guideline"
+            app:layout_constraintStart_toEndOf="@id/gesture_tutorial_menu_home_button"
+            app:layout_constraintEnd_toStartOf="@id/gesture_tutorial_menu_overview_button">
+
+            <ImageView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/gesture_tutorial_back_step_shape"
+                android:layout_marginBottom="@dimen/gesture_tutorial_menu_back_shape_bottom_margin"
+                android:scaleType="fitXY"
+                android:adjustViewBounds="true"
+
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"/>
+
+            <TextView
+                style="@style/TextAppearance.GestureTutorial.MenuButton.Back"
+                android:id="@+id/gesture_tutorial_menu_back_button_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/back_gesture_tutorial_title"
+
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"/>
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/gesture_tutorial_menu_overview_button"
+            android:layout_width="0dp"
+            android:layout_height="@dimen/gesture_tutorial_menu_button_height"
+            android:layout_marginBottom="24dp"
+            android:background="@drawable/gesture_tutorial_menu_overview_button_background"
+            android:clipToOutline="true"
+
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toTopOf="@id/guideline"
+            app:layout_constraintStart_toEndOf="@id/gesture_tutorial_menu_back_button"
+            app:layout_constraintEnd_toEndOf="parent">
+
+            <ImageView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/gesture_tutorial_overview_step_shape"
+                android:scaleType="fitXY"
+                android:adjustViewBounds="true"
+
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"/>
+
+            <TextView
+                style="@style/TextAppearance.GestureTutorial.MenuButton.Overview"
+                android:id="@+id/gesture_tutorial_menu_overview_button_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/overview_gesture_tutorial_title"
+
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"/>
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+
+            app:layout_constraintGuide_end="@dimen/gesture_tutorial_menu_done_button_spacing"/>
+
+        <Button
+            style="@style/TextAppearance.GestureTutorial.ButtonLabel"
+            android:id="@+id/gesture_tutorial_menu_done_button"
+            android:layout_width="wrap_content"
+            android:layout_height="40dp"
+            android:layout_marginVertical="16dp"
+            android:text="@string/gesture_tutorial_action_button_label"
+            android:background="@drawable/gesture_tutorial_action_button_background"
+            android:backgroundTint="?androidprv:attr/materialColorPrimary"
+            android:stateListAnimator="@null"
+
+            app:layout_constraintTop_toBottomOf="@id/guideline"
+            app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"/>
 
     </androidx.constraintlayout.widget.ConstraintLayout>
 
-    <androidx.constraintlayout.widget.ConstraintLayout
-        android:id="@+id/gesture_tutorial_menu_back_button"
-        android:layout_width="0dp"
-        android:layout_height="@dimen/gesture_tutorial_menu_button_height"
-        android:layout_marginEnd="@dimen/gesture_tutorial_menu_button_spacing"
-        android:layout_marginBottom="24dp"
-        android:background="@drawable/gesture_tutorial_menu_back_button_background"
-        android:clipToOutline="true"
-
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/guideline"
-        app:layout_constraintStart_toEndOf="@id/gesture_tutorial_menu_home_button"
-        app:layout_constraintEnd_toStartOf="@id/gesture_tutorial_menu_overview_button">
-
-        <ImageView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:src="@drawable/gesture_tutorial_back_step_shape"
-            android:layout_marginBottom="@dimen/gesture_tutorial_menu_back_shape_bottom_margin"
-            android:scaleType="fitXY"
-            android:adjustViewBounds="true"
-
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toStartOf="parent"/>
-
-        <TextView
-            style="@style/TextAppearance.GestureTutorial.MenuButton.Back"
-            android:id="@+id/gesture_tutorial_menu_back_button_text"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/back_gesture_tutorial_title"
-
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"/>
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
-
-    <androidx.constraintlayout.widget.ConstraintLayout
-        android:id="@+id/gesture_tutorial_menu_overview_button"
-        android:layout_width="0dp"
-        android:layout_height="@dimen/gesture_tutorial_menu_button_height"
-        android:layout_marginBottom="24dp"
-        android:background="@drawable/gesture_tutorial_menu_overview_button_background"
-        android:clipToOutline="true"
-
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/guideline"
-        app:layout_constraintStart_toEndOf="@id/gesture_tutorial_menu_back_button"
-        app:layout_constraintEnd_toEndOf="parent">
-
-        <ImageView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:src="@drawable/gesture_tutorial_overview_step_shape"
-            android:scaleType="fitXY"
-            android:adjustViewBounds="true"
-
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"/>
-
-        <TextView
-            style="@style/TextAppearance.GestureTutorial.MenuButton.Overview"
-            android:id="@+id/gesture_tutorial_menu_overview_button_text"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/overview_gesture_tutorial_title"
-
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"/>
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
-
-    <androidx.constraintlayout.widget.Guideline
-        android:id="@+id/guideline"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal"
-
-        app:layout_constraintGuide_end="@dimen/gesture_tutorial_menu_done_button_spacing"/>
-
-    <Button
-        style="@style/TextAppearance.GestureTutorial.ButtonLabel"
-        android:id="@+id/gesture_tutorial_menu_done_button"
-        android:layout_width="wrap_content"
-        android:layout_height="40dp"
-        android:layout_marginVertical="16dp"
-        android:text="@string/gesture_tutorial_action_button_label"
-        android:background="@drawable/gesture_tutorial_action_button_background"
-        android:backgroundTint="?androidprv:attr/materialColorPrimary"
-        android:stateListAnimator="@null"
-
-        app:layout_constraintTop_toBottomOf="@id/guideline"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"/>
-
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+</FrameLayout>
\ No newline at end of file
diff --git a/quickstep/res/layout/activity_allset.xml b/quickstep/res/layout/activity_allset.xml
index 2c312a7..685a151 100644
--- a/quickstep/res/layout/activity_allset.xml
+++ b/quickstep/res/layout/activity_allset.xml
@@ -41,6 +41,7 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:fillViewport="true"
+        android:fitsSystemWindows="true"
 
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
@@ -95,7 +96,6 @@
                 style="@style/TextAppearance.GestureTutorial.Feedback.Subtitle"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginBottom="24dp"
                 android:text="@string/allset_hint"
                 android:textSize="@dimen/allset_page_swipe_up_text_size"
                 android:gravity="center_horizontal"
diff --git a/quickstep/res/layout/fallback_recents_activity.xml b/quickstep/res/layout/fallback_recents_activity.xml
index bfeb82d..f0ea09c 100644
--- a/quickstep/res/layout/fallback_recents_activity.xml
+++ b/quickstep/res/layout/fallback_recents_activity.xml
@@ -15,6 +15,7 @@
 -->
 <com.android.launcher3.LauncherRootView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/launcher"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:fitsSystemWindows="true">
diff --git a/quickstep/res/layout/floating_desktop_app_select.xml b/quickstep/res/layout/floating_desktop_app_select.xml
new file mode 100644
index 0000000..375fc44
--- /dev/null
+++ b/quickstep/res/layout/floating_desktop_app_select.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+     Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<com.android.quickstep.views.DesktopAppSelectView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="@dimen/desktop_mode_floating_app_select_height"
+    android:layout_gravity="top|center_horizontal"
+    android:background="@drawable/bg_floating_desktop_select"
+    android:elevation="@dimen/desktop_mode_floating_app_select_elevation"
+    android:gravity="center_vertical"
+    android:orientation="horizontal">
+
+    <TextView
+        android:id="@+id/desktop_app_select_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/desktop_mode_floating_app_select_text_margin"
+        android:layout_marginStart="@dimen/desktop_mode_floating_app_select_margin"
+        android:drawablePadding="@dimen/desktop_mode_floating_app_select_text_margin"
+        android:drawableStart="@drawable/ic_desktop"
+        android:drawableTint="?androidprv:attr/materialColorOnPrimaryContainer"
+        android:fontFamily="google-sans-medium"
+        android:gravity="center_vertical"
+        android:text="@string/desktop_select_app_toast"
+        android:textColor="?androidprv:attr/materialColorOnPrimaryContainer"
+        android:textSize="@dimen/desktop_mode_floating_app_select_text_size" />
+
+    <Button
+        android:id="@+id/close_button"
+        style="@android:style/Widget.DeviceDefault.Button.Borderless"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/desktop_mode_floating_app_select_margin"
+        android:minWidth="0dp"
+        android:fontFamily="google-sans-medium"
+        android:text="@string/desktop_button_close_app_toast"
+        android:textAllCaps="false"
+        android:textColor="?androidprv:attr/materialColorPrimary"
+        android:textSize="@dimen/desktop_mode_floating_app_select_text_size" />
+
+</com.android.quickstep.views.DesktopAppSelectView>
diff --git a/quickstep/res/layout/gesture_tutorial_activity.xml b/quickstep/res/layout/gesture_tutorial_activity.xml
index 0e763ec..82caedf 100644
--- a/quickstep/res/layout/gesture_tutorial_activity.xml
+++ b/quickstep/res/layout/gesture_tutorial_activity.xml
@@ -13,80 +13,8 @@
     See the License for the specific language governing permissions and
     limitations under the License.
 -->
-<androidx.constraintlayout.widget.ConstraintLayout
+<FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/gesture_tutorial_fragment_container"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <FrameLayout
-        android:id="@+id/gesture_tutorial_fragment_container"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent" />
-
-    <RelativeLayout
-        android:id="@+id/rotation_prompt"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:background="?attr/surfaceHome"
-        android:visibility="gone"
-
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent">
-
-        <androidx.constraintlayout.widget.ConstraintLayout
-            android:id="@+id/background"
-            android:layout_width="300dp"
-            android:layout_height="wrap_content"
-            android:layout_centerHorizontal="true"
-            android:layout_centerVertical="true"
-            android:background="@drawable/rotate_prompt_bg"
-            android:padding="24dp">
-
-            <ImageView
-                android:id="@+id/icon"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:src="@drawable/rotate_tutorial_warning"
-
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toTopOf="parent"
-                app:layout_constraintEnd_toEndOf="parent" />
-
-            <TextView
-                android:id="@+id/rotate_title"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="20dp"
-                android:lineSpacingExtra="2sp"
-                android:text="@string/gesture_tutorial_rotation_prompt_title"
-                android:textAppearance="@style/rotate_prompt_title"
-
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/icon"
-                app:layout_constraintEnd_toEndOf="parent" />
-
-            <TextView
-                android:id="@+id/rotate_screen_subtitle"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="16dp"
-                android:gravity="center"
-                android:text="@string/gesture_tutorial_rotation_prompt"
-                android:textAppearance="@style/rotate_prompt_subtitle"
-
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/rotate_title"
-                app:layout_constraintEnd_toEndOf="parent" />
-
-        </androidx.constraintlayout.widget.ConstraintLayout>
-    </RelativeLayout>
-
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+    android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/quickstep/res/layout/gesture_tutorial_rotation_prompt.xml b/quickstep/res/layout/gesture_tutorial_rotation_prompt.xml
new file mode 100644
index 0000000..b41a96d
--- /dev/null
+++ b/quickstep/res/layout/gesture_tutorial_rotation_prompt.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="?attr/surfaceHome">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/background"
+        android:layout_width="300dp"
+        android:layout_height="wrap_content"
+        android:layout_centerHorizontal="true"
+        android:layout_centerVertical="true"
+        android:background="@drawable/rotate_prompt_bg"
+        android:padding="24dp"
+
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent">
+
+        <ImageView
+            android:id="@+id/icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/rotate_tutorial_warning"
+
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintEnd_toEndOf="parent" />
+
+        <TextView
+            android:id="@+id/rotate_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="20dp"
+            android:lineSpacingExtra="2sp"
+            android:text="@string/gesture_tutorial_rotation_prompt_title"
+            android:textAppearance="@style/rotate_prompt_title"
+
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/icon"
+            app:layout_constraintEnd_toEndOf="parent" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="16dp"
+            android:gravity="center"
+            android:text="@string/gesture_tutorial_rotation_prompt"
+            android:textAppearance="@style/rotate_prompt_subtitle"
+
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/rotate_title"
+            app:layout_constraintEnd_toEndOf="parent" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/quickstep/res/layout/gesture_tutorial_step_menu.xml b/quickstep/res/layout/gesture_tutorial_step_menu.xml
index cf78b1b..c8ee6e9 100644
--- a/quickstep/res/layout/gesture_tutorial_step_menu.xml
+++ b/quickstep/res/layout/gesture_tutorial_step_menu.xml
@@ -13,154 +13,161 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<androidx.constraintlayout.widget.ConstraintLayout
+<FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:theme="@style/GestureTutorialActivity"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:paddingTop="@dimen/gesture_tutorial_menu_padding_top"
-    android:paddingBottom="@dimen/gesture_tutorial_menu_padding_bottom"
-    android:paddingHorizontal="@dimen/gesture_tutorial_menu_padding_horizontal"
     android:background="?androidprv:attr/materialColorSurfaceContainer"
-    android:clipToPadding="false">
+    android:fitsSystemWindows="true">
 
     <androidx.constraintlayout.widget.ConstraintLayout
-        android:id="@+id/gesture_tutorial_menu_home_button"
         android:layout_width="match_parent"
-        android:layout_height="@dimen/gesture_tutorial_menu_button_height"
-        android:background="@drawable/gesture_tutorial_menu_home_button_background"
-        android:clipToOutline="true"
-        app:layout_constraintVertical_chainStyle="packed"
+        android:layout_height="match_parent"
+        android:paddingTop="@dimen/gesture_tutorial_menu_padding_top"
+        android:paddingBottom="@dimen/gesture_tutorial_menu_padding_bottom"
+        android:paddingHorizontal="@dimen/gesture_tutorial_menu_padding_horizontal"
+        android:clipToPadding="false">
 
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/gesture_tutorial_menu_back_button"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent">
-
-        <ImageView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:src="@drawable/gesture_tutorial_home_step_shape"
-            android:scaleType="fitXY"
-            android:adjustViewBounds="true"
-
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"/>
-
-        <TextView
-            style="@style/TextAppearance.GestureTutorial.MenuButton.Home"
-            android:id="@+id/gesture_tutorial_menu_home_button_text"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/home_gesture_tutorial_title"
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/gesture_tutorial_menu_home_button"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/gesture_tutorial_menu_button_height"
+            android:background="@drawable/gesture_tutorial_menu_home_button_background"
+            android:clipToOutline="true"
+            app:layout_constraintVertical_chainStyle="packed"
 
             app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintBottom_toTopOf="@id/gesture_tutorial_menu_back_button"
             app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent">
+
+            <ImageView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/gesture_tutorial_home_step_shape"
+                android:scaleType="fitXY"
+                android:adjustViewBounds="true"
+
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"/>
+
+            <TextView
+                style="@style/TextAppearance.GestureTutorial.MenuButton.Home"
+                android:id="@+id/gesture_tutorial_menu_home_button_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/home_gesture_tutorial_title"
+
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"/>
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/gesture_tutorial_menu_back_button"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/gesture_tutorial_menu_button_height"
+            android:layout_marginTop="@dimen/gesture_tutorial_menu_button_spacing"
+            android:background="@drawable/gesture_tutorial_menu_back_button_background"
+            android:clipToOutline="true"
+
+            app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_menu_home_button"
+            app:layout_constraintBottom_toTopOf="@id/gesture_tutorial_menu_overview_button"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent">
+
+            <ImageView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/gesture_tutorial_back_step_shape"
+                android:scaleType="fitXY"
+                android:adjustViewBounds="true"
+
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"/>
+
+            <TextView
+                style="@style/TextAppearance.GestureTutorial.MenuButton.Back"
+                android:id="@+id/gesture_tutorial_menu_back_button_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/back_gesture_tutorial_title"
+
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"/>
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/gesture_tutorial_menu_overview_button"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/gesture_tutorial_menu_button_height"
+            android:layout_marginTop="@dimen/gesture_tutorial_menu_button_spacing"
+            android:background="@drawable/gesture_tutorial_menu_overview_button_background"
+            android:clipToOutline="true"
+
+            app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_menu_back_button"
+            app:layout_constraintBottom_toTopOf="@id/guideline"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent">
+
+            <ImageView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/gesture_tutorial_overview_step_shape"
+                android:scaleType="fitXY"
+                android:adjustViewBounds="true"
+
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"/>
+
+            <TextView
+                style="@style/TextAppearance.GestureTutorial.MenuButton.Overview"
+                android:id="@+id/gesture_tutorial_menu_overview_button_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/overview_gesture_tutorial_title"
+
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"/>
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+
+            app:layout_constraintGuide_end="@dimen/gesture_tutorial_menu_done_button_spacing"/>
+
+        <Button
+            style="@style/TextAppearance.GestureTutorial.ButtonLabel"
+            android:id="@+id/gesture_tutorial_menu_done_button"
+            android:layout_width="wrap_content"
+            android:layout_height="40dp"
+            android:layout_marginVertical="16dp"
+            android:text="@string/gesture_tutorial_action_button_label"
+            android:background="@drawable/gesture_tutorial_action_button_background"
+            android:backgroundTint="?androidprv:attr/materialColorPrimary"
+            android:stateListAnimator="@null"
+
+            app:layout_constraintTop_toBottomOf="@id/guideline"
+            app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"/>
 
     </androidx.constraintlayout.widget.ConstraintLayout>
 
-    <androidx.constraintlayout.widget.ConstraintLayout
-        android:id="@+id/gesture_tutorial_menu_back_button"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/gesture_tutorial_menu_button_height"
-        android:layout_marginTop="@dimen/gesture_tutorial_menu_button_spacing"
-        android:background="@drawable/gesture_tutorial_menu_back_button_background"
-        android:clipToOutline="true"
-
-        app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_menu_home_button"
-        app:layout_constraintBottom_toTopOf="@id/gesture_tutorial_menu_overview_button"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent">
-
-        <ImageView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:src="@drawable/gesture_tutorial_back_step_shape"
-            android:scaleType="fitXY"
-            android:adjustViewBounds="true"
-
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toStartOf="parent"/>
-
-        <TextView
-            style="@style/TextAppearance.GestureTutorial.MenuButton.Back"
-            android:id="@+id/gesture_tutorial_menu_back_button_text"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/back_gesture_tutorial_title"
-
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"/>
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
-
-    <androidx.constraintlayout.widget.ConstraintLayout
-        android:id="@+id/gesture_tutorial_menu_overview_button"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/gesture_tutorial_menu_button_height"
-        android:layout_marginTop="@dimen/gesture_tutorial_menu_button_spacing"
-        android:background="@drawable/gesture_tutorial_menu_overview_button_background"
-        android:clipToOutline="true"
-
-        app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_menu_back_button"
-        app:layout_constraintBottom_toTopOf="@id/guideline"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent">
-
-        <ImageView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:src="@drawable/gesture_tutorial_overview_step_shape"
-            android:scaleType="fitXY"
-            android:adjustViewBounds="true"
-
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"/>
-
-        <TextView
-            style="@style/TextAppearance.GestureTutorial.MenuButton.Overview"
-            android:id="@+id/gesture_tutorial_menu_overview_button_text"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/overview_gesture_tutorial_title"
-
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"/>
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
-
-    <androidx.constraintlayout.widget.Guideline
-        android:id="@+id/guideline"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal"
-
-        app:layout_constraintGuide_end="@dimen/gesture_tutorial_menu_done_button_spacing"/>
-
-    <Button
-        style="@style/TextAppearance.GestureTutorial.ButtonLabel"
-        android:id="@+id/gesture_tutorial_menu_done_button"
-        android:layout_width="wrap_content"
-        android:layout_height="40dp"
-        android:layout_marginVertical="16dp"
-        android:text="@string/gesture_tutorial_action_button_label"
-        android:background="@drawable/gesture_tutorial_action_button_background"
-        android:backgroundTint="?androidprv:attr/materialColorPrimary"
-        android:stateListAnimator="@null"
-
-        app:layout_constraintTop_toBottomOf="@id/guideline"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"/>
-
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+</FrameLayout>
\ No newline at end of file
diff --git a/quickstep/res/layout/keyboard_quick_switch_overview.xml b/quickstep/res/layout/keyboard_quick_switch_overview.xml
index e7b1f23..4a9b023 100644
--- a/quickstep/res/layout/keyboard_quick_switch_overview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_overview.xml
@@ -22,7 +22,7 @@
     android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
     android:clipToOutline="true"
     android:importantForAccessibility="yes"
-    launcher:borderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content"
diff --git a/quickstep/res/layout/keyboard_quick_switch_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_taskview.xml
index 4d213fa..6ed3c6e 100644
--- a/quickstep/res/layout/keyboard_quick_switch_taskview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_taskview.xml
@@ -23,7 +23,7 @@
     android:importantForAccessibility="yes"
     android:background="@drawable/keyboard_quick_switch_task_view_background"
     android:clipToOutline="true"
-    launcher:borderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content"
diff --git a/quickstep/res/layout/split_instructions_view.xml b/quickstep/res/layout/split_instructions_view.xml
index 91fb05c..c663bf4 100644
--- a/quickstep/res/layout/split_instructions_view.xml
+++ b/quickstep/res/layout/split_instructions_view.xml
@@ -24,12 +24,15 @@
     android:paddingTop="@dimen/split_instructions_vertical_padding"
     android:paddingBottom="@dimen/split_instructions_vertical_padding"
     android:elevation="@dimen/split_instructions_elevation"
-    android:visibility="gone">
+    android:visibility="gone"
+    android:importantForAccessibility="yes">
     <androidx.appcompat.widget.AppCompatTextView
         android:id="@+id/split_instructions_text"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content"
         android:gravity="center"
         android:textColor="?androidprv:attr/textColorOnAccent"
+        android:drawableEnd="@drawable/ic_split_exit"
+        android:drawablePadding="@dimen/split_instructions_drawable_padding"
         android:text="@string/toast_split_select_app" />
 </com.android.quickstep.views.SplitInstructionsView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 4865aef..29c9992 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -24,7 +24,8 @@
     android:clipChildren="false"
     android:defaultFocusHighlightEnabled="false"
     android:focusable="true"
-    launcher:borderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
+    launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
 
     <com.android.quickstep.views.TaskThumbnailView
         android:id="@+id/snapshot"
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index fd82c66..06f4d06 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -25,7 +25,8 @@
     android:clipToOutline="true"
     android:defaultFocusHighlightEnabled="false"
     android:focusable="true"
-    launcher:borderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
+    launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
 
     <View
         android:id="@+id/background"
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index c9fa9c0..75ff626 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -29,7 +29,8 @@
     android:clipChildren="false"
     android:defaultFocusHighlightEnabled="false"
     android:focusable="true"
-    launcher:borderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
+    launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
 
     <com.android.quickstep.views.TaskThumbnailView
         android:id="@+id/snapshot"
diff --git a/quickstep/res/layout/taskbar_all_apps.xml b/quickstep/res/layout/taskbar_all_apps.xml
index 976cd9e..e234165 100644
--- a/quickstep/res/layout/taskbar_all_apps.xml
+++ b/quickstep/res/layout/taskbar_all_apps.xml
@@ -14,18 +14,11 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.taskbar.allapps.TaskbarAllAppsSlideInView
+<com.android.launcher3.taskbar.allapps.TaskbarAllAppsContainerView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:accessibilityPaneTitle="@string/all_apps_label">
-
-    <com.android.launcher3.taskbar.allapps.TaskbarAllAppsContainerView
-        android:id="@+id/apps_view"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:clipChildren="true"
-        android:clipToPadding="false"
-        android:focusable="false"
-        android:saveEnabled="false" />
-</com.android.launcher3.taskbar.allapps.TaskbarAllAppsSlideInView>
+    android:clipChildren="true"
+    android:clipToPadding="false"
+    android:focusable="false"
+    android:saveEnabled="false" />
diff --git a/quickstep/res/layout/taskbar_all_apps_sheet.xml b/quickstep/res/layout/taskbar_all_apps_sheet.xml
new file mode 100644
index 0000000..a1d5fa6
--- /dev/null
+++ b/quickstep/res/layout/taskbar_all_apps_sheet.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.launcher3.taskbar.allapps.TaskbarAllAppsSlideInView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:accessibilityPaneTitle="@string/all_apps_label">
+
+    <include
+        android:id="@+id/apps_view"
+        layout="@layout/taskbar_all_apps" />
+</com.android.launcher3.taskbar.allapps.TaskbarAllAppsSlideInView>
diff --git a/quickstep/res/layout/taskbar_divider.xml b/quickstep/res/layout/taskbar_divider.xml
index 73f3811..0a92fa9 100644
--- a/quickstep/res/layout/taskbar_divider.xml
+++ b/quickstep/res/layout/taskbar_divider.xml
@@ -13,16 +13,9 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<FrameLayout
+<com.android.launcher3.views.IconButtonView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="@dimen/taskbar_icon_min_touch_size"
     android:layout_height="@dimen/taskbar_icon_min_touch_size"
     android:contentDescription="@string/taskbar_divider_a11y_title"
-    android:backgroundTint="@android:color/transparent">
-
-    <View
-        android:layout_height="32dp"
-        android:layout_width="2dp"
-        android:layout_gravity="center"
-        android:background="@drawable/taskbar_divider_bg" />
-</FrameLayout>
\ No newline at end of file
+    android:backgroundTint="@android:color/transparent" />
\ No newline at end of file
diff --git a/quickstep/res/layout/taskbar_divider_popup_menu.xml b/quickstep/res/layout/taskbar_divider_popup_menu.xml
index 195443e..00e47c9 100644
--- a/quickstep/res/layout/taskbar_divider_popup_menu.xml
+++ b/quickstep/res/layout/taskbar_divider_popup_menu.xml
@@ -32,7 +32,7 @@
         android:clickable="true"
         android:gravity="center_vertical"
         android:orientation="horizontal"
-        android:background="@drawable/top_rounded_popup_ripple"
+        android:background="@drawable/rounded_popup_ripple"
         android:paddingEnd="10dp"
         android:paddingStart="10dp"
         android:theme="@style/PopupItem">
@@ -59,40 +59,4 @@
             android:text="@string/always_show_taskbar" />
 
     </LinearLayout>
-
-    <LinearLayout
-        android:id="@+id/navigation_mode_switch_option"
-        android:layout_width="match_parent"
-        android:layout_height="52dp"
-        android:layout_gravity="center_vertical"
-        android:elevation="2dp"
-        android:clickable="true"
-        android:focusable="true"
-        android:background="@drawable/bottom_rounded_popup_ripple"
-        android:gravity="center_vertical"
-        android:orientation="horizontal"
-        android:paddingEnd="10dp"
-        android:paddingStart="10dp"
-        android:theme="@style/PopupItem">
-
-        <View
-            android:layout_width="24dp"
-            android:layout_height="24dp"
-            android:layout_margin="4dp"
-            android:background="@drawable/ic_touch"
-            android:backgroundTint="?android:attr/textColorPrimary" />
-
-        <com.android.launcher3.BubbleTextView
-            style="@style/BaseIcon"
-            android:id="@+id/change_navigation_mode_text"
-            android:gravity="start|center_vertical"
-            android:textAlignment="viewStart"
-            android:paddingStart="12dp"
-            android:singleLine="true"
-            android:ellipsize="end"
-            android:textSize="14sp"
-            android:textColor="?android:attr/textColorPrimary"
-            android:text="@string/change_navigation_mode" />
-
-    </LinearLayout>
 </com.android.launcher3.taskbar.TaskbarDividerPopupView>
\ No newline at end of file
diff --git a/quickstep/res/layout/transient_taskbar.xml b/quickstep/res/layout/transient_taskbar.xml
index bf4b811..0890a4e 100644
--- a/quickstep/res/layout/transient_taskbar.xml
+++ b/quickstep/res/layout/transient_taskbar.xml
@@ -44,12 +44,12 @@
         android:layout_height="@dimen/bubblebar_size"
         android:layout_gravity="bottom|end"
         android:layout_marginEnd="@dimen/transient_taskbar_bottom_margin"
-        android:layout_marginBottom="@dimen/transient_taskbar_bottom_margin"
         android:paddingEnd="@dimen/taskbar_icon_spacing"
         android:paddingStart="@dimen/taskbar_icon_spacing"
         android:visibility="gone"
         android:gravity="center"
         android:clipChildren="false"
+        android:elevation="@dimen/bubblebar_elevation"
         />
 
     <FrameLayout
diff --git a/quickstep/res/values-af/strings.xml b/quickstep/res/values-af/strings.xml
index f7a1d1a..b589e32 100644
--- a/quickstep/res/values-af/strings.xml
+++ b/quickstep/res/values-af/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Skermkiekie"</string>
     <string name="action_split" msgid="2098009717623550676">"Verdeel"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tik op ’n ander app om verdeelde skerm te gebruik"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Verlaat verdeeldeskermkeuse"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Kies nog ’n app as jy verdeelde skerm wil gebruik"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Jou organisasie laat nie hierdie program toe nie"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Slaan navigasietutoriaal oor?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Skuif na regs onder"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Wys nog # app.}other{Wys nog # apps.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> en <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Voeg nou app by werkskerm"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Kanselleer"</string>
 </resources>
diff --git a/quickstep/res/values-am/strings.xml b/quickstep/res/values-am/strings.xml
index 9b30d67..a678b3f 100644
--- a/quickstep/res/values-am/strings.xml
+++ b/quickstep/res/values-am/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"ቅጽበታዊ ገፅ ዕይታ"</string>
     <string name="action_split" msgid="2098009717623550676">"ክፈል"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"የተከፈለ ማያ ገጽን ለመጠቀም ሌላ መተግበሪያ መታ ያድርጉ"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ከተከፈለ ማያ ገፅ ምርጫ ይውጡ"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"የተከፈለ ማያ ገጽን ለመቀበል ሌላ መተግበሪያ ይምረጡ"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"ይህ ድርጊት በመተግበሪያው ወይም በእርስዎ ድርጅት አይፈቀድም"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"የአሰሳ አጋዥ ሥልጠናን ይዝለሉ?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ወደ ታች/ቀኝ ይውሰዱ"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{ተጨማሪ # መተግበሪያ አሳይ።}one{ተጨማሪ # መተግበሪያ አሳይ።}other{ተጨማሪ # መተግበሪያዎች አሳይ።}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> እና <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"መተግበሪያን ወደ ዴስክቶፕ በማከል ላይ"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"ይቅር"</string>
 </resources>
diff --git a/quickstep/res/values-ar/strings.xml b/quickstep/res/values-ar/strings.xml
index f58a75e..6d7effc 100644
--- a/quickstep/res/values-ar/strings.xml
+++ b/quickstep/res/values-ar/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"لقطة شاشة"</string>
     <string name="action_split" msgid="2098009717623550676">"تقسيم"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"انقر على تطبيق آخر لاستخدام وضع تقسيم الشاشة."</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"الخروج من وضع تقسيم الشاشة"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"اختَر تطبيقًا آخر لاستخدام وضع تقسيم الشاشة."</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"لا يسمح التطبيق أو لا تسمح مؤسستك بهذا الإجراء."</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"هل تريد تخطي الدليل التوجيهي للتنقّل؟"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"الانتقال إلى يسار الشاشة أو أسفلها"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{إظهار تطبيق واحد آخر}zero{إظهار # تطبيق آخر}two{إظهار تطبيقَين آخرَين}few{إظهار # تطبيقات أخرى}many{إظهار # تطبيقًا آخر}other{إظهار # تطبيق آخر}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"\"<xliff:g id="APP_NAME_1">%1$s</xliff:g>\" و\"<xliff:g id="APP_NAME_2">%2$s</xliff:g>\""</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"إضافة تطبيق إلى سطح المكتب"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"إلغاء"</string>
 </resources>
diff --git a/quickstep/res/values-as/strings.xml b/quickstep/res/values-as/strings.xml
index 05d1b99..a68aee5 100644
--- a/quickstep/res/values-as/strings.xml
+++ b/quickstep/res/values-as/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"স্ক্ৰীনশ্বট"</string>
     <string name="action_split" msgid="2098009717623550676">"বিভাজন কৰক"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"বিভাজিত স্ক্ৰীন ব্যৱহাৰ কৰিবলৈ অন্য এটা এপত টিপক"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"বিভাজিত স্ক্ৰীনৰ বাছনিৰ পৰা বাহিৰ হওক"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"বিভাজিত স্ক্ৰীন ব্যৱহাৰ কৰিবলৈ অন্য এটা এপ্ বাছক"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"এপ্‌টোৱে অথবা আপোনাৰ প্ৰতিষ্ঠানে এই কাৰ্যটোৰ অনুমতি নিদিয়ে"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"নেভিগেশ্বনৰ টিউট’ৰিয়েল এৰিব বিচাৰে নেকি?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"তলৰ সোঁফাললৈ নিয়ক"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{আৰু # টা এপ্‌ দেখুৱাওক।}one{আৰু # টা এপ্‌ দেখুৱাওক।}other{আৰু # টা এপ্‌ দেখুৱাওক।}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> আৰু <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"ডেস্কটপত এপ্ যোগ দি থকা হৈছে"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"বাতিল কৰক"</string>
 </resources>
diff --git a/quickstep/res/values-az/strings.xml b/quickstep/res/values-az/strings.xml
index 3e5bb6f..9fff2d8 100644
--- a/quickstep/res/values-az/strings.xml
+++ b/quickstep/res/values-az/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Skrinşot"</string>
     <string name="action_split" msgid="2098009717623550676">"Ayırın"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Bölünmüş ekran üçün başqa tətbiqə toxunun"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Bölünmüş ekran seçimindən çıxın"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Bölünmüş ekrandan istifadə üçün başqa tətbiq seçin"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Bu əməliyyata tətbiq və ya təşkilatınız tərəfindən icazə verilmir"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Naviqasiya dərsliyi ötürülsün?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Aşağı/sağa köçürün"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Daha # tətbiqi göstərin.}other{Daha # tətbiqi göstərin.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> və <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Tətbiqin masaüstünə əlavə edilməsi"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Ləğv edin"</string>
 </resources>
diff --git a/quickstep/res/values-b+sr+Latn/strings.xml b/quickstep/res/values-b+sr+Latn/strings.xml
index e6d87f8..9a7aa4b 100644
--- a/quickstep/res/values-b+sr+Latn/strings.xml
+++ b/quickstep/res/values-b+sr+Latn/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Snimak ekrana"</string>
     <string name="action_split" msgid="2098009717623550676">"Podeli"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Dodirnite drugu aplikaciju za podeljeni ekran"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Izlazak iz biranja podeljenog ekrana"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Odaberite drugu aplikaciju za podeljeni ekran"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Aplikacija ili organizacija ne dozvoljavaju ovu radnju"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Želite da preskočite vodič za kretanje?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premesti dole desno"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Prikaži još # aplikaciju.}one{Prikaži još # aplikaciju.}few{Prikaži još # aplikacije.}other{Prikaži još # aplikacija.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> i <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Dodaje se aplikacija na radnu povrršinu"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Otkaži"</string>
 </resources>
diff --git a/quickstep/res/values-be/strings.xml b/quickstep/res/values-be/strings.xml
index c7358ff..ca62108 100644
--- a/quickstep/res/values-be/strings.xml
+++ b/quickstep/res/values-be/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Здымак экрана"</string>
     <string name="action_split" msgid="2098009717623550676">"Падзелены экран"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Каб падзяліць экран, націсніце на іншую праграму"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Выйсці з рэжыму падзеленага экрана"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Каб падзяліць экран, выберыце іншую праграму"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Гэта дзеянне не дазволена праграмай ці вашай арганізацыяй"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Прапусціць дапаможнік па навігацыі?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Перамясціць уніз/управа"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Паказаць ячшэ # праграму.}one{Паказаць ячшэ # праграму.}few{Паказаць ячшэ # праграмы.}many{Паказаць ячшэ # праграм.}other{Паказаць ячшэ # праграмы.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> і <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Дадаванне праграмы на камп\'ютар"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Скасаваць"</string>
 </resources>
diff --git a/quickstep/res/values-bg/strings.xml b/quickstep/res/values-bg/strings.xml
index dbb878d..72e731a 100644
--- a/quickstep/res/values-bg/strings.xml
+++ b/quickstep/res/values-bg/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Екранна снимка"</string>
     <string name="action_split" msgid="2098009717623550676">"Разделяне на екрана"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Докоснете друго прил., за да ползвате разд. екран"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Изход от избора на разделен екран"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"За разделен екран изберете още едно приложение"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Това действие не е разрешено от приложението или организацията ви"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Пропускане на урока за навигиране?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Преместване долу/вдясно"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Показване на още # приложение.}other{Показване на още # приложения.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> и <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Приложението се добавя на настолния компютър"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Отказ"</string>
 </resources>
diff --git a/quickstep/res/values-bn/strings.xml b/quickstep/res/values-bn/strings.xml
index 7d8316c..0973a50 100644
--- a/quickstep/res/values-bn/strings.xml
+++ b/quickstep/res/values-bn/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"স্ক্রিনশট নিন"</string>
     <string name="action_split" msgid="2098009717623550676">"স্প্লিট"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"স্প্লিট স্ক্রিন ব্যবহারের জন্য অ্যাপে ট্যাপ করুন"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"স্প্লিট স্ক্রিন বেছে নেওয়ার বিকল্প থেকে বেরিয়ে আসুন"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"স্প্লিট স্ক্রিন ব্যবহার করতে অন্য অ্যাপ বেছে নিন"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"এই অ্যাপ বা আপনার প্রতিষ্ঠান এই অ্যাকশনটি পারফর্ম করার অনুমতি দেয়নি"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"নেভিগেশন টিউটোরিয়াল এড়িয়ে যেতে চান?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"নিচে/ডানদিকে সরান"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{আরও #টি অ্যাপ দেখুন।}one{আরও #টি অ্যাপ দেখুন।}other{আরও #টি অ্যাপ দেখুন।}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ও <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"ডেস্কটপে অ্যাপ যোগ করা হচ্ছে"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"বাতিল করুন"</string>
 </resources>
diff --git a/quickstep/res/values-bs/strings.xml b/quickstep/res/values-bs/strings.xml
index 56f2fa8..a2e4e33 100644
--- a/quickstep/res/values-bs/strings.xml
+++ b/quickstep/res/values-bs/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Snimak ekrana"</string>
     <string name="action_split" msgid="2098009717623550676">"Podijeli"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Dodirnite drugu apl. da koristite podijeljeni ekran"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Izlaz iz odabira podijeljenog ekrana"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Odaberite drugu apl. da koristite podijeljeni ekran"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Ovu radnju ne dozvoljava aplikacija ili vaša organizacija"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Preskočiti vodič za navigiranje?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premjesti dolje desno"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Prikaži još # aplikaciju.}one{Prikaži još # aplikaciju.}few{Prikaži još # aplikacije.}other{Prikaži još # aplikacija.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> i <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Dodavanje aplikacije na radnu površinu"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Otkaži"</string>
 </resources>
diff --git a/quickstep/res/values-ca/strings.xml b/quickstep/res/values-ca/strings.xml
index 6dff768..0cfe820 100644
--- a/quickstep/res/values-ca/strings.xml
+++ b/quickstep/res/values-ca/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Captura de pantalla"</string>
     <string name="action_split" msgid="2098009717623550676">"Divideix"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Toca una altra app per utilitzar pantalla dividida"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Surt de la selecció de pantalla dividida"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Tria una altra app per utilitzar pantalla dividida"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"L\'aplicació o la teva organització no permeten aquesta acció"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Vols ometre el tutorial de navegació?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mou a la part inferior o a la dreta"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Mostra # aplicació més.}other{Mostra # aplicacions més.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> i <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"S\'està afegint l\'aplicació a l\'ordinador"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Cancel·la"</string>
 </resources>
diff --git a/quickstep/res/values-cs/strings.xml b/quickstep/res/values-cs/strings.xml
index 40f8c3e..3582005 100644
--- a/quickstep/res/values-cs/strings.xml
+++ b/quickstep/res/values-cs/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Snímek obrazovky"</string>
     <string name="action_split" msgid="2098009717623550676">"Rozdělit"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Obrazovku rozdělíte klepnutím na jinou aplikaci"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Výběr opuštění rozdělené obrazovky"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Vyberte podporovanou aplikaci"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Aplikace nebo organizace zakazuje tuto akci"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Přeskočit výukový program k navigaci?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Přesunout doprava dolů"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Zobrazit # další aplikaci.}few{Zobrazit # další aplikace.}many{Zobrazit # další aplikace.}other{Zobrazit # dalších aplikací.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> a <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Přidání aplikace na plochu"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Zrušit"</string>
 </resources>
diff --git a/quickstep/res/values-da/strings.xml b/quickstep/res/values-da/strings.xml
index 7c786f3..1603fac 100644
--- a/quickstep/res/values-da/strings.xml
+++ b/quickstep/res/values-da/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
     <string name="action_split" msgid="2098009717623550676">"Opdel"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tryk på en anden app for at bruge opdelt skærm"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Luk valg af opdelt skærm"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Vælg en anden app for at bruge opdelt skærm"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Appen eller din organisation tillader ikke denne handling"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Vil du springe vejledningen for navigation over?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Flyt til bunden eller højre side"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Vis # app mere.}one{Vis # app mere.}other{Vis # apps mere.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> og <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Appen føjes til computeren"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Annuller"</string>
 </resources>
diff --git a/quickstep/res/values-de/strings.xml b/quickstep/res/values-de/strings.xml
index 5fd1232..828b85f 100644
--- a/quickstep/res/values-de/strings.xml
+++ b/quickstep/res/values-de/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
     <string name="action_split" msgid="2098009717623550676">"Teilen"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Für Splitscreen auf weitere App tippen"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Splitscreen-Auswahl beenden"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Für Splitscreen andere App auswählen"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Die App oder deine Organisation lässt diese Aktion nicht zu"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Tutorial zur Bedienung überspringen?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Nach unten / Nach rechts verschieben"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{# weitere App anzeigen.}other{# weitere Apps anzeigen.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> und <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Hinzufügen einer App zum Desktop"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Abbrechen"</string>
 </resources>
diff --git a/quickstep/res/values-el/strings.xml b/quickstep/res/values-el/strings.xml
index d1c16ba..ab9fe6d 100644
--- a/quickstep/res/values-el/strings.xml
+++ b/quickstep/res/values-el/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Στιγμιότυπο οθόνης"</string>
     <string name="action_split" msgid="2098009717623550676">"Διαχωρισμός"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Πατήστε άλλη εφαρμογή για διαχωρισμό οθόνης"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Έξοδος από την επιλογή διαχωρισμού οθόνης"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Επιλέξτε άλλη εφαρμογή για διαχωρισμό οθόνης"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Αυτή η ενέργεια δεν επιτρέπεται από την εφαρμογή ή τον οργανισμό σας."</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Παράβλεψη οδηγού πλοήγησης;"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Μετακίνηση κάτω/δεξιά"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Εμφάνιση # ακόμα εφαρμογής.}other{Εμφάνιση # ακόμα εφαρμογών.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> και <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Γίνεται προσθήκη εφαρμογής στον υπολογιστή"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Ακύρωση"</string>
 </resources>
diff --git a/quickstep/res/values-en-rAU/strings.xml b/quickstep/res/values-en-rAU/strings.xml
index c2b000e..878c062 100644
--- a/quickstep/res/values-en-rAU/strings.xml
+++ b/quickstep/res/values-en-rAU/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
     <string name="action_split" msgid="2098009717623550676">"Split"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tap another app to use split screen"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Exit split screen selection"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choose another app to use split screen"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"This action isn\'t allowed by the app or your organisation"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Skip navigation tutorial?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Show # more app.}other{Show # more apps.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> and <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Adding app to desktop"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Cancel"</string>
 </resources>
diff --git a/quickstep/res/values-en-rCA/strings.xml b/quickstep/res/values-en-rCA/strings.xml
index 0def6cf..eed32b3 100644
--- a/quickstep/res/values-en-rCA/strings.xml
+++ b/quickstep/res/values-en-rCA/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
     <string name="action_split" msgid="2098009717623550676">"Split"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tap another app to use split screen"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Exit split screen selection"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choose another app to use split screen"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"This action isn\'t allowed by the app or your organization"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Skip navigation tutorial?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Show # more app.}other{Show # more apps.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> and <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Adding app to Desktop"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Cancel"</string>
 </resources>
diff --git a/quickstep/res/values-en-rGB/strings.xml b/quickstep/res/values-en-rGB/strings.xml
index c2b000e..878c062 100644
--- a/quickstep/res/values-en-rGB/strings.xml
+++ b/quickstep/res/values-en-rGB/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
     <string name="action_split" msgid="2098009717623550676">"Split"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tap another app to use split screen"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Exit split screen selection"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choose another app to use split screen"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"This action isn\'t allowed by the app or your organisation"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Skip navigation tutorial?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Show # more app.}other{Show # more apps.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> and <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Adding app to desktop"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Cancel"</string>
 </resources>
diff --git a/quickstep/res/values-en-rIN/strings.xml b/quickstep/res/values-en-rIN/strings.xml
index c2b000e..878c062 100644
--- a/quickstep/res/values-en-rIN/strings.xml
+++ b/quickstep/res/values-en-rIN/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
     <string name="action_split" msgid="2098009717623550676">"Split"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tap another app to use split screen"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Exit split screen selection"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choose another app to use split screen"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"This action isn\'t allowed by the app or your organisation"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Skip navigation tutorial?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Show # more app.}other{Show # more apps.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> and <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Adding app to desktop"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Cancel"</string>
 </resources>
diff --git a/quickstep/res/values-en-rXC/strings.xml b/quickstep/res/values-en-rXC/strings.xml
index 54b746b..ae825c1 100644
--- a/quickstep/res/values-en-rXC/strings.xml
+++ b/quickstep/res/values-en-rXC/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‎‏‎‏‏‎‎‏‎‏‏‎‏‎‏‎‏‏‏‎‏‎‏‎‏‎‏‎‏‎‎‏‏‎‎‏‏‎‎‏‏‎‎‏‎‎‎‎‏‏‏‏‎‎‎‏‎‏‎Screenshot‎‏‎‎‏‎"</string>
     <string name="action_split" msgid="2098009717623550676">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‏‎‏‎‎‎‏‏‏‎‏‏‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‏‏‏‎‎‏‎‎‏‎‎‎‏‎‎‏‏‏‏‎‎‏‎‏‏‎‏‎‏‎‎‎Split‎‏‎‎‏‎"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‎‏‏‏‎‏‏‏‎‏‎‎‎‏‎‏‏‎‎‏‎‏‎‏‏‎‎‎‏‏‏‏‎‎‏‎‏‎‏‏‎‎‏‏‏‏‏‎‎‎‎‏‏‎‏‎‎Tap another app to use split screen‎‏‎‎‏‎"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‏‎‏‎‏‏‎‏‎‏‎‏‎‏‎‎‎‏‎‎‏‎‏‎‎‏‏‏‏‏‏‎‎‏‎‎‎‏‎‎‎‏‏‎‎‎‎‏‎‏‎‎‎‏‏‎‎‏‎‎Exit split screen selection‎‏‎‎‏‎"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‎‎‎‎‎‏‎‎‏‏‏‎‎‎‎‏‎‎‏‎‎‏‎‏‎‎‎‎‎‏‏‏‏‎‎‎‎‎‎‎‏‎‏‎‏‏‏‎‏‏‎‎‏‎‎Choose another app to use split screen‎‏‎‎‏‎"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‏‎‎‏‎‏‏‏‏‏‏‎‎‎‏‏‎‎‎‎‏‎‎‏‏‎‎‏‏‏‎‏‎‏‏‏‎‎‎‎‎‎‎‎‏‏‏‏‏‏‏‎‎‏‎‏‎‏‎‎This action isn\'t allowed by the app or your organization‎‏‎‎‏‎"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‏‎‏‎‎‏‏‎‏‏‎‏‏‏‎‎‎‎‎‎‎‏‏‏‎‎‏‏‎‏‏‎‏‏‎‎‎‎‎‏‎‎‏‏‎‎‏‎‎‏‏‏‎‏‎‎Skip navigation tutorial?‎‏‎‎‏‎"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‎‏‏‎‎‎‎‎‎‎‏‎‏‏‏‏‏‎‎‎‏‎‎‏‏‎‏‎‎‎‏‎‏‏‎‏‎‎‏‏‏‎‎‏‏‎‏‏‏‏‎‎‎‎‏‎‎Move to bottom/right‎‏‎‎‏‎"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‎‎‏‏‏‏‏‏‎‎‎‎‎‎‏‏‏‎‏‏‎‏‎‎‎‎‎‎‏‏‎‎‏‎‎‎‏‎‏‎‏‏‏‏‎‏‏‎‎‎‎‏‏‎‎‏‎Show # more app.‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‎‎‏‏‏‏‏‏‎‎‎‎‎‎‏‏‏‎‏‏‎‏‎‎‎‎‎‎‏‏‎‎‏‎‎‎‏‎‏‎‏‏‏‏‎‏‏‎‎‎‎‏‏‎‎‏‎Show # more apps.‎‏‎‎‏‎}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‏‎‏‏‎‏‏‎‎‎‎‏‏‎‎‏‎‎‎‎‏‏‏‎‎‎‎‏‏‏‏‎‎‏‎‏‎‏‏‎‏‎‎‎‏‏‏‏‏‏‎‎‎‎‎‏‎‎‎‎‎‏‎‎‏‏‎<xliff:g id="APP_NAME_1">%1$s</xliff:g>‎‏‎‎‏‏‏‎ and ‎‏‎‎‏‏‎<xliff:g id="APP_NAME_2">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‎‎‎‎‎‎‎‎‎‎‏‏‎‎‎‎‏‎‏‏‏‎‏‎‏‎‏‏‎‎‎‏‏‏‎‎‏‏‎‏‎‎‏‏‎‏‎‎‎‎‎‎‏‎‏‏‏‎‎Adding app to Desktop‎‏‎‎‏‎"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‎‏‎‏‎‏‎‎‎‏‎‏‎‏‎‏‎‎‎‎‏‎‎‎‏‏‏‎‎‎‏‏‏‎‏‏‎‏‎‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‎‎‎‎‎Cancel‎‏‎‎‏‎"</string>
 </resources>
diff --git a/quickstep/res/values-es-rUS/strings.xml b/quickstep/res/values-es-rUS/strings.xml
index 5dc9271..cf5d6e9 100644
--- a/quickstep/res/values-es-rUS/strings.xml
+++ b/quickstep/res/values-es-rUS/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Captura de pantalla"</string>
     <string name="action_split" msgid="2098009717623550676">"Pantalla dividida"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Presiona otra app para usar la pantalla dividida"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Salir de la selección de pantalla dividida"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Elige otra app para usar la pantalla dividida"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"La app o tu organización no permiten realizar esta acción"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"¿Omitir el instructivo de navegación?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover a la parte inferior o derecha"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Mostrar # app más.}other{Mostrar # apps más.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> y <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Agregando app al escritorio"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Cancelar"</string>
 </resources>
diff --git a/quickstep/res/values-es/strings.xml b/quickstep/res/values-es/strings.xml
index ca8cbee..d40780c 100644
--- a/quickstep/res/values-es/strings.xml
+++ b/quickstep/res/values-es/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Hacer captura"</string>
     <string name="action_split" msgid="2098009717623550676">"Dividir"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Toca otra aplicación para usar la pantalla dividida"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Salir de la selección de pantalla dividida"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Elige otra app para usar la pantalla dividida"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"No puedes hacerlo porque la aplicación o tu organización no lo permiten"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"¿Saltar tutorial de navegación?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover abajo/a la derecha"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Mostrar # aplicación más.}other{Mostrar # aplicaciones más.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> y <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Añadiendo aplicación al ordenador"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Cancelar"</string>
 </resources>
diff --git a/quickstep/res/values-et/strings.xml b/quickstep/res/values-et/strings.xml
index 0297d73..176741a 100644
--- a/quickstep/res/values-et/strings.xml
+++ b/quickstep/res/values-et/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Ekraanipilt"</string>
     <string name="action_split" msgid="2098009717623550676">"Eralda"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Jagatud ekraanikuva kasutamiseks puudutage muud rakendust"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Jagatud ekraanikuva valikust väljumine"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Valige jagatud ekraanikuva jaoks muu rakendus"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Rakendus või teie organisatsioon on selle toimingu keelanud"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Kas jätta navigeerimise õpetused vahele?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Teisalda alla/paremale"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Kuva veel # rakendus.}other{Kuva veel # rakendust.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ja <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Rakenduse lisamine arvutisse"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Tühista"</string>
 </resources>
diff --git a/quickstep/res/values-eu/strings.xml b/quickstep/res/values-eu/strings.xml
index 7d18d32..0d6c664 100644
--- a/quickstep/res/values-eu/strings.xml
+++ b/quickstep/res/values-eu/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Atera pantaila-argazki bat"</string>
     <string name="action_split" msgid="2098009717623550676">"Zatitu"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Sakatu beste aplikazio bat pantaila zatitzeko"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Irten Pantaila zatitzea eginbidearen hautapenetik"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Pantaila zatitua ikusteko, aukeratu beste aplikazio bat"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Aplikazioak edo erakundeak ez du eman ekintza hori gauzatzeko baimena"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Nabigazio-tutoriala saltatu nahi duzu?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Eraman behera, eskuinetara"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Erakutsi beste # aplikazio.}other{Erakutsi beste # aplikazio.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> eta <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Aplikazioa mahaigainean gehitzen"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Utzi"</string>
 </resources>
diff --git a/quickstep/res/values-fa/strings.xml b/quickstep/res/values-fa/strings.xml
index b5545ef..85b4862 100644
--- a/quickstep/res/values-fa/strings.xml
+++ b/quickstep/res/values-fa/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"نماگرفت"</string>
     <string name="action_split" msgid="2098009717623550676">"دونیمه"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"زدن روی برنامه‌ای دیگر برای استفاده از صفحه دونیمه"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"خروج از انتخاب صفحهٔ دونیمه"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"انتخاب برنامه‌ای دیگر برای استفاده از صفحه دونیمه"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"برنامه یا سازمان شما اجازه نمی‌دهد این کنش انجام شود."</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"آموزش گام‌به‌گام پیمایش رد شود؟"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"انتقال به پایین/ راست"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{نمایش # برنامه دیگر.}one{نمایش # برنامه دیگر.}other{نمایش # برنامه دیگر.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> و <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"درحال افزودن برنامه به رایانه"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"لغو"</string>
 </resources>
diff --git a/quickstep/res/values-fi/strings.xml b/quickstep/res/values-fi/strings.xml
index 748a245..60d6618 100644
--- a/quickstep/res/values-fi/strings.xml
+++ b/quickstep/res/values-fi/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Kuvakaappaus"</string>
     <string name="action_split" msgid="2098009717623550676">"Jaa"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Avaa jaettu näyttö napauttamalla toista sovellusta"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Poistu jaetun näytön valinnasta"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Käytä jaettua näyttöä valitsemalla toinen sovellus"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Sovellus tai organisaatio ei salli tätä toimintoa"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Ohitetaanko navigointiohje?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Siirrä alas tai oikealle"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Näytä # muu sovellus.}other{Näytä # muuta sovellusta.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ja <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Sovelluksen lisääminen työpöydälle"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Peru"</string>
 </resources>
diff --git a/quickstep/res/values-fr-rCA/strings.xml b/quickstep/res/values-fr-rCA/strings.xml
index 4029bb2..a98e9c5 100644
--- a/quickstep/res/values-fr-rCA/strings.xml
+++ b/quickstep/res/values-fr-rCA/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Capture d\'écran"</string>
     <string name="action_split" msgid="2098009717623550676">"Partager"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Toucher une autre appli pour partager l\'écran"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Quitter la sélection d\'écran divisé"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choisir une autre application pour utiliser l\'écran partagé"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"L\'application ou votre organisation n\'autorise pas cette action"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Ignorer le tutoriel sur la navigation?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Déplacer vers le coin inférieur droit de l\'écran"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Afficher # autre application.}one{Afficher # autre application.}other{Afficher # autres applications.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> et <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Ajout de l\'application au bureau en cours…"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Annuler"</string>
 </resources>
diff --git a/quickstep/res/values-fr/strings.xml b/quickstep/res/values-fr/strings.xml
index b86c18f..2e2e0f1 100644
--- a/quickstep/res/values-fr/strings.xml
+++ b/quickstep/res/values-fr/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Capture d\'écran"</string>
     <string name="action_split" msgid="2098009717623550676">"Partager"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Appuyez sur autre appli pour l\'écran partagé"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Quitter la sélection de l\'écran partagé"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Sélect. autre appli pour utiliser l\'écran partagé"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Cette action n\'est pas autorisée par l\'application ou par votre organisation"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Ignorer le tutoriel de navigation ?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Déplacer en bas ou à droite"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Afficher # autre appli.}one{Afficher # autre appli.}other{Afficher # autre applis.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> et <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Ajout de l\'appli au bureau"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Annuler"</string>
 </resources>
diff --git a/quickstep/res/values-gl/strings.xml b/quickstep/res/values-gl/strings.xml
index 53bc4d9..11479e1 100644
--- a/quickstep/res/values-gl/strings.xml
+++ b/quickstep/res/values-gl/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Facer captura"</string>
     <string name="action_split" msgid="2098009717623550676">"Dividir"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Para usar a pantalla dividida, toca outra app"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Saír da selección de pantalla dividida"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Escolle outra app para usar a pantalla dividida"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"A aplicación ou a túa organización non permite realizar esta acción"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Queres omitir o titorial de navegación?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover á parte inferior ou á dereita"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Mostrar # aplicación máis.}other{Mostrar # aplicacións máis.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> e <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Engadindo aplicación ao ordenador"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Cancelar"</string>
 </resources>
diff --git a/quickstep/res/values-gu/strings.xml b/quickstep/res/values-gu/strings.xml
index 519affd..ef44fb9 100644
--- a/quickstep/res/values-gu/strings.xml
+++ b/quickstep/res/values-gu/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"સ્ક્રીનશૉટ"</string>
     <string name="action_split" msgid="2098009717623550676">"વિભાજિત કરો"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"વિભાજિત સ્ક્રીન વાપરવા, કોઈ અન્ય ઍપ પર ટૅપ કરો"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"\'સ્ક્રીનને વિભાજિત કરો\' પસંદગીમાંથી બહાર નીકળો"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"સ્ક્રીન વિભાજનનો ઉપયોગ કરવા કોઈ અન્ય ઍપ પસંદ કરો"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"ઍપ કે તમારી સંસ્થા દ્વારા આ ક્રિયા કરવાની મંજૂરી નથી"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"નૅવિગેશન ટ્યૂટૉરિઅલ છોડી દઈએ?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"સૌથી નીચે જમણી બાજુએ ખસેડો"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{વધુ # ઍપ બતાવો.}one{વધુ # ઍપ બતાવો.}other{વધુ # ઍપ બતાવો.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> અને <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"ડેસ્કટૉપ પર ઍપ ઉમેરી રહ્યાં છીએ"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"રદ કરો"</string>
 </resources>
diff --git a/quickstep/res/values-hi/strings.xml b/quickstep/res/values-hi/strings.xml
index 15ddc68..28551ad 100644
--- a/quickstep/res/values-hi/strings.xml
+++ b/quickstep/res/values-hi/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"स्क्रीनशॉट लें"</string>
     <string name="action_split" msgid="2098009717623550676">"स्प्लिट स्क्रीन मोड"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"स्प्लिट स्क्रीन के लिए दूसरे ऐप्लिकेशन पर टैप करें"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"स्प्लिट स्क्रीन मोड से बाहर निकलें"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"स्प्लिट स्क्रीन के लिए, दूसरा ऐप्लिकेशन चुनें"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"ऐप्लिकेशन या आपका संगठन इस कार्रवाई की अनुमति नहीं देता"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"क्या आपको अभी नेविगेशन ट्यूटोरियल नहीं देखना है?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"नीचे/दाईं तरफ़ ले जाएं"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{# और ऐप्लिकेशन दिखाएं.}one{# और ऐप्लिकेशन दिखाएं.}other{# और ऐप्लिकेशन दिखाएं.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> और <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"डेस्कटॉप पर ऐप्लिकेशन जोड़ा जा रहा है"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"रद्द करें"</string>
 </resources>
diff --git a/quickstep/res/values-hr/strings.xml b/quickstep/res/values-hr/strings.xml
index 19786cd..abad768 100644
--- a/quickstep/res/values-hr/strings.xml
+++ b/quickstep/res/values-hr/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Snimka zaslona"</string>
     <string name="action_split" msgid="2098009717623550676">"Podijeli"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Dodirnite drugu aplikaciju za podijeljeni zaslon"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Zatvori odabir podijeljenog zaslona"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Odaberite drugu aplikaciju za upotrebu podijeljenog zaslona"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Aplikacija ili vaša organizacija ne dopuštaju ovu radnju"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Želite li preskočiti vodič za kretanje?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premjesti dolje/desno"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Prikaži više aplikacija (još #).}one{Prikaži više aplikacija (još #).}few{Prikaži više aplikacija (još #).}other{Prikaži više aplikacija (još #).}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> i <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Dodavanje aplikacije na radnu površinu"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Odustani"</string>
 </resources>
diff --git a/quickstep/res/values-hu/strings.xml b/quickstep/res/values-hu/strings.xml
index 6e4f860..16cd5f5 100644
--- a/quickstep/res/values-hu/strings.xml
+++ b/quickstep/res/values-hu/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Képernyőkép"</string>
     <string name="action_split" msgid="2098009717623550676">"Felosztás"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Koppintson másik appra az osztott képernyőhöz"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Kilépés az osztott képernyő elemeinek kiválasztásából"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Válasszon másik appot a képernyő felosztásához"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Az alkalmazás vagy az Ön szervezete nem engedélyezi ezt a műveletet"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Kihagyja a navigáció bemutatóját?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mozgatás alulra vagy a jobb oldalra"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{# további alkalmazás megjelenítése.}other{# további alkalmazás megjelenítése.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> és <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Alkalmazás hozzáadása az asztalhoz"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Mégse"</string>
 </resources>
diff --git a/quickstep/res/values-hy/strings.xml b/quickstep/res/values-hy/strings.xml
index b6bfa44..48eecd6 100644
--- a/quickstep/res/values-hy/strings.xml
+++ b/quickstep/res/values-hy/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Սքրինշոթ անել"</string>
     <string name="action_split" msgid="2098009717623550676">"Տրոհել"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Հպեք այլ հավելվածի՝ տրոհված էկրանից օգտվելու համար"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Դուրս գալ տրոհված էկրանի ռեժիմից"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Ընտրեք այլ հավելված՝ կիսված էկրանից օգտվելու համար"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Այս գործողությունն արգելված է հավելվածի կամ ձեր կազմակերպության կողմից"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Բաց թողնե՞լ նավիգացիայի ուղեցույցը"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Տեղափոխել ներքևի աջ անկյուն"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Ցուցադրել ևս # հավելված։}one{Ցուցադրել ևս # հավելված։}other{Ցուցադրել ևս # հավելված։}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> և <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Հավելվածն ավելացվում է աշխատասեղանին"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Չեղարկել"</string>
 </resources>
diff --git a/quickstep/res/values-in/strings.xml b/quickstep/res/values-in/strings.xml
index 27833a8..2852fc0 100644
--- a/quickstep/res/values-in/strings.xml
+++ b/quickstep/res/values-in/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
     <string name="action_split" msgid="2098009717623550676">"Pisahkan"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Ketuk aplikasi lain untuk memakai layar terpisah"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Keluar dari pemilihan layar terpisah"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Pilih aplikasi lain untuk memakai layar terpisah"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Tindakan ini tidak diizinkan oleh aplikasi atau organisasi Anda"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Lewati tutorial gestur?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Pindahkan ke bawah/kanan"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Tampilkan # aplikasi lain.}other{Tampilkan # aplikasi lain.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> dan <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Menambahkan aplikasi ke Desktop"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Batalkan"</string>
 </resources>
diff --git a/quickstep/res/values-is/strings.xml b/quickstep/res/values-is/strings.xml
index 3ff7185..998c23f 100644
--- a/quickstep/res/values-is/strings.xml
+++ b/quickstep/res/values-is/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Skjámynd"</string>
     <string name="action_split" msgid="2098009717623550676">"Skipta"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Ýttu á annað forrit til að nota skjáskiptingu"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Loka skjáskiptingu"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Veldu annað forrit til að nota skjáskiptingu"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Forritið eða fyrirtækið leyfir ekki þessa aðgerð"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Sleppa flettileiðsögn?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Færa neðst/til hægri"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Sýna # forrit í viðbót.}one{Sýna # forrit í viðbót.}other{Sýna # forrit í viðbót.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> og <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Forriti bætt við skjáborð"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Hætta við"</string>
 </resources>
diff --git a/quickstep/res/values-it/strings.xml b/quickstep/res/values-it/strings.xml
index 5777911..3755dbb 100644
--- a/quickstep/res/values-it/strings.xml
+++ b/quickstep/res/values-it/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
     <string name="action_split" msgid="2098009717623550676">"Dividi"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tocca un\'altra app per usare lo schermo diviso"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Esci dalla selezione dello schermo diviso"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Scegli un\'altra app per usare lo schermo diviso"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Questa azione non è consentita dall\'app o dall\'organizzazione"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Saltare il tutorial di navigazione?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Sposta in basso/a destra"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Mostra # altra app.}other{Mostra altre # app.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> e <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Aggiunta app a desktop in corso…"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Annulla"</string>
 </resources>
diff --git a/quickstep/res/values-iw/strings.xml b/quickstep/res/values-iw/strings.xml
index 5c960c9..e07d338 100644
--- a/quickstep/res/values-iw/strings.xml
+++ b/quickstep/res/values-iw/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"צילום מסך"</string>
     <string name="action_split" msgid="2098009717623550676">"פיצול"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"צריך להקיש על אפליקציה אחרת כדי להשתמש במסך מפוצל"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"יציאה מתצוגת מסך מפוצל"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"כדי להשתמש במסך מפוצל צריך לבחור אפליקציה אחרת"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"האפליקציה או הארגון שלך אינם מתירים את הפעולה הזאת"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"לדלג על המדריך לניווט?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"העברה לפינה הימנית/התחתונה"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{הצגת אפליקציה אחת (#) נוספת.}one{הצגת # אפליקציות נוספות.}two{הצגת # אפליקציות נוספות.}other{הצגת # אפליקציות נוספות.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ו-<xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"האפליקציה מתווספת לשולחן העבודה"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"ביטול"</string>
 </resources>
diff --git a/quickstep/res/values-ja/strings.xml b/quickstep/res/values-ja/strings.xml
index 13a3425..7517f9d 100644
--- a/quickstep/res/values-ja/strings.xml
+++ b/quickstep/res/values-ja/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"スクリーンショット"</string>
     <string name="action_split" msgid="2098009717623550676">"分割"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"分割画面を使用するには、他のアプリをタップします"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"分割画面の選択を終了します"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"分割画面にするには、別のアプリを選択してください"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"この操作はアプリまたは組織で許可されていません"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"操作チュートリアルをスキップしますか?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"下 / 右に移動"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{他 # 件のアプリを表示できます。}other{他 # 件のアプリを表示できます。}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> と <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"アプリをデスクトップに追加する"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"キャンセル"</string>
 </resources>
diff --git a/quickstep/res/values-ka/strings.xml b/quickstep/res/values-ka/strings.xml
index d987ac0..64aa870 100644
--- a/quickstep/res/values-ka/strings.xml
+++ b/quickstep/res/values-ka/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"ეკრანის ანაბეჭდი"</string>
     <string name="action_split" msgid="2098009717623550676">"გაყოფა"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"შეეხეთ სხვა აპს ეკრანის გასაყოფად"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ეკრანის გაყოფის არჩევანიდან გასვლა"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"აირჩიეთ სხვა აპი ეკრანის გასაყოფად"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"ეს მოქმედება არ არის დაშვებული აპის ან თქვენი ორგანიზაციის მიერ"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"გსურთ, გამოტოვოთ ნავიგაციის სახელმძღვანელო?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ქვემოთ/მარჯვნივ გადატანა"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{#-ით მეტი აპის ჩენება}other{#-ით მეტი აპის ჩვენება.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> და <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"მიმდინარეობს აპის დესკტოპზე დამატება"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"გაუქმება"</string>
 </resources>
diff --git a/quickstep/res/values-kk/strings.xml b/quickstep/res/values-kk/strings.xml
index 8a9f94e..cde0dfd 100644
--- a/quickstep/res/values-kk/strings.xml
+++ b/quickstep/res/values-kk/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Скриншот"</string>
     <string name="action_split" msgid="2098009717623550676">"Бөлу"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Экранды бөлу режимін пайдалану үшін басқа қолданбаны түртіңіз."</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Экранды бөлу режимінен шығу"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Экранды бөлу үшін басқа қолданбаны таңдаңыз."</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Бұл әрекетке қолданба не ұйым рұқсат етпейді."</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Қимылдар оқулығын өткізіп жіберу керек пе?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Төмен/оңға жылжыту"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Тағы # қолданбаны көрсету.}other{Тағы # қолданбаны көрсету.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> және <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Жұмыс үстеліне қолданба қосу"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Бас тарту"</string>
 </resources>
diff --git a/quickstep/res/values-km/strings.xml b/quickstep/res/values-km/strings.xml
index 0b12d79..89d8335 100644
--- a/quickstep/res/values-km/strings.xml
+++ b/quickstep/res/values-km/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"រូបថតអេក្រង់"</string>
     <string name="action_split" msgid="2098009717623550676">"បំបែក"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"ចុចកម្មវិធី​ផ្សេងទៀត ដើម្បីប្រើ​មុខងារបំបែកអេក្រង់"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ចាកចេញពីការជ្រើសរើសរបស់មុខងារ​បំបែកអេក្រង់"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"ជ្រើសរើសកម្មវិធីផ្សេងទៀត ដើម្បីប្រើមុខងារ​បំបែកអេក្រង់"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"សកម្មភាពនេះ​មិនត្រូវបានអនុញ្ញាតដោយកម្មវិធី​ ឬ​ស្ថាប័ន​របស់អ្នកទេ"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"រំលង​មេរៀន​អំពី​ការរុករក​ឬ?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ផ្លាស់ទីទៅខាងក្រោម/ស្ដាំ"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{បង្ហាញកម្មវិធី # ទៀត។}other{បង្ហាញ​កម្មវិធី # ទៀត។}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> និង <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"កំពុងបញ្ចូល​កម្មវិធីទៅកុំព្យូទ័រ"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"បោះបង់"</string>
 </resources>
diff --git a/quickstep/res/values-kn/strings.xml b/quickstep/res/values-kn/strings.xml
index 2666846..b7cdd15 100644
--- a/quickstep/res/values-kn/strings.xml
+++ b/quickstep/res/values-kn/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"ಸ್ಕ್ರೀನ್‌ಶಾಟ್"</string>
     <string name="action_split" msgid="2098009717623550676">"ವಿಭಜಿಸಿ"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಬಳಸಲು ಬೇರೆ ಆ್ಯಪ್ ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಆಯ್ಕೆಯಿಂದ ನಿರ್ಗಮಿಸಿ"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"\"ಪರದೆ ಬೇರ್ಪಡಿಸಿ\" ಬಳಸಲು ಬೇರೆ ಆ್ಯಪ್ ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"ಆ್ಯಪ್ ಅಥವಾ ನಿಮ್ಮ ಸಂಸ್ಥೆಯು ಈ ಕ್ರಿಯೆಯನ್ನು ಅನುಮತಿಸುವುದಿಲ್ಲ"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"ನ್ಯಾವಿಗೇಶನ್ ಟ್ಯುಟೋರಿಯಲ್ ಸ್ಕಿಪ್ ಮಾಡಬೇಕೇ?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ಕೆಳಗಿನ/ಬಲಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{ಇನ್ನೂ # ಆ್ಯಪ್ ಅನ್ನು ತೋರಿಸಿ.}one{ಇನ್ನೂ # ಆ್ಯಪ್‌ಗಳನ್ನು ತೋರಿಸಿ.}other{ಇನ್ನೂ # ಆ್ಯಪ್‌ಗಳನ್ನು ತೋರಿಸಿ.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ಮತ್ತು <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"ಡೆಸ್ಕ್‌ಟಾಪ್‌ಗೆ ಆ್ಯಪ್ ಅನ್ನು ಸೇರಿಸಲಾಗುತ್ತಿದೆ"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"ರದ್ದುಮಾಡಿ"</string>
 </resources>
diff --git a/quickstep/res/values-ko/strings.xml b/quickstep/res/values-ko/strings.xml
index 8a4c9b4..c00eeed 100644
--- a/quickstep/res/values-ko/strings.xml
+++ b/quickstep/res/values-ko/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"스크린샷"</string>
     <string name="action_split" msgid="2098009717623550676">"분할"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"다른 앱을 탭하여 화면 분할 사용"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"화면 분할 선택 종료"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"화면 분할을 사용하려면 다른 앱을 선택하세요."</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"이 작업은 앱 또는 조직에서 허용되지 않습니다."</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"이동 방법 튜토리얼을 건너뛰시겠습니까?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"하단/오른쪽으로 이동"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{앱 #개 더 표시}other{앱 #개 더 표시}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> 및 <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"데스크톱에 앱 추가하기"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"취소"</string>
 </resources>
diff --git a/quickstep/res/values-ky/strings.xml b/quickstep/res/values-ky/strings.xml
index 7c74fa6..f5d8782 100644
--- a/quickstep/res/values-ky/strings.xml
+++ b/quickstep/res/values-ky/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Скриншот"</string>
     <string name="action_split" msgid="2098009717623550676">"Бөлүү"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Экранды бөлүү үчүн башка колдонмону таптап коюңуз"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Тандалган экранды бөлүүдөн чыгуу"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Экранды бөлүү үчүн башка колдонмону тандаңыз"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Бул аракетти аткарууга колдонмо же ишканаңыз тыюу салган"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Жаңсоолор үйрөткүчүн өткөрүп жибересизби?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Төмөнкү/оң бурчка жылдыруу"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Дагы # колдонмону көрсөтүү.}other{Дагы # колдонмону көрсөтүү.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> жана <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Колдонмону иш тактага кошуу"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Жокко чыгаруу"</string>
 </resources>
diff --git a/quickstep/res/values-lo/strings.xml b/quickstep/res/values-lo/strings.xml
index 66db658..2f67c30 100644
--- a/quickstep/res/values-lo/strings.xml
+++ b/quickstep/res/values-lo/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"ຮູບໜ້າຈໍ"</string>
     <string name="action_split" msgid="2098009717623550676">"ແບ່ງ"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"ແຕະແອັບອື່ນເພື່ອໃຊ້ໜ້າຈໍແຍກ"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ອອກຈາກາກນເລືອກການແບ່ງໜ້າຈໍ"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"ເລືອກແອັບອື່ນເພື່ອໃຊ້ການແບ່ງໜ້າຈໍ"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"ແອັບ ຫຼື ອົງການຂອງທ່ານບໍ່ອະນຸຍາດໃຫ້ໃຊ້ຄຳສັ່ງນີ້"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"ຂ້າມການສອນການນຳໃຊ້ການນຳທາງບໍ?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ຍ້າຍໄປຂວາ/ລຸ່ມ"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{ສະແດງອີກ # ແອັບ.}other{ສະແດງອີກ # ແອັບ.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ແລະ <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"ການເພີ່ມແອັບໄປໃສ່ເດັສທັອບ"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"ຍົກເລີກ"</string>
 </resources>
diff --git a/quickstep/res/values-lt/strings.xml b/quickstep/res/values-lt/strings.xml
index 91909e9..8bb1252 100644
--- a/quickstep/res/values-lt/strings.xml
+++ b/quickstep/res/values-lt/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Ekrano kopija"</string>
     <string name="action_split" msgid="2098009717623550676">"Išskaidymo režimas"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Išskaidyto ekrano režimas palietus kitą programą"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Išeiti iš išskaidyto ekrano pasirinkimo"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Išskaidyto ekrano režimą naudokite kita programa"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Jūsų organizacijoje arba naudojant šią programą neleidžiama atlikti šio veiksmo"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Praleisti naršymo mokymo programą?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Perkelti žemyn, dešinėn"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Rodyti dar # programą.}one{Rodyti dar # programą.}few{Rodyti dar # programas.}many{Rodyti dar # programos.}other{Rodyti dar # programų.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"„<xliff:g id="APP_NAME_1">%1$s</xliff:g>“ ir „<xliff:g id="APP_NAME_2">%2$s</xliff:g>“"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Pridedama programa prie darbalaukio"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Atšaukti"</string>
 </resources>
diff --git a/quickstep/res/values-lv/strings.xml b/quickstep/res/values-lv/strings.xml
index c3cd847..a8bff55 100644
--- a/quickstep/res/values-lv/strings.xml
+++ b/quickstep/res/values-lv/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Veikt ekrānuzņēmumu"</string>
     <string name="action_split" msgid="2098009717623550676">"Sadalīt"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Lai sadalītu ekrānu, pieskarieties citai lietotnei"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Izejiet no ekrāna sadalīšanas režīma atlases."</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Izvēlieties citu lietotni, lai sadalītu ekrānu"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Lietotne vai jūsu organizācija neatļauj veikt šo darbību."</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Vai izlaist navigācijas mācības?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Pārvietot uz apakšējo/labo stūri"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Rādīt vēl # lietotni}zero{Rādīt vēl # lietotnes}one{Rādīt vēl # lietotni}other{Rādīt vēl # lietotnes}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"“<xliff:g id="APP_NAME_1">%1$s</xliff:g>” un “<xliff:g id="APP_NAME_2">%2$s</xliff:g>”"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Notiek lietotnes pievienošana datoram"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Atcelt"</string>
 </resources>
diff --git a/quickstep/res/values-mk/strings.xml b/quickstep/res/values-mk/strings.xml
index 9122310..258a544 100644
--- a/quickstep/res/values-mk/strings.xml
+++ b/quickstep/res/values-mk/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Слика од екранот"</string>
     <string name="action_split" msgid="2098009717623550676">"Раздели"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Допрете друга аплик. за да користите поделен екран"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Излези од изборот на поделен екран"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Изберете друга апликација за да користите поделен екран"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Апликацијата или вашата организација не го дозволува дејствово"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Да се прескокне упатството за навигација?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Премести долу десно"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Прикажи уште # апликација.}one{Прикажи уште # апликација.}other{Прикажи уште # апликации.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> и <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Додавање на апликацијата во „Работна површина“"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Откажи"</string>
 </resources>
diff --git a/quickstep/res/values-ml/strings.xml b/quickstep/res/values-ml/strings.xml
index b6ad6ac..fffcd54 100644
--- a/quickstep/res/values-ml/strings.xml
+++ b/quickstep/res/values-ml/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"സ്ക്രീൻഷോട്ട്"</string>
     <string name="action_split" msgid="2098009717623550676">"വിഭജിക്കുക"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"സ്പ്ലിറ്റ് സ്ക്രീനിന് മറ്റൊരു ആപ്പിൽ ടാപ്പ് ചെയ്യൂ"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"സ്‌ക്രീൻ വിഭജന തിരഞ്ഞെടുപ്പിൽ നിന്ന് പുറത്തുകടക്കുക"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"സ്ക്രീൻ വിഭജന മോഡിന് മറ്റൊരു ആപ്പ് തിരഞ്ഞെടുക്കൂ"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"ഈ നടപടി എടുക്കുന്നത് ആപ്പോ നിങ്ങളുടെ സ്ഥാപനമോ അനുവദിക്കുന്നില്ല"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"നാവിഗേഷൻ ട്യൂട്ടോറിയൽ ഒഴിവാക്കണോ?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"താഴേക്കോ വലത്തേക്കോ നീക്കുക"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{# ആപ്പ് കൂടി കാണിക്കുക.}other{# ആപ്പുകൾ കൂടി കാണിക്കുക.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g>, <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"ആപ്പ് ഡെസ്ക്ടോപ്പിലേക്ക് ചേർക്കുന്നു"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"റദ്ദാക്കുക"</string>
 </resources>
diff --git a/quickstep/res/values-mn/strings.xml b/quickstep/res/values-mn/strings.xml
index 8f82ebd..8c7ae78 100644
--- a/quickstep/res/values-mn/strings.xml
+++ b/quickstep/res/values-mn/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Дэлгэцийн агшин дарах"</string>
     <string name="action_split" msgid="2098009717623550676">"Хуваах"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Дэлгэцийг хуваахыг ашиглахын тулд өөр аппыг товш"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Дэлгэцийг хуваах сонголтоос гарах"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Дэлгэцийг хуваах горим ашиглах өөр апп сонгоно уу"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Энэ үйлдлийг апп эсвэл танай байгууллага зөвшөөрдөггүй"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Навигацын практик хичээлийг алгасах уу?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Баруун доод хэсэг рүү зөөх"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Өөр # аппыг харуулна уу.}other{Өөр # аппыг харуулна уу.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> болон <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Компьютерт апп нэмж байна"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Цуцлах"</string>
 </resources>
diff --git a/quickstep/res/values-mr/strings.xml b/quickstep/res/values-mr/strings.xml
index 73260b2..5bf47ce 100644
--- a/quickstep/res/values-mr/strings.xml
+++ b/quickstep/res/values-mr/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"स्क्रीनशॉट"</string>
     <string name="action_split" msgid="2098009717623550676">"स्प्लिट"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"स्प्लिट स्क्रीन वापरण्यासाठी दुसऱ्या ॲपवर टॅप करा"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"स्प्लिट स्क्रीन निवडीतून बाहेर पडा"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"स्प्लिट स्क्रीन वापरण्यासाठी दुसरे ॲप निवडा"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"अ‍ॅप किंवा तुमच्या संस्थेद्वारे ही क्रिया करण्याची अनुमती नाही"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"नेव्हिगेशन ट्यूटोरियल वगळायचे आहे का?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"तळाशी/उजवीकडे हलवा"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{आणखी # अ‍ॅप दाखवा.}other{आणखी # अ‍ॅप्स दाखवा.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> आणि <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"डेस्कटॉपवर ॲप जोडत आहे"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"रद्द करा"</string>
 </resources>
diff --git a/quickstep/res/values-ms/strings.xml b/quickstep/res/values-ms/strings.xml
index f9b0225..5e61b88 100644
--- a/quickstep/res/values-ms/strings.xml
+++ b/quickstep/res/values-ms/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Tangkapan skrin"</string>
     <string name="action_split" msgid="2098009717623550676">"Pisah"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Ketik apl lain untuk menggunakan skrin pisah"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Keluar daripada pilihan skrin pisah"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Pilih apl lain untuk menggunakan skrin pisah"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Tindakan ini tidak dibenarkan oleh apl atau organisasi anda"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Langkau tutorial navigasi?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Alihkan ke bawah/kanan"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Tunjukkan # lagi apl.}other{Tunjukkan # lagi apl.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> dan <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Menambahkan apl pada Desktop"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Batal"</string>
 </resources>
diff --git a/quickstep/res/values-my/strings.xml b/quickstep/res/values-my/strings.xml
index 14595cf..6f1a276 100644
--- a/quickstep/res/values-my/strings.xml
+++ b/quickstep/res/values-my/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"ဖန်သားပြင်ဓာတ်ပုံ"</string>
     <string name="action_split" msgid="2098009717623550676">"ခွဲထုတ်ရန်"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"မျက်နှာပြင် ခွဲ၍ပြသရန် အက်ပ်နောက်တစ်ခုကို တို့ပါ"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"မျက်နှာပြင် ခွဲ၍ပြသခြင်း ရွေးချယ်မှုမှ ထွက်ရန်"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"မျက်နှာပြင်ခွဲ၍ပြသခြင်းသုံးရန် နောက်အက်ပ်တစ်ခုရွေးပါ"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"ဤလုပ်ဆောင်ချက်ကို အက်ပ် သို့မဟုတ် သင်၏အဖွဲ့အစည်းက ခွင့်မပြုပါ"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"လမ်းညွှန်ခြင်း ရှင်းလင်းပို့ချချက်ကို ကျော်မလား။"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"အောက်ခြေ/ညာဘက်သို့ ရွှေ့ရန်"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{နောက်ထပ်အက်ပ် # ခု ပြပါ။}other{နောက်ထပ်အက်ပ် # ခု ပြပါ။}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> နှင့် <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"‘ဒက်စ်တော့’ တွင် အက်ပ်ကို ထည့်ခြင်း"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"မလုပ်တော့"</string>
 </resources>
diff --git a/quickstep/res/values-nb/strings.xml b/quickstep/res/values-nb/strings.xml
index 5fe4abd..d4b8de9 100644
--- a/quickstep/res/values-nb/strings.xml
+++ b/quickstep/res/values-nb/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Skjermdump"</string>
     <string name="action_split" msgid="2098009717623550676">"Del opp"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Trykk på en annen app for å bruke delt skjerm"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Avslutt valg av delt skjerm"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Velg en annen app for å bruke delt skjerm"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Appen eller organisasjonen din tillater ikke denne handlingen"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Vil du hoppe over navigeringsveiledningen?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Flytt til nederst/høyre"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Vis # app til.}other{Vis # apper til.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> og <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Legg til apper på datamaskin"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Avbryt"</string>
 </resources>
diff --git a/quickstep/res/values-ne/strings.xml b/quickstep/res/values-ne/strings.xml
index 7a86f28..b326cb4 100644
--- a/quickstep/res/values-ne/strings.xml
+++ b/quickstep/res/values-ne/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"स्क्रिनसट"</string>
     <string name="action_split" msgid="2098009717623550676">"स्प्लिट गर्नुहोस्"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"स्प्लिटस्क्रिन प्रयोग गर्न अर्को एपमा ट्याप गर्नु…"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"स्प्लिट स्क्रिन मोडबाट बाहिरिनुहोस्"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"स्प्लिट स्क्रिन प्रयोग गर्न अर्को एप रोज्नुहोस्"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"यो एप वा तपाईंको सङ्गठनले यो कारबाही गर्ने अनुमति दिँदैन"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"नेभिगेसन ट्युटोरियल स्किप गर्ने हो?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"फेद/दायाँतिर सार्नुहोस्"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{थप # एप देखाइयोस्।}other{थप # वटा एप देखाइयोस्।}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> र <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"डेस्कटपमा एप हालिँदै छ"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"रद्द गर्नुहोस्"</string>
 </resources>
diff --git a/quickstep/res/values-nl/strings.xml b/quickstep/res/values-nl/strings.xml
index ed63bad..6a7efe5 100644
--- a/quickstep/res/values-nl/strings.xml
+++ b/quickstep/res/values-nl/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
     <string name="action_split" msgid="2098009717623550676">"Splitsen"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tik op nog een app om je scherm te splitsen"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Sluit de selectie voor gesplitst scherm"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Kies andere app om gesplitst scherm te gebruiken"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Deze actie wordt niet toegestaan door de app of je organisatie"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Navigatietutorial overslaan?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Naar beneden/rechts verplaatsen"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Nog # app tonen.}other{Nog # apps tonen.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> en <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"App toevoegen aan desktop"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Annuleren"</string>
 </resources>
diff --git a/quickstep/res/values-or/strings.xml b/quickstep/res/values-or/strings.xml
index 5e2a16e..f0a5053 100644
--- a/quickstep/res/values-or/strings.xml
+++ b/quickstep/res/values-or/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"ସ୍କ୍ରିନସଟ୍"</string>
     <string name="action_split" msgid="2098009717623550676">"ସ୍ପ୍ଲିଟ୍"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"ସ୍ପ୍ଲିଟସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପରେ ଟାପ କର"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ଚୟନରୁ ବାହାରି ଯାଆନ୍ତୁ"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପ ବାଛନ୍ତୁ"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"ଆପ୍ କିମ୍ବା ଆପଣଙ୍କ ସଂସ୍ଥା ଦ୍ୱାରା ଏହି କାର୍ଯ୍ୟକୁ ଅନୁମତି ଦିଆଯାଇ ନାହିଁ"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"ନାଭିଗେସନ୍ ଟ୍ୟୁଟୋରିଆଲକୁ ବାଦ୍ ଦେବେ?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ନିମ୍ନ/ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{ଅଧିକ #ଟି ଆପ ଦେଖାନ୍ତୁ।}other{ଅଧିକ #ଟି ଆପ୍ସ ଦେଖାନ୍ତୁ।}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ଏବଂ <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"ଡେସ୍କଟପରେ ଆପ ଯୋଗ କରାଯାଉଛି"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"ବାତିଲ କରନ୍ତୁ"</string>
 </resources>
diff --git a/quickstep/res/values-pa/strings.xml b/quickstep/res/values-pa/strings.xml
index 88bc4eb..c66e26f 100644
--- a/quickstep/res/values-pa/strings.xml
+++ b/quickstep/res/values-pa/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"ਸਕ੍ਰੀਨਸ਼ਾਟ"</string>
     <string name="action_split" msgid="2098009717623550676">"ਸਪਲਿਟ"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਨੂੰ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ \'ਤੇ ਟੈਪ ਕਰੋ"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਦੀ ਚੋਣ ਤੋਂ ਬਾਹਰ ਜਾਓ"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ ਨੂੰ ਚੁਣੋ"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"ਐਪ ਜਾਂ ਤੁਹਾਡੀ ਸੰਸਥਾ ਵੱਲੋਂ ਇਸ ਕਾਰਵਾਈ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਹੈ"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"ਕੀ ਨੈਵੀਗੇਸ਼ਨ ਟਿਊਟੋਰੀਅਲ ਨੂੰ ਛੱਡਣਾ ਹੈ?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ਹੇਠਾਂ/ਸੱਜੇ ਪਾਸੇ ਲੈ ਕੇ ਜਾਓ"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{# ਹੋਰ ਐਪ ਦਿਖਾਓ।}one{# ਹੋਰ ਐਪ ਦਿਖਾਓ।}other{# ਹੋਰ ਐਪਾਂ ਦਿਖਾਓ।}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ਅਤੇ <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"ਐਪ ਨੂੰ ਡੈਸਕਟਾਪ \'ਤੇ ਸ਼ਾਮਲ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"ਰੱਦ ਕਰੋ"</string>
 </resources>
diff --git a/quickstep/res/values-pl/strings.xml b/quickstep/res/values-pl/strings.xml
index 4077212..7f0600f 100644
--- a/quickstep/res/values-pl/strings.xml
+++ b/quickstep/res/values-pl/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Zrzut ekranu"</string>
     <string name="action_split" msgid="2098009717623550676">"Podziel"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Aby podzielić ekran, kliknij drugą aplikację"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Wyjdź z wyboru podzielonego ekranu"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Wybierz drugą aplikację, aby podzielić ekran"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Nie możesz wykonać tego działania, bo nie zezwala na to aplikacja lub Twoja organizacja"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Pominąć samouczek nawigacji?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Przesuń w dolny prawy róg"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Pokaż jeszcze # aplikację.}few{Pokaż jeszcze # aplikacje.}many{Pokaż jeszcze # aplikacji.}other{Pokaż jeszcze # aplikacji.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> i <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Dodaję aplikację do komputera"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Anuluj"</string>
 </resources>
diff --git a/quickstep/res/values-pt-rPT/strings.xml b/quickstep/res/values-pt-rPT/strings.xml
index a886799..791a127 100644
--- a/quickstep/res/values-pt-rPT/strings.xml
+++ b/quickstep/res/values-pt-rPT/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Fazer captura de ecrã"</string>
     <string name="action_split" msgid="2098009717623550676">"Dividir"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Toque noutra app para usar o ecrã dividido"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Saia da seleção de ecrã dividido"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Escolher outra app para usar o ecrã dividido"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Esta ação não é permitida pela app ou a sua entidade."</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Ignorar o tutorial de navegação?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover para a part superior direita"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Mostrar mais # app.}other{Mostrar mais # apps.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> e <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"A adicionar a app ao computador"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Cancelar"</string>
 </resources>
diff --git a/quickstep/res/values-pt/strings.xml b/quickstep/res/values-pt/strings.xml
index 1430885..c943da5 100644
--- a/quickstep/res/values-pt/strings.xml
+++ b/quickstep/res/values-pt/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Capturar tela"</string>
     <string name="action_split" msgid="2098009717623550676">"Dividir"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Toque em outro app para usar a tela dividida"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Sair da seleção de tela dividida"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Escolha outro app para usar na tela dividida"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Essa ação não é permitida pelo app ou pela organização"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Pular o tutorial de navegação?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover para baixo/para a direita"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Mostrar mais # app.}one{Mostrar mais # app.}other{Mostrar mais # apps.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> e <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Adicionando app ao computador"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Cancelar"</string>
 </resources>
diff --git a/quickstep/res/values-ro/strings.xml b/quickstep/res/values-ro/strings.xml
index 1948599..5db7859 100644
--- a/quickstep/res/values-ro/strings.xml
+++ b/quickstep/res/values-ro/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Captură de ecran"</string>
     <string name="action_split" msgid="2098009717623550676">"Împărțit"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Atinge altă aplicație pentru ecranul împărțit"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Ieși din selecția cu ecran împărțit"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Alege altă aplicație pentru ecranul împărțit"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Această acțiune nu este permisă de aplicație sau de organizația ta"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Omiți tutorialul de navigare?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mută în dreapta jos"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Afișează încă # aplicație}few{Afișează încă # aplicații}other{Afișează încă # de aplicații}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> și <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Se adaugă aplicația pe computer"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Anulează"</string>
 </resources>
diff --git a/quickstep/res/values-ru/strings.xml b/quickstep/res/values-ru/strings.xml
index c62fcca..a11175e 100644
--- a/quickstep/res/values-ru/strings.xml
+++ b/quickstep/res/values-ru/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Скриншот"</string>
     <string name="action_split" msgid="2098009717623550676">"Разделить"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Для разделения экрана выберите другое приложение."</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Выйдите из режима разделения экрана."</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Выберите другое приложение для разделения экрана."</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Это действие заблокировано приложением или организацией."</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Пропустить руководство по жестам?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Переместить вниз или вправо"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Показать ещё # приложение}one{Показать ещё # приложение}few{Показать ещё # приложения}many{Показать ещё # приложений}other{Показать ещё # приложения}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> и <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Добавление приложения на компьютер"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Отмена"</string>
 </resources>
diff --git a/quickstep/res/values-si/strings.xml b/quickstep/res/values-si/strings.xml
index 2c1e67d..3722b2f 100644
--- a/quickstep/res/values-si/strings.xml
+++ b/quickstep/res/values-si/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"තිර රුව"</string>
     <string name="action_split" msgid="2098009717623550676">"බෙදන්න"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"බෙදුම් තිරය භාවිතා කිරීමට තවත් යෙදුමක් තට්ටු කරන්න"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"බෙදීම් තිර තේරීමෙන් පිටවන්න"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"බෙදීම් තිරය භාවිතා කිරීමට වෙනත් යෙදුමක් තෝරා ගන්න"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"මෙම ක්‍රියාව යෙදුම හෝ ඔබේ සංවිධානය මගින් ඉඩ නොදේ"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"නිබන්ධනය සංචාලනය මඟ හරින්නද?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"පහළ/දකුණ වෙත ගෙන යන්න"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{තවත් # යෙදුමක් පෙන්වන්න.}one{තවත් යෙදුම් #ක් පෙන්වන්න.}other{තවත් යෙදුම් #ක් පෙන්වන්න.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> සහ <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"ඩෙස්ක්ටොප් වෙත යෙදුම එක් කිරීම"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"අවලංගු කරන්න"</string>
 </resources>
diff --git a/quickstep/res/values-sk/strings.xml b/quickstep/res/values-sk/strings.xml
index b0e859b..aee281c 100644
--- a/quickstep/res/values-sk/strings.xml
+++ b/quickstep/res/values-sk/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Snímka obrazovky"</string>
     <string name="action_split" msgid="2098009717623550676">"Rozdeliť"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Obrazovku rozdelíte klepnutím na inú aplikáciu"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Ukončite výber rozdelenej obrazovky"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Na použitie rozd. obrazovky vyberte inú aplikáciu"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Aplikácia alebo vaša organizácia túto akciu nepovoľuje"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Chcete preskočiť návod na navigáciu?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Presunúť dole alebo doprava"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Zobraziť # ďalšiu aplikáciu.}few{Zobraziť # ďalšie aplikácie.}many{Show # more apps.}other{Zobraziť # ďalších aplikácií.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> a <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Pridanie aplikácie na plochu"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Zrušiť"</string>
 </resources>
diff --git a/quickstep/res/values-sl/strings.xml b/quickstep/res/values-sl/strings.xml
index e72dac1..4787f04 100644
--- a/quickstep/res/values-sl/strings.xml
+++ b/quickstep/res/values-sl/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Posnetek zaslona"</string>
     <string name="action_split" msgid="2098009717623550676">"Razdeli"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Za razdeljeni zaslon se dotaknite še 1 aplikacije"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Zapri izbiro razdeljenega zaslona"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Izberite drugo aplikacijo za uporabo razdeljenega zaslona."</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Aplikacija ali vaša organizacija ne dovoljuje tega dejanja"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Želite preskočiti vadnico za krmarjenje?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premakni na dno/desno"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Pokaži še # aplikacijo.}one{Pokaži še # aplikacijo.}two{Pokaži še # aplikaciji.}few{Pokaži še # aplikacije.}other{Pokaži še # aplikacij.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> in <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Dodajanje aplikacije na namizje"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Prekliči"</string>
 </resources>
diff --git a/quickstep/res/values-sq/strings.xml b/quickstep/res/values-sq/strings.xml
index 6f661c3..a9e8b94 100644
--- a/quickstep/res/values-sq/strings.xml
+++ b/quickstep/res/values-sq/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Pamja e ekranit"</string>
     <string name="action_split" msgid="2098009717623550676">"Ndaj"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Trokit një apl. tjetër; përdor ekranin e ndarë"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Dil nga zgjedhja e ekranit të ndarë"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Zgjidh një aplikacion tjetër për të përdorur ekranin e ndarë"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Ky veprim nuk lejohet nga aplikacioni ose organizata jote"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Të kapërcehet udhëzuesi i navigimit?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Lëviz në fund/djathtas"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Shfaq # aplikacion tjetër.}other{Shfaq # aplikacione të tjera.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> dhe <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Shtimi i aplikacionit te desktopi"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Anulo"</string>
 </resources>
diff --git a/quickstep/res/values-sr/strings.xml b/quickstep/res/values-sr/strings.xml
index c985e08..da057d8 100644
--- a/quickstep/res/values-sr/strings.xml
+++ b/quickstep/res/values-sr/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Снимак екрана"</string>
     <string name="action_split" msgid="2098009717623550676">"Подели"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Додирните другу апликацију за подељени екран"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Излазак из бирања подељеног екрана"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Одаберите другу апликацију за подељени екран"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Апликација или организација не дозвољавају ову радњу"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Желите да прескочите водич за кретање?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Премести доле десно"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Прикажи још # апликацију.}one{Прикажи још # апликацију.}few{Прикажи још # апликације.}other{Прикажи још # апликација.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> и <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Додаје се апликација на радну поврршину"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Откажи"</string>
 </resources>
diff --git a/quickstep/res/values-sv/strings.xml b/quickstep/res/values-sv/strings.xml
index 7e97556..31853f9 100644
--- a/quickstep/res/values-sv/strings.xml
+++ b/quickstep/res/values-sv/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Skärmbild"</string>
     <string name="action_split" msgid="2098009717623550676">"Delat"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Tryck på en annan app för att använda delad skärm"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Avsluta val av delad skärm"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Välj en annan app för att använda delad skärm"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Appen eller organisationen tillåter inte den här åtgärden"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Vill du hoppa över självstudierna?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Flytta längst ned/till höger"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Visa # app till.}other{Visa # appar till.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> och <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Lägger till appen på skrivbordet"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Avbryt"</string>
 </resources>
diff --git a/quickstep/res/values-sw/strings.xml b/quickstep/res/values-sw/strings.xml
index 983964b..8c438f4 100644
--- a/quickstep/res/values-sw/strings.xml
+++ b/quickstep/res/values-sw/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Picha ya skrini"</string>
     <string name="action_split" msgid="2098009717623550676">"Iliyogawanywa"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Gusa programu nyingine ili utumie kipengele cha kugawa skrini"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Ondoka kwenye hali ya skrini iliyogawanywa"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Chagua programu nyingine ili utumie hali ya kugawa skrini"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Kitendo hiki hakiruhusiwi na programu au shirika lako"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Ungependa kuruka mafunzo ya usogezaji?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Sogeza chini/kulia"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Onyesha programu # zaidi.}other{Onyesha programu # zaidi.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> na <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Kuweka programu kwenye Eneo-kazi"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Ghairi"</string>
 </resources>
diff --git a/quickstep/res/values-sw720dp/dimens.xml b/quickstep/res/values-sw720dp/dimens.xml
index 9e832bc..1caffb8 100644
--- a/quickstep/res/values-sw720dp/dimens.xml
+++ b/quickstep/res/values-sw720dp/dimens.xml
@@ -43,4 +43,7 @@
     <dimen name="taskbar_app_window_threshold">100dp</dimen>
     <dimen name="taskbar_home_overview_threshold">180dp</dimen>
     <dimen name="taskbar_catch_up_threshold">300dp</dimen>
+
+    <!-- Taskbar swipe up threshold multipliers -->
+    <item name="taskbar_nav_threshold_mult" format="float" type="dimen">3</item>
 </resources>
diff --git a/quickstep/res/values-ta/strings.xml b/quickstep/res/values-ta/strings.xml
index 2016c9f..ef5bd3f 100644
--- a/quickstep/res/values-ta/strings.xml
+++ b/quickstep/res/values-ta/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"ஸ்கிரீன்ஷாட்"</string>
     <string name="action_split" msgid="2098009717623550676">"பிரி"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"திரைப் பிரிப்பைப் பயன்படுத்த வேறு ஆப்ஸைத் தட்டவும்"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"திரைப் பிரிப்பு தேர்வில் இருந்து வெளியேறும்"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"திரைப் பிரிப்பை பயன்படுத்த வேறு ஆப்ஸை தேர்வுசெய்க"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"ஆப்ஸோ உங்கள் நிறுவனமோ இந்த செயலை அனுமதிப்பதில்லை"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"வழிகாட்டுதல் பயிற்சியைத் தவிர்க்கவா?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"கீழே/வலதுபுறம் நகர்த்தும்"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{மேலும் # ஆப்ஸைக் காட்டு.}other{மேலும் # ஆப்ஸைக் காட்டு.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> மற்றும் <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"ஆப்ஸை டெஸ்க்டாப்பில் சேர்க்கிறது"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"ரத்துசெய்"</string>
 </resources>
diff --git a/quickstep/res/values-te/strings.xml b/quickstep/res/values-te/strings.xml
index b9d4b31..a2adfe1 100644
--- a/quickstep/res/values-te/strings.xml
+++ b/quickstep/res/values-te/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"స్క్రీన్‌షాట్"</string>
     <string name="action_split" msgid="2098009717623550676">"స్ప్లిట్ చేయండి"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"స్ప్లిట్ స్క్రీన్ కోసం మరొక యాప్‌ను ట్యాప్ చేయండి"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"స్ప్లిట్ స్క్రీన్ ఎంపిక నుండి ఎగ్జిట్ అవ్వండి"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"స్ప్లిట్ స్క్రీన్ ఉపయోగానికి మరొక యాప్ ఎంచుకోండి"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"ఈ చర్యను యాప్ గానీ, మీ సంస్థ గానీ అనుమతించవు"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"నావిగేషన్ ట్యుటోరియల్‌ను స్కిప్ చేయాలా?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"దిగువ/కుడి వైపునకు తరలించండి"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{మరో # యాప్‌ను చూడండి.}other{మరో # యాప్‌లను చూడండి.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g>, <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"డెస్క్‌టాప్‌నకు యాప్‌ను జోడిస్తోంది"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"రద్దు చేయండి"</string>
 </resources>
diff --git a/quickstep/res/values-th/strings.xml b/quickstep/res/values-th/strings.xml
index 0f921d1..0339046 100644
--- a/quickstep/res/values-th/strings.xml
+++ b/quickstep/res/values-th/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"ภาพหน้าจอ"</string>
     <string name="action_split" msgid="2098009717623550676">"แยก"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"แตะแอปอื่นเพื่อใช้การแยกหน้าจอ"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ออกจากการเลือกโหมดแยกหน้าจอ"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"เลือกแอปอื่นเพื่อใช้การแยกหน้าจอ"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"แอปหรือองค์กรของคุณไม่อนุญาตการดำเนินการนี้"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"ข้ามบทแนะนำการนำทางไหม"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ย้ายไปที่ด้านล่างหรือด้านขวา"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{แสดงเพิ่มเติมอีก # แอป}other{แสดงเพิ่มเติมอีก # แอป}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> และ <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"การเพิ่มแอปไปยังเดสก์ท็อป"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"ยกเลิก"</string>
 </resources>
diff --git a/quickstep/res/values-tl/strings.xml b/quickstep/res/values-tl/strings.xml
index 7185cb0..7bb38c2 100644
--- a/quickstep/res/values-tl/strings.xml
+++ b/quickstep/res/values-tl/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
     <string name="action_split" msgid="2098009717623550676">"Split"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Mag-tap ng ibang app para gamitin ang split screen"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Lumabas sa pagpili ng split screen"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Pumili ng ibang app para gamitin ang split screen"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Hindi pinapayagan ng app o ng iyong organisasyon ang pagkilos na ito"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Laktawan ang tutorial sa pag-navigate?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Ilipat sa ibaba/kanan"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Magpakita ng # pang app.}one{Magpakita ng # pang app.}other{Magpakita ng # pang app.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> at <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Idinaragdag ang app sa Desktop"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Kanselahin"</string>
 </resources>
diff --git a/quickstep/res/values-tr/strings.xml b/quickstep/res/values-tr/strings.xml
index 1648b22..905183a 100644
--- a/quickstep/res/values-tr/strings.xml
+++ b/quickstep/res/values-tr/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Ekran görüntüsü"</string>
     <string name="action_split" msgid="2098009717623550676">"Böl"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Bölünmüş ekran için başka bir uygulamaya dokunun"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Bölünmüş ekran seçiminden çıkın"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Bölünmüş ekran kullanmak için başka bir uygulama seçin"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Uygulamanız veya kuruluşunuz bu işleme izin vermiyor"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Gezinme eğitimi atlansın mı?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Sağ alta taşı"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{# uygulama daha göster.}other{# uygulama daha göster}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> ve <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Uygulama Masaüstü\'ne ekleniyor"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"İptal"</string>
 </resources>
diff --git a/quickstep/res/values-uk/strings.xml b/quickstep/res/values-uk/strings.xml
index 1f91165..6cbf71f 100644
--- a/quickstep/res/values-uk/strings.xml
+++ b/quickstep/res/values-uk/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Знімок екрана"</string>
     <string name="action_split" msgid="2098009717623550676">"Розділити"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Щоб розділити екран, виберіть ще один додаток"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Вийти з режиму розділення екрана"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Щоб розділити екран, виберіть ще один додаток"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Ця дія заборонена додатком або адміністратором організації"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Пропустити посібник із навігації?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Перемістити вниз або вправо"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Показати ще # додаток.}one{Показати ще # додаток.}few{Показати ще # додатки.}many{Показати ще # додатків.}other{Показати ще # додатка.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> та <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Встановлення додатка на комп’ютер"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Скасувати"</string>
 </resources>
diff --git a/quickstep/res/values-ur/strings.xml b/quickstep/res/values-ur/strings.xml
index 504f3aa..2392c6c 100644
--- a/quickstep/res/values-ur/strings.xml
+++ b/quickstep/res/values-ur/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"اسکرین شاٹ"</string>
     <string name="action_split" msgid="2098009717623550676">"اسپلٹ"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"اسپلٹ اسکرین کا استعمال کرنے کیلئے دوسری ایپ پر تھپتھپائیں"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"اسپلٹ اسکرین کے انتخاب سے باہر نکلیں"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"اسپلٹ اسکرین کے استعمال کیلئے دوسری ایپ منتخب کریں"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"ایپ یا آپ کی تنظیم کی جانب سے اس کارروائی کی اجازت نہیں ہے"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"نیویگیشن کا ٹیوٹوریل نظر انداز کریں؟"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"نیچے/دائیں طرف منتقل کریں"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{# مزید ایپ دکھائیں۔}other{# مزید ایپس دکھائیں۔}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> اور <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"ڈیسک ٹاپ پر ایپ شامل کرنا"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"منسوخ کریں"</string>
 </resources>
diff --git a/quickstep/res/values-uz/strings.xml b/quickstep/res/values-uz/strings.xml
index 9f00663..da2d363 100644
--- a/quickstep/res/values-uz/strings.xml
+++ b/quickstep/res/values-uz/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Skrinshot"</string>
     <string name="action_split" msgid="2098009717623550676">"Ajratish"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Ekranni ikkiga ajratish uchun boshqa ilovani bosing"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Ekranni ikkiga ajratish tanlovidan chiqish"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Ekranni ikkiga ajratish uchun boshqa ilovani tanlang"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Bu amal ilova yoki tashkilotingiz tomonidan taqiqlangan"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Navigatsiya darsi yopilsinmi?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Pastga yoki oʻngga oʻtkazish"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Yana # ta ilovani chiqarish}other{Yana # ta ilovani chiqarish}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> va <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Ilova kompyuterga qoʻshilmoqda"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Bekor qilish"</string>
 </resources>
diff --git a/quickstep/res/values-vi/strings.xml b/quickstep/res/values-vi/strings.xml
index 48bc1cb..dae4212 100644
--- a/quickstep/res/values-vi/strings.xml
+++ b/quickstep/res/values-vi/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Chụp ảnh màn hình"</string>
     <string name="action_split" msgid="2098009717623550676">"Chia đôi màn hình"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Nhấn vào ứng dụng khác để chia đôi màn hình"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Thoát khỏi lựa chọn chia đôi màn hình"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Chọn một ứng dụng khác để dùng chế độ chia đôi màn hình"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Ứng dụng hoặc tổ chức của bạn không cho phép thực hiện hành động này"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Bỏ qua phần hướng dẫn thao tác?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Chuyển xuống dưới cùng/sang bên phải"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Hiện thêm # ứng dụng.}other{Hiện thêm # ứng dụng.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> và <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Đang thêm ứng dụng vào máy tính"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Huỷ"</string>
 </resources>
diff --git a/quickstep/res/values-zh-rCN/strings.xml b/quickstep/res/values-zh-rCN/strings.xml
index 80d7525..3980b21 100644
--- a/quickstep/res/values-zh-rCN/strings.xml
+++ b/quickstep/res/values-zh-rCN/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"屏幕截图"</string>
     <string name="action_split" msgid="2098009717623550676">"拆分"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"点按另一个应用即可使用分屏"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"退出分屏选择模式"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"另外选择一个应用才可使用分屏模式"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"该应用或您所在的单位不允许执行此操作"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"要跳过导航教程吗?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"移到底部/右侧"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{显示另外 # 个应用。}other{显示另外 # 个应用。}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g>和<xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"将应用添加到桌面"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"取消"</string>
 </resources>
diff --git a/quickstep/res/values-zh-rHK/strings.xml b/quickstep/res/values-zh-rHK/strings.xml
index 5eaf95e..0532ae5 100644
--- a/quickstep/res/values-zh-rHK/strings.xml
+++ b/quickstep/res/values-zh-rHK/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"螢幕截圖"</string>
     <string name="action_split" msgid="2098009717623550676">"分割"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"輕按其他應用程式以使用分割螢幕"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"退出分割螢幕選取頁面"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"選擇其他應用程式才能使用分割螢幕"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"應用程式或你的機構不允許此操作"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"要略過手勢操作教學課程嗎?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"移至底部/右側"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{顯示另外 # 個應用程式。}other{顯示另外 # 個應用程式。}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"「<xliff:g id="APP_NAME_1">%1$s</xliff:g>」和「<xliff:g id="APP_NAME_2">%2$s</xliff:g>」"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"正在新增應用程式至桌面"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"取消"</string>
 </resources>
diff --git a/quickstep/res/values-zh-rTW/strings.xml b/quickstep/res/values-zh-rTW/strings.xml
index b7e6e06..214fcfd 100644
--- a/quickstep/res/values-zh-rTW/strings.xml
+++ b/quickstep/res/values-zh-rTW/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"螢幕截圖"</string>
     <string name="action_split" msgid="2098009717623550676">"分割"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"輕觸另一個應用程式即可使用分割畫面"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"退出分割畫面選擇器"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"必須選擇另一個應用程式才能使用分割畫面"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"這個應用程式或貴機構不允許執行這個動作"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"要略過手勢操作教學課程嗎?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"移到底部/右側"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{顯示另外 # 個應用程式。}other{顯示另外 # 個應用程式。}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"「<xliff:g id="APP_NAME_1">%1$s</xliff:g>」和「<xliff:g id="APP_NAME_2">%2$s</xliff:g>」"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"新增應用程式至桌面"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"取消"</string>
 </resources>
diff --git a/quickstep/res/values-zu/strings.xml b/quickstep/res/values-zu/strings.xml
index 9ca2106..9655a44 100644
--- a/quickstep/res/values-zu/strings.xml
+++ b/quickstep/res/values-zu/strings.xml
@@ -95,6 +95,7 @@
     <string name="action_screenshot" msgid="8171125848358142917">"Isithombe-skrini"</string>
     <string name="action_split" msgid="2098009717623550676">"Hlukanisa"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Thepha enye i-app ukuze usebenzise isikrini sokuhlukanisa"</string>
+    <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Phuma ekukhetheni ukuhlukaniswa kwesikrini"</string>
     <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Khetha enye i-app ukuze usebenzise ukuhlukanisa isikrini"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"Lesi senzo asivunyelwanga uhlelo lokusebenza noma inhlangano yakho"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Yeqa isifundo sokuzulazula?"</string>
@@ -128,4 +129,6 @@
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Hamba phansi/kwesokudla"</string>
     <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Bonisa i-app e-# ngaphezulu.}one{Bonisa ama-app angu-# ngaphezulu.}other{Bonisa ama-app angu-# ngaphezulu.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"I-<xliff:g id="APP_NAME_1">%1$s</xliff:g> ne-<xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
+    <string name="desktop_select_app_toast" msgid="2306057322833956910">"Yengeza i-app ku-Deskithophu"</string>
+    <string name="desktop_button_close_app_toast" msgid="5283096349579408560">"Khansela"</string>
 </resources>
diff --git a/quickstep/res/values/attrs.xml b/quickstep/res/values/attrs.xml
index fb51919..7288774 100644
--- a/quickstep/res/values/attrs.xml
+++ b/quickstep/res/values/attrs.xml
@@ -26,7 +26,8 @@
      -->
     <declare-styleable name="TaskView">
         <!-- Border color for a keyboard quick switch task views -->
-        <attr name="borderColor" format="color" />
+        <attr name="focusBorderColor" format="color" />
+        <attr name="hoverBorderColor" format="color" />
     </declare-styleable>
 
     <!--
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
index 36c7352..1b5b0ee 100644
--- a/quickstep/res/values/colors.xml
+++ b/quickstep/res/values/colors.xml
@@ -25,6 +25,10 @@
 
     <!-- Taskbar -->
     <color name="taskbar_nav_icon_selection_ripple">#E0E0E0</color>
+    <color name="taskbar_nav_icon_light_color_on_home">#ffffff</color>
+    <!-- The dark navigation button color is only used in the rare cases that taskbar isn't drawing
+    its background and the underlying app has requested dark buttons. -->
+    <color name="taskbar_nav_icon_dark_color_on_home">#99000000</color>
     <color name="taskbar_stashed_handle_light_color">#EBffffff</color>
     <color name="taskbar_stashed_handle_dark_color">#99000000</color>
 
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index bb4f74d..b024418 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -38,6 +38,8 @@
     <dimen name="task_thumbnail_icon_size">48dp</dimen>
     <!--  The icon size for the focused task, placed in center of touch target  -->
     <dimen name="task_thumbnail_icon_drawable_size">44dp</dimen>
+    <!--  The border width shown when task is hovered  -->
+    <dimen name="task_hover_border_width">4dp</dimen>
     <!--  The space under the focused task icon  -->
     <dimen name="overview_task_margin">16dp</dimen>
     <!--  The horizontal space between tasks  -->
@@ -269,6 +271,9 @@
     <dimen name="floating_rotation_button_taskbar_left_margin">20dp</dimen>
     <dimen name="floating_rotation_button_taskbar_bottom_margin">10dp</dimen>
 
+    <!-- Copied from frameworks/base/packages/SystemUI -->
+    <dimen name="navigation_home_handle_width">108dp</dimen>
+
     <!-- Taskbar -->
     <dimen name="taskbar_size">@*android:dimen/taskbar_frame_height</dimen>
     <dimen name="taskbar_ime_size">48dp</dimen>
@@ -323,6 +328,12 @@
     <!-- Taskbar swipe down threshold -->
     <dimen name="taskbar_to_nav_threshold">24dp</dimen>
 
+    <!-- Taskbar swipe up threshold multipliers -->
+    <item name="taskbar_nav_threshold_mult" format="float" type="dimen">4.5</item>
+    <item name="taskbar_app_window_threshold_mult" format="float" type="dimen">10</item>
+    <item name="taskbar_home_overview_threshold_mult" format="float" type="dimen">18</item>
+    <item name="taskbar_catch_up_threshold_mult" format="float" type="dimen">30</item>
+
     <!--  Taskbar 3 button spacing  -->
     <dimen name="taskbar_button_space_inbetween">24dp</dimen>
     <dimen name="taskbar_button_space_inbetween_phone">40dp</dimen>
@@ -356,13 +367,22 @@
     <dimen name="bubblebar_stashed_size">@dimen/transient_taskbar_stashed_height</dimen>
     <dimen name="bubblebar_stashed_handle_height">@dimen/taskbar_stashed_handle_height</dimen>
     <dimen name="bubblebar_pointer_size">8dp</dimen>
+    <dimen name="bubblebar_elevation">1dp</dimen>
 
     <dimen name="bubblebar_icon_size">50dp</dimen>
     <dimen name="bubblebar_badge_size">24dp</dimen>
     <dimen name="bubblebar_icon_overlap">12dp</dimen>
+    <dimen name="bubblebar_overflow_inset">24dp</dimen>
     <dimen name="bubblebar_icon_spacing">3dp</dimen>
     <dimen name="bubblebar_icon_elevation">1dp</dimen>
 
+    <!-- Bubble bar dismiss view -->
+    <dimen name="bubblebar_dismiss_target_size">96dp</dimen>
+    <dimen name="bubblebar_dismiss_target_small_size">60dp</dimen>
+    <dimen name="bubblebar_dismiss_target_icon_size">24dp</dimen>
+    <dimen name="bubblebar_dismiss_target_bottom_margin">50dp</dimen>
+    <dimen name="bubblebar_dismiss_floating_gradient_height">548dp</dimen>
+
     <!-- Launcher splash screen -->
     <!-- Note: keep this value in sync with the WindowManager/Shell dimens.xml -->
     <!--     starting_surface_exit_animation_window_shift_length -->
@@ -383,4 +403,12 @@
     <dimen name="keyboard_quick_switch_task_view_radius">16dp</dimen>
     <dimen name="keyboard_quick_switch_no_recent_items_icon_size">24dp</dimen>
     <dimen name="keyboard_quick_switch_no_recent_items_icon_margin">8dp</dimen>
+
+    <!-- Desktop mode -->
+    <dimen name="desktop_mode_floating_app_select_height">56dp</dimen>
+    <dimen name="desktop_mode_floating_app_select_elevation">4dp</dimen>
+    <dimen name="desktop_mode_floating_app_select_margin">16dp</dimen>
+    <dimen name="desktop_mode_floating_app_select_text_size">14sp</dimen>
+    <dimen name="desktop_mode_floating_app_select_text_margin">8dp</dimen>
+
 </resources>
diff --git a/quickstep/res/values/override.xml b/quickstep/res/values/override.xml
index 4f472f0..df32626 100644
--- a/quickstep/res/values/override.xml
+++ b/quickstep/res/values/override.xml
@@ -25,6 +25,14 @@
 
   <string name="model_delegate_class" translatable="false">com.android.launcher3.model.QuickstepModelDelegate</string>
 
+  <string name="nav_handle_long_press_handler_class" translatable="false"></string>
+
+  <string name="assist_utils_class" translatable="false"></string>
+
   <string name="secondary_display_predictions_class" translatable="false">com.android.launcher3.secondarydisplay.SecondaryDisplayPredictionsImpl</string>
 
+  <string name="taskbar_model_callbacks_factory_class" translatable="false">com.android.launcher3.taskbar.TaskbarModelCallbacksFactory</string>
+
+  <string name="assist_state_manager_class" translatable="false"></string>
+
 </resources>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 6d0dbae..dd9fc63 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -230,6 +230,7 @@
     <string name="action_split">Split</string>
     <!-- Label for toast with instructions for split screen selection mode. [CHAR_LIMIT=50] -->
     <string name="toast_split_select_app">Tap another app to use split screen</string>
+    <string name="toast_split_select_cont_desc">Exit split screen selection</string>
     <!-- Label for toast when app selected for split isn't supported. [CHAR_LIMIT=50] -->
     <string name="toast_split_app_unsupported">Choose another app to use split screen</string>
     <!-- Message shown when an action is blocked by a policy enforced by the app or the organization managing the device. [CHAR_LIMIT=NONE] -->
@@ -308,4 +309,10 @@
 
     <!-- Accessibility label for quick switch tiles showing split tasks [CHAR LIMIT=NONE] -->
     <string name="quick_switch_split_task"><xliff:g id="app_name_1" example="Chrome">%1$s</xliff:g> and <xliff:g id="app_name_2" example="Gmail">%2$s</xliff:g></string>
+
+    <!-- ******* Desktop ******* -->
+    <!-- Text shown in popup to choose a desktop app. [CHAR LIMIT=60] -->
+    <string name="desktop_select_app_toast">Adding app to Desktop</string>
+    <!-- Text shown on a button that closes the popup for choosing a desktop app. [CHAR_LIMIT=40] -->
+    <string name="desktop_button_close_app_toast">Cancel</string>
 </resources>
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index 21b7fd5..fc03704 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -298,4 +298,9 @@
     <style name="rotate_prompt_subtitle" parent="TextAppearance.GestureTutorial.Dialog.Subtitle">
         <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
     </style>
+
+    <style name="ArrowTipTaskbarStyle">
+        <item name="arrowTipBackground">?androidprv:attr/materialColorSurfaceContainer</item>
+        <item name="arrowTipTextColor">?androidprv:attr/materialColorOnSurface</item>
+    </style>
 </resources>
diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java
index 28bd701..f64b5cf 100644
--- a/quickstep/src/com/android/launcher3/LauncherInitListener.java
+++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java
@@ -15,23 +15,16 @@
  */
 package com.android.launcher3;
 
-import android.animation.AnimatorSet;
 import android.annotation.TargetApi;
 import android.os.Build;
-import android.os.CancellationSignal;
-import android.view.RemoteAnimationTarget;
 
-import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.quickstep.util.ActivityInitListener;
-import com.android.quickstep.util.RemoteAnimationProvider;
 
 import java.util.function.BiPredicate;
 
 @TargetApi(Build.VERSION_CODES.P)
 public class LauncherInitListener extends ActivityInitListener<Launcher> {
 
-    private RemoteAnimationProvider mRemoteAnimationProvider;
-
     /**
      * @param onInitListener a callback made when the activity is initialized. The callback should
      *                       return true to continue receiving callbacks (ie. for if the activity is
@@ -43,37 +36,7 @@
 
     @Override
     public boolean handleInit(Launcher launcher, boolean alreadyOnHome) {
-        if (mRemoteAnimationProvider != null) {
-            QuickstepTransitionManager appTransitionManager =
-                    ((QuickstepLauncher) launcher).getAppTransitionManager();
-
-            // Set a one-time animation provider. After the first call, this will get cleared.
-            // TODO: Probably also check the intended target id.
-            CancellationSignal cancellationSignal = new CancellationSignal();
-            appTransitionManager.setRemoteAnimationProvider(new RemoteAnimationProvider() {
-                @Override
-                public AnimatorSet createWindowAnimation(RemoteAnimationTarget[] appTargets,
-                        RemoteAnimationTarget[] wallpaperTargets) {
-
-                    // On the first call clear the reference.
-                    cancellationSignal.cancel();
-                    RemoteAnimationProvider provider = mRemoteAnimationProvider;
-                    mRemoteAnimationProvider = null;
-
-                    if (provider != null && launcher.getStateManager().getState().overviewUi) {
-                        return provider.createWindowAnimation(appTargets, wallpaperTargets);
-                    }
-                    return null;
-                }
-            }, cancellationSignal);
-        }
         launcher.deferOverlayCallbacksUntilNextResumeOrStop();
         return super.handleInit(launcher, alreadyOnHome);
     }
-
-    @Override
-    public void unregister() {
-        mRemoteAnimationProvider = null;
-        super.unregister();
-    }
 }
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index f3e62c1..741249d 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -31,6 +31,12 @@
 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
 import static android.window.TransitionFilter.CONTAINER_ORDER_TOP;
 
+import static com.android.app.animation.Interpolators.ACCELERATE_1_5;
+import static com.android.app.animation.Interpolators.AGGRESSIVE_EASE;
+import static com.android.app.animation.Interpolators.DECELERATE_1_5;
+import static com.android.app.animation.Interpolators.DECELERATE_1_7;
+import static com.android.app.animation.Interpolators.EXAGGERATED_EASE;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
@@ -42,12 +48,6 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.Utilities.mapBoundToRange;
-import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
-import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
-import static com.android.launcher3.anim.Interpolators.EXAGGERATED_EASE;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_BACK_SWIPE_HOME_ANIMATION;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_SCRIM_FOR_APP_LAUNCH;
 import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION;
@@ -60,6 +60,8 @@
 import static com.android.launcher3.views.FloatingIconView.getFloatingIconView;
 import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
 import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
+import static com.android.systemui.shared.system.InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME;
+import static com.android.systemui.shared.system.InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME_FALLBACK;
 import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
 import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
 
@@ -82,7 +84,6 @@
 import android.graphics.RectF;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
-import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -140,7 +141,6 @@
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.RectFSpringAnim.DefaultSpringConfig;
 import com.android.quickstep.util.RectFSpringAnim.TaskbarHotseatSpringConfig;
-import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
 import com.android.quickstep.util.SurfaceTransaction;
 import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
@@ -151,6 +151,7 @@
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.DelegateLaunchAnimatorController;
+import com.android.systemui.animation.LaunchableView;
 import com.android.systemui.animation.RemoteAnimationDelegate;
 import com.android.systemui.shared.system.BlurUtils;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
@@ -183,7 +184,7 @@
     private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
             "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS";
 
-    private static final long APP_LAUNCH_DURATION = 500;
+    public static final long APP_LAUNCH_DURATION = 500;
 
     private static final long APP_LAUNCH_ALPHA_DURATION = 50;
     private static final long APP_LAUNCH_ALPHA_START_DELAY = 25;
@@ -229,7 +230,6 @@
 
     private DeviceProfile mDeviceProfile;
 
-    private RemoteAnimationProvider mRemoteAnimationProvider;
     // Strong refs to runners which are cleared when the launcher activity is destroyed
     private RemoteAnimationFactory mWallpaperOpenRunner;
     private RemoteAnimationFactory mAppLaunchRunner;
@@ -286,7 +286,7 @@
 
         mOpeningXInterpolator = AnimationUtils.loadInterpolator(context, R.interpolator.app_open_x);
         mOpeningInterpolator = AnimationUtils.loadInterpolator(context,
-                R.interpolator.three_point_fast_out_extra_slow_in);
+                R.interpolator.emphasized_interpolator);
     }
 
     @Override
@@ -468,16 +468,6 @@
         return bounds;
     }
 
-    public void setRemoteAnimationProvider(final RemoteAnimationProvider animationProvider,
-            CancellationSignal cancellationSignal) {
-        mRemoteAnimationProvider = animationProvider;
-        cancellationSignal.setOnCancelListener(() -> {
-            if (animationProvider == mRemoteAnimationProvider) {
-                mRemoteAnimationProvider = null;
-            }
-        });
-    }
-
     /** Dump debug logs to bug report. */
     public void dump(@NonNull String prefix, @NonNull PrintWriter printWriter) {}
 
@@ -566,7 +556,7 @@
 
                 ObjectAnimator scaleAnim = ObjectAnimator.ofFloat(view, SCALE_PROPERTY, scales)
                         .setDuration(CONTENT_SCALE_DURATION);
-                scaleAnim.setInterpolator(DEACCEL_1_5);
+                scaleAnim.setInterpolator(DECELERATE_1_5);
                 launcherAnimator.play(scaleAnim);
             });
 
@@ -584,7 +574,7 @@
                     ObjectAnimator scrim = ObjectAnimator.ofArgb(scrimView, VIEW_BACKGROUND_COLOR,
                             colors);
                     scrim.setDuration(CONTENT_SCRIM_DURATION);
-                    scrim.setInterpolator(DEACCEL_1_5);
+                    scrim.setInterpolator(DECELERATE_1_5);
 
                     launcherAnimator.play(scrim);
                 }
@@ -1234,7 +1224,7 @@
      * ie. pressing home, swiping up from nav bar.
      */
     RemoteAnimationFactory createWallpaperOpenRunner(boolean fromUnlock) {
-        return new WallpaperOpenLauncherAnimationRunner(mHandler, fromUnlock);
+        return new WallpaperOpenLauncherAnimationRunner(fromUnlock);
     }
 
     /**
@@ -1352,7 +1342,7 @@
     /**
      * Closing animator that animates the window into its final location on the workspace.
      */
-    private RectFSpringAnim getClosingWindowAnimators(AnimatorSet animation,
+    protected RectFSpringAnim getClosingWindowAnimators(AnimatorSet animation,
             RemoteAnimationTarget[] targets, View launcherView, PointF velocityPxPerS,
             RectF closingWindowStartRect, float startWindowCornerRadius) {
         FloatingIconView floatingIconView = null;
@@ -1477,11 +1467,11 @@
         float startShadowRadius = areAllTargetsTranslucent(appTargets) ? 0 : mMaxShadowRadius;
         closingAnimator.setDuration(duration);
         closingAnimator.addUpdateListener(new MultiValueUpdateListener() {
-            FloatProp mDy = new FloatProp(0, mClosingWindowTransY, 0, duration, DEACCEL_1_7);
-            FloatProp mScale = new FloatProp(1f, 1f, 0, duration, DEACCEL_1_7);
+            FloatProp mDy = new FloatProp(0, mClosingWindowTransY, 0, duration, DECELERATE_1_7);
+            FloatProp mScale = new FloatProp(1f, 1f, 0, duration, DECELERATE_1_7);
             FloatProp mAlpha = new FloatProp(1f, 0f, 25, 125, LINEAR);
             FloatProp mShadowRadius = new FloatProp(startShadowRadius, 0, 0, duration,
-                    DEACCEL_1_7);
+                    DECELERATE_1_7);
 
             @Override
             public void onUpdate(float percent, boolean initOnly) {
@@ -1592,89 +1582,80 @@
             RectF startRect,
             float startWindowCornerRadius,
             boolean fromPredictiveBack) {
-        AnimatorSet anim = null;
+        AnimatorSet anim = new AnimatorSet();
         RectFSpringAnim rectFSpringAnim = null;
 
-        RemoteAnimationProvider provider = mRemoteAnimationProvider;
-        if (provider != null) {
-            anim = provider.createWindowAnimation(appTargets, wallpaperTargets);
+        final boolean launcherIsForceInvisibleOrOpening = mLauncher.isForceInvisible()
+                || launcherIsATargetWithMode(appTargets, MODE_OPENING);
+
+        View launcherView = findLauncherView(appTargets);
+        boolean playFallBackAnimation = (launcherView == null
+                && launcherIsForceInvisibleOrOpening)
+                || mLauncher.getWorkspace().isOverlayShown()
+                || shouldPlayFallbackClosingAnimation(appTargets);
+
+        boolean playWorkspaceReveal = true;
+        boolean skipAllAppsScale = false;
+        if (fromUnlock) {
+            anim.play(getUnlockWindowAnimator(appTargets, wallpaperTargets));
+        } else if (ENABLE_BACK_SWIPE_HOME_ANIMATION.get()
+                && !playFallBackAnimation) {
+            // Use a fixed velocity to start the animation.
+            float velocityPxPerS = DynamicResource.provider(mLauncher)
+                    .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s);
+            PointF velocity = new PointF(0, -velocityPxPerS);
+            rectFSpringAnim = getClosingWindowAnimators(
+                    anim, appTargets, launcherView, velocity, startRect,
+                    startWindowCornerRadius);
+            if (mLauncher.isInState(LauncherState.ALL_APPS)) {
+                // Skip scaling all apps, otherwise FloatingIconView will get wrong
+                // layout bounds.
+                skipAllAppsScale = true;
+            } else if (!fromPredictiveBack) {
+                anim.play(new StaggeredWorkspaceAnim(mLauncher, velocity.y,
+                        true /* animateOverviewScrim */, launcherView).getAnimators());
+
+                if (!areAllTargetsTranslucent(appTargets)) {
+                    anim.play(ObjectAnimator.ofFloat(mLauncher.getDepthController().stateDepth,
+                            MULTI_PROPERTY_VALUE,
+                            BACKGROUND_APP.getDepth(mLauncher), NORMAL.getDepth(mLauncher)));
+                }
+
+                // We play StaggeredWorkspaceAnim as a part of the closing window animation.
+                playWorkspaceReveal = false;
+            }
+        } else {
+            anim.play(getFallbackClosingWindowAnimators(appTargets));
         }
 
-        if (anim == null) {
-            anim = new AnimatorSet();
+        // Normally, we run the launcher content animation when we are transitioning
+        // home, but if home is already visible, then we don't want to animate the
+        // contents of launcher unless we know that we are animating home as a result
+        // of the home button press with quickstep, which will result in launcher being
+        // started on touch down, prior to the animation home (and won't be in the
+        // targets list because it is already visible). In that case, we force
+        // invisibility on touch down, and only reset it after the animation to home
+        // is initialized.
+        if (launcherIsForceInvisibleOrOpening) {
+            addCujInstrumentation(anim, playFallBackAnimation
+                    ? CUJ_APP_CLOSE_TO_HOME_FALLBACK :  CUJ_APP_CLOSE_TO_HOME);
+            // Only register the content animation for cancellation when state changes
+            mLauncher.getStateManager().setCurrentAnimation(anim);
 
-            final boolean launcherIsForceInvisibleOrOpening = mLauncher.isForceInvisible()
-                    || launcherIsATargetWithMode(appTargets, MODE_OPENING);
-
-            View launcherView = findLauncherView(appTargets);
-            boolean playFallBackAnimation = (launcherView == null
-                    && launcherIsForceInvisibleOrOpening)
-                    || mLauncher.getWorkspace().isOverlayShown()
-                    || shouldPlayFallbackClosingAnimation(appTargets);
-
-            boolean playWorkspaceReveal = true;
-            boolean skipAllAppsScale = false;
-            if (fromUnlock) {
-                anim.play(getUnlockWindowAnimator(appTargets, wallpaperTargets));
-            } else if (ENABLE_BACK_SWIPE_HOME_ANIMATION.get()
-                    && !playFallBackAnimation) {
-                // Use a fixed velocity to start the animation.
-                float velocityPxPerS = DynamicResource.provider(mLauncher)
-                        .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s);
-                PointF velocity = new PointF(0, -velocityPxPerS);
-                rectFSpringAnim = getClosingWindowAnimators(
-                        anim, appTargets, launcherView, velocity, startRect,
-                        startWindowCornerRadius);
-                if (mLauncher.isInState(LauncherState.ALL_APPS)) {
-                    // Skip scaling all apps, otherwise FloatingIconView will get wrong
-                    // layout bounds.
-                    skipAllAppsScale = true;
-                } else if (!fromPredictiveBack) {
-                    anim.play(new StaggeredWorkspaceAnim(mLauncher, velocity.y,
-                            true /* animateOverviewScrim */, launcherView).getAnimators());
-
-                    if (!areAllTargetsTranslucent(appTargets)) {
-                        anim.play(ObjectAnimator.ofFloat(mLauncher.getDepthController().stateDepth,
-                                MULTI_PROPERTY_VALUE,
-                                BACKGROUND_APP.getDepth(mLauncher), NORMAL.getDepth(mLauncher)));
+            if (mLauncher.isInState(LauncherState.ALL_APPS)) {
+                Pair<AnimatorSet, Runnable> contentAnimator =
+                        getLauncherContentAnimator(false, LAUNCHER_RESUME_START_DELAY,
+                                skipAllAppsScale);
+                anim.play(contentAnimator.first);
+                anim.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        contentAnimator.second.run();
                     }
-
-                    // We play StaggeredWorkspaceAnim as a part of the closing window animation.
-                    playWorkspaceReveal = false;
-                }
+                });
             } else {
-                anim.play(getFallbackClosingWindowAnimators(appTargets));
-            }
-
-            // Normally, we run the launcher content animation when we are transitioning
-            // home, but if home is already visible, then we don't want to animate the
-            // contents of launcher unless we know that we are animating home as a result
-            // of the home button press with quickstep, which will result in launcher being
-            // started on touch down, prior to the animation home (and won't be in the
-            // targets list because it is already visible). In that case, we force
-            // invisibility on touch down, and only reset it after the animation to home
-            // is initialized.
-            if (launcherIsForceInvisibleOrOpening) {
-                addCujInstrumentation(
-                        anim, InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
-                // Only register the content animation for cancellation when state changes
-                mLauncher.getStateManager().setCurrentAnimation(anim);
-
-                if (mLauncher.isInState(LauncherState.ALL_APPS)) {
-                    Pair<AnimatorSet, Runnable> contentAnimator =
-                            getLauncherContentAnimator(false, LAUNCHER_RESUME_START_DELAY,
-                                    skipAllAppsScale);
-                    anim.play(contentAnimator.first);
-                    anim.addListener(new AnimatorListenerAdapter() {
-                        @Override
-                        public void onAnimationEnd(Animator animation) {
-                            contentAnimator.second.run();
-                        }
-                    });
-                } else {
-                    if (playWorkspaceReveal) {
-                        anim.play(new WorkspaceRevealAnim(mLauncher, false).getAnimators());
-                    }
+                if (playWorkspaceReveal) {
+                    anim.play(new WorkspaceRevealAnim(mLauncher, false).getAnimators());
                 }
             }
         }
@@ -1687,11 +1668,9 @@
      */
     protected class WallpaperOpenLauncherAnimationRunner implements RemoteAnimationFactory {
 
-        private final Handler mHandler;
         private final boolean mFromUnlock;
 
-        public WallpaperOpenLauncherAnimationRunner(Handler handler, boolean fromUnlock) {
-            mHandler = handler;
+        public WallpaperOpenLauncherAnimationRunner(boolean fromUnlock) {
             mFromUnlock = fromUnlock;
         }
 
@@ -1799,7 +1778,7 @@
         @Nullable
         private static ContainerAnimationRunner from(
                 View v, StartingWindowListener startingWindowListener, RunnableList onEndCallback) {
-            View viewToUse = findViewWithBackground(v);
+            View viewToUse = findLaunchableViewWithBackground(v);
             if (viewToUse == null) {
                 viewToUse = v;
             }
@@ -1837,19 +1816,23 @@
                     new ActivityLaunchAnimator.AnimationDelegate(controller, callback, listener));
         }
 
-        /** Finds the closest parent of [view] (inclusive) with a background drawable. */
+        /**
+         * Finds the closest parent of [view] (inclusive) that implements {@link LaunchableView} and
+         * has a background drawable.
+         */
         @Nullable
-        private static View findViewWithBackground(View view) {
+        private static <T extends View & LaunchableView> T findLaunchableViewWithBackground(
+                View view) {
             View current = view;
-            while (current.getBackground() == null) {
+            while (current.getBackground() == null || !(current instanceof LaunchableView)) {
                 if (!(current.getParent() instanceof View)) {
                     return null;
                 }
 
-                current = (View) view.getParent();
+                current = (View) current.getParent();
             }
 
-            return current;
+            return (T) current;
         }
 
         @Override
@@ -2047,7 +2030,7 @@
             if (progress >= end) {
                 return 0f;
             }
-            return Utilities.mapToRange(progress, start, end, 1, 0, ACCEL_1_5);
+            return Utilities.mapToRange(progress, start, end, 1, 0, ACCELERATE_1_5);
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
index 34316db..f82474a 100644
--- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -36,13 +36,11 @@
 import com.android.launcher3.allapps.FloatingHeaderRow;
 import com.android.launcher3.allapps.FloatingHeaderView;
 import com.android.launcher3.anim.AlphaUpdateListener;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.keyboard.FocusIndicatorHelper;
 import com.android.launcher3.keyboard.FocusIndicatorHelper.SimpleFocusIndicatorHelper;
 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.ItemLongClickListener;
 import com.android.launcher3.views.ActivityContext;
 
 import java.util.ArrayList;
@@ -65,7 +63,6 @@
     private FloatingHeaderView mParent;
 
     private boolean mPredictionsEnabled = false;
-    private OnLongClickListener mOnIconLongClickListener = ItemLongClickListener.INSTANCE_ALL_APPS;
 
     public PredictionRowView(@NonNull Context context) {
         this(context, null);
@@ -106,22 +103,12 @@
                 mActivityContext.getAppsView().getAppsStore().unregisterIconContainer(this);
             }
         }
-
-        // Set the predicted row in All Apps' text line to 1.
-        if (FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get()
-                || FeatureFlags.ENABLE_TWOLINE_DEVICESEARCH.get()) {
-            for (int i = 0; i < getChildCount(); i++) {
-                BubbleTextView icon = (BubbleTextView) getChildAt(i);
-                icon.setMaxLines(1);
-            }
-        }
     }
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(getExpectedHeight(),
                 MeasureSpec.EXACTLY));
-        updateVisibility();
     }
 
     @Override
@@ -185,15 +172,6 @@
         applyPredictionApps();
     }
 
-    /**
-     * Sets the long click listener for predictions for any future predictions.
-     *
-     * Existing predictions in the container are not updated with this new callback.
-     */
-    public void setOnIconLongClickListener(OnLongClickListener onIconLongClickListener) {
-        mOnIconLongClickListener = onIconLongClickListener;
-    }
-
     @Override
     public void onDeviceProfileChanged(DeviceProfile dp) {
         mNumPredictedAppsPerRow = dp.numShownAllAppsColumns;
@@ -209,9 +187,9 @@
             LayoutInflater inflater = mActivityContext.getAppsView().getLayoutInflater();
             while (getChildCount() < mNumPredictedAppsPerRow) {
                 BubbleTextView icon = (BubbleTextView) inflater.inflate(
-                        R.layout.all_apps_icon, this, false);
+                        R.layout.all_apps_prediction_row_icon, this, false);
                 icon.setOnClickListener(mActivityContext.getItemOnClickListener());
-                icon.setOnLongClickListener(mOnIconLongClickListener);
+                icon.setOnLongClickListener(mActivityContext.getAllAppsItemLongClickListener());
                 icon.setLongPressTimeoutFactor(1f);
                 icon.setOnFocusChangeListener(mFocusHelper);
 
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
index 048243e..b77c43f 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
@@ -95,9 +95,7 @@
             }
         }
         if (pageId == -1) {
-            pageId = LauncherSettings.Settings.call(mLauncher.getContentResolver(),
-                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
-                    .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+            pageId = mLauncher.getModel().getModelDbController().getNewScreenId();
             mNewScreens = IntArray.wrap(pageId);
         }
         boolean isPortrait = !mLauncher.getDeviceProfile().isVerticalBarLayout();
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
index 80bdb6f..db225be 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
@@ -19,7 +19,6 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_DENY;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_SEEN;
 
-import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Rect;
@@ -37,7 +36,6 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.uioverrides.PredictedAppIcon;
@@ -160,14 +158,11 @@
     }
 
     private void animateOpen() {
-        if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+        if (mIsOpen || mOpenCloseAnimation.getAnimationPlayer().isRunning()) {
             return;
         }
         mIsOpen = true;
-        mOpenCloseAnimator.setValues(
-                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-        mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-        mOpenCloseAnimator.start();
+        setUpDefaultOpenAnimation().start();
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index 85d0ab5..a63f9e8 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -22,11 +22,13 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_PREDICTION_PINNED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_RANKED;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.FlagDebugUtils.appendFlag;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.content.ComponentName;
+import android.util.Log;
 import android.view.HapticFeedbackConstants;
 import android.view.View;
 import android.view.ViewGroup;
@@ -60,9 +62,11 @@
 import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.views.Snackbar;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.StringJoiner;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
@@ -74,6 +78,7 @@
         SystemShortcut.Factory<QuickstepLauncher>, DeviceProfile.OnDeviceProfileChangeListener,
         DragSource, ViewGroup.OnHierarchyChangeListener {
 
+    private static final String TAG = "HotseatPredictionController";
     private static final int FLAG_UPDATE_PAUSED = 1 << 0;
     private static final int FLAG_DRAG_IN_PROGRESS = 1 << 1;
     private static final int FLAG_FILL_IN_PROGRESS = 1 << 2;
@@ -114,8 +119,7 @@
         WorkspaceItemInfo dragItem = new WorkspaceItemInfo((WorkspaceItemInfo) v.getTag());
         v.setVisibility(View.INVISIBLE);
         mLauncher.getWorkspace().beginDragShared(
-                v, null, this, dragItem, new DragPreviewProvider(v),
-                mLauncher.getDefaultWorkspaceDragOptions());
+                v, null, this, dragItem, new DragPreviewProvider(v), new DragOptions());
         return true;
     };
 
@@ -184,6 +188,7 @@
     }
 
     private void fillGapsWithPrediction(boolean animate) {
+        Log.d(TAG, "fillGapsWithPrediction flags: " + getStateString(mPauseFlags));
         if (mPauseFlags != 0) {
             return;
         }
@@ -208,12 +213,16 @@
             View child = mHotseat.getChildAt(
                     mHotseat.getCellXFromOrder(rank),
                     mHotseat.getCellYFromOrder(rank));
+            Log.d(TAG, "Hotseat app child is: " + child + " and isPredictedIcon() evaluates to"
+                    + ": " + isPredictedIcon(child));
 
             if (child != null && !isPredictedIcon(child)) {
                 continue;
             }
             if (mPredictedItems.size() <= predictionIndex) {
                 // Remove predicted apps from the past
+                Log.d(TAG, "Remove predicted apps from the past\nPrediction Index: "
+                        + predictionIndex);
                 if (isPredictedIcon(child)) {
                     mHotseat.removeView(child);
                 }
@@ -221,6 +230,11 @@
             }
             WorkspaceItemInfo predictedItem =
                     (WorkspaceItemInfo) mPredictedItems.get(predictionIndex++);
+            Log.d(TAG, "Predicted item is: " + predictedItem);
+            if (child != null) {
+                Log.d(TAG, "Predicted item is enabled: " + child.isEnabled());
+            }
+
             if (isPredictedIcon(child) && child.isEnabled()) {
                 PredictedAppIcon icon = (PredictedAppIcon) child;
                 boolean animateIconChange = icon.shouldAnimateIconChange(predictedItem);
@@ -240,6 +254,7 @@
     }
 
     private void bindItems(List<WorkspaceItemInfo> itemsToAdd, boolean animate) {
+        Log.d(TAG, "bindItems to hotseat: " + itemsToAdd);
         AnimatorSet animationSet = new AnimatorSet();
         for (WorkspaceItemInfo item : itemsToAdd) {
             PredictedAppIcon icon = PredictedAppIcon.createIcon(mHotseat, item);
@@ -279,6 +294,7 @@
      * start and pauses predicted apps update on the hotseat
      */
     public void setPauseUIUpdate(boolean paused) {
+        Log.d(TAG, "setPauseUIUpdate parameter `paused` is " + paused);
         mPauseFlags = paused
                 ? (mPauseFlags | FLAG_UPDATE_PAUSED)
                 : (mPauseFlags & ~FLAG_UPDATE_PAUSED);
@@ -293,8 +309,10 @@
     public void setPredictedItems(FixedContainerItems items) {
         mPredictedItems = new ArrayList(items.items);
         if (mPredictedItems.isEmpty()) {
+            Log.d(TAG, "Predicted items is initially empty");
             HotseatRestoreHelper.restoreBackup(mLauncher);
         }
+        Log.d(TAG, "Predicted items: " + mPredictedItems);
         fillGapsWithPrediction();
     }
 
@@ -501,4 +519,21 @@
                 && ((WorkspaceItemInfo) view.getTag()).container
                 == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
     }
+
+    private static String getStateString(int flags) {
+        StringJoiner str = new StringJoiner("|");
+        appendFlag(str, flags, FLAG_UPDATE_PAUSED, "FLAG_UPDATE_PAUSED");
+        appendFlag(str, flags, FLAG_DRAG_IN_PROGRESS, "FLAG_DRAG_IN_PROGRESS");
+        appendFlag(str, flags, FLAG_FILL_IN_PROGRESS, "FLAG_FILL_IN_PROGRESS");
+        appendFlag(str, flags, FLAG_REMOVING_PREDICTED_ICON,
+                "FLAG_REMOVING_PREDICTED_ICON");
+        return str.toString();
+    }
+
+    public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + this.getClass().getSimpleName());
+        writer.println(prefix + "\tFlags: " + getStateString(mPauseFlags));
+        writer.println(prefix + "\tmHotSeatItemsCount: " + mHotSeatItemsCount);
+        writer.println(prefix + "\tmPredictedItems: " + mPredictedItems);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
index 726abff..8c01d04 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
@@ -22,9 +22,10 @@
 import android.content.Context;
 
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherModel;
 import com.android.launcher3.model.GridBackupTable;
-import com.android.launcher3.provider.LauncherDbUtils;
+import com.android.launcher3.model.ModelDbController;
+import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 
 /**
  * A helper class to manage migration revert restoration for hybrid hotseat
@@ -36,16 +37,13 @@
      */
     public static void createBackup(Context context) {
         MODEL_EXECUTOR.execute(() -> {
-            try (LauncherDbUtils.SQLiteTransaction transaction = (LauncherDbUtils.SQLiteTransaction)
-                    LauncherSettings.Settings.call(
-                            context.getContentResolver(),
-                            LauncherSettings.Settings.METHOD_NEW_TRANSACTION)
-                            .getBinder(LauncherSettings.Settings.EXTRA_VALUE)) {
+            ModelDbController dbController = LauncherAppState.getInstance(context)
+                    .getModel().getModelDbController();
+            try (SQLiteTransaction transaction = dbController.newTransaction()) {
                 GridBackupTable backupTable = new GridBackupTable(context, transaction.getDb());
                 backupTable.createCustomBackupTable(HYBRID_HOTSEAT_BACKUP_TABLE);
                 transaction.commit();
-                LauncherSettings.Settings.call(context.getContentResolver(),
-                        LauncherSettings.Settings.METHOD_REFRESH_HOTSEAT_RESTORE_TABLE);
+                dbController.refreshHotseatRestoreTable();
             }
         });
     }
@@ -55,18 +53,15 @@
      */
     public static void restoreBackup(Context context) {
         MODEL_EXECUTOR.execute(() -> {
-            try (LauncherDbUtils.SQLiteTransaction transaction = (LauncherDbUtils.SQLiteTransaction)
-                    LauncherSettings.Settings.call(
-                            context.getContentResolver(),
-                            LauncherSettings.Settings.METHOD_NEW_TRANSACTION)
-                            .getBinder(LauncherSettings.Settings.EXTRA_VALUE)) {
+            LauncherModel model = LauncherAppState.getInstance(context).getModel();
+            try (SQLiteTransaction transaction = model.getModelDbController().newTransaction()) {
                 if (!tableExists(transaction.getDb(), HYBRID_HOTSEAT_BACKUP_TABLE)) {
                     return;
                 }
                 GridBackupTable backupTable = new GridBackupTable(context, transaction.getDb());
                 backupTable.restoreFromCustomBackupTable(HYBRID_HOTSEAT_BACKUP_TABLE, true);
                 transaction.commit();
-                LauncherAppState.getInstance(context).getModel().forceReload();
+                model.forceReload();
             }
         });
     }
diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
index bc97df1..32361a8 100644
--- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -52,6 +52,7 @@
 import androidx.annotation.CallSuper;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.InvariantDeviceProfile;
@@ -93,11 +94,14 @@
     private static final boolean IS_DEBUG = false;
     private static final String TAG = "QuickstepModelDelegate";
 
-    private final PredictorState mAllAppsState =
+    @VisibleForTesting
+    final PredictorState mAllAppsState =
             new PredictorState(CONTAINER_PREDICTION, "all_apps_predictions");
-    private final PredictorState mHotseatState =
+    @VisibleForTesting
+    final PredictorState mHotseatState =
             new PredictorState(CONTAINER_HOTSEAT_PREDICTION, "hotseat_predictions");
-    private final PredictorState mWidgetsRecommendationState =
+    @VisibleForTesting
+    final PredictorState mWidgetsRecommendationState =
             new PredictorState(CONTAINER_WIDGETS_PREDICTION, "widgets_prediction");
 
     private final InvariantDeviceProfile mIDP;
@@ -348,12 +352,7 @@
                         .build()));
 
         // TODO: get bundle
-        registerPredictor(mHotseatState, apm.createAppPredictionSession(
-                new AppPredictionContext.Builder(context)
-                        .setUiSurface("hotseat")
-                        .setPredictedTargetCount(mIDP.numDatabaseHotseatIcons)
-                        .setExtras(convertDataModelToAppTargetBundle(context, mDataModel))
-                        .build()));
+        registerHotseatPredictor(apm, context);
 
         registerWidgetsPredictor(apm.createAppPredictionSession(
                 new AppPredictionContext.Builder(context)
@@ -363,6 +362,29 @@
                         .build()));
     }
 
+    @WorkerThread
+    private void recreateHotseatPredictor() {
+        mHotseatState.destroyPredictor();
+        if (!mActive) {
+            return;
+        }
+        Context context = mApp.getContext();
+        AppPredictionManager apm = context.getSystemService(AppPredictionManager.class);
+        if (apm == null) {
+            return;
+        }
+        registerHotseatPredictor(apm, context);
+    }
+
+    private void registerHotseatPredictor(AppPredictionManager apm, Context context) {
+        registerPredictor(mHotseatState, apm.createAppPredictionSession(
+                new AppPredictionContext.Builder(context)
+                        .setUiSurface("hotseat")
+                        .setPredictedTargetCount(mIDP.numDatabaseHotseatIcons)
+                        .setExtras(convertDataModelToAppTargetBundle(context, mDataModel))
+                        .build()));
+    }
+
     private void registerPredictor(PredictorState state, AppPredictor predictor) {
         state.setTargets(Collections.emptyList());
         state.predictor = predictor;
@@ -393,7 +415,8 @@
         mWidgetsRecommendationState.predictor.requestPredictionUpdate();
     }
 
-    private void onAppTargetEvent(AppTargetEvent event, int client) {
+    @VisibleForTesting
+    void onAppTargetEvent(AppTargetEvent event, int client) {
         PredictorState state;
         switch(client) {
             case CONTAINER_PREDICTION:
@@ -411,6 +434,13 @@
             state.predictor.notifyAppTargetEvent(event);
             Log.d(TAG, "notifyAppTargetEvent action=" + event.getAction()
                     + " launchLocation=" + event.getLaunchLocation());
+            if (state == mHotseatState
+                    && (event.getAction() == AppTargetEvent.ACTION_PIN
+                            || event.getAction() == AppTargetEvent.ACTION_UNPIN)) {
+                // Recreate hot seat predictor when we need to query for hot seat due to pin or
+                // unpin app icons.
+                recreateHotseatPredictor();
+            }
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayPredictionsImpl.java b/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayPredictionsImpl.java
index 8720bd8..b982688 100644
--- a/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayPredictionsImpl.java
+++ b/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayPredictionsImpl.java
@@ -18,9 +18,7 @@
 import static com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT;
 
 import android.content.Context;
-import android.view.View;
 
-import com.android.launcher3.allapps.ActivityAllAppsContainerView;
 import com.android.launcher3.appprediction.AppsDividerView;
 import com.android.launcher3.appprediction.PredictionRowView;
 import com.android.launcher3.model.BgDataModel;
@@ -56,12 +54,4 @@
                 .findFixedRowByType(PredictionRowView.class)
                 .setPredictedApps(item.items);
     }
-
-    @Override
-    public void setLongClickListener(ActivityAllAppsContainerView<?> appsView,
-            View.OnLongClickListener onIconLongClickListener) {
-        appsView.getFloatingHeaderView()
-                .findFixedRowByType(PredictionRowView.class)
-                .setOnIconLongClickListener(onIconLongClickListener);
-    }
 }
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
index 70aa5d7..882682d 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
@@ -16,7 +16,7 @@
 
 package com.android.launcher3.statehandlers;
 
-import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
@@ -193,5 +193,6 @@
         writer.println(prefix + "\tmIgnoreStateChangesDuringMultiWindowAnimation="
                 + mIgnoreStateChangesDuringMultiWindowAnimation);
         writer.println(prefix + "\tmPauseBlurs=" + mPauseBlurs);
+        writer.println(prefix + "\tmWaitingOnSurfaceValidity=" + mWaitingOnSurfaceValidity);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index d087d39..ecf483c 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -15,14 +15,22 @@
  */
 package com.android.launcher3.statehandlers;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import android.os.SystemProperties;
 import android.util.Log;
 import android.view.View;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.views.DesktopAppSelectView;
+import com.android.wm.shell.desktopmode.IDesktopTaskListener;
 
 /**
  * Controls the visibility of the workspace and the resumed / paused state when desktop mode
@@ -32,18 +40,69 @@
 
     private static final String TAG = "DesktopVisController";
     private static final boolean DEBUG = false;
-
+    private static final boolean IS_STASHING_ENABLED = SystemProperties.getBoolean(
+            "persist.wm.debug.desktop_stashing", false);
     private final Launcher mLauncher;
 
     private boolean mFreeformTasksVisible;
     private boolean mInOverviewState;
     private boolean mGestureInProgress;
 
+    @Nullable
+    private IDesktopTaskListener mDesktopTaskListener;
+    private DesktopAppSelectView mSelectAppToast;
+
     public DesktopVisibilityController(Launcher launcher) {
         mLauncher = launcher;
     }
 
     /**
+     * Register a listener with System UI to receive updates about desktop tasks state
+     */
+    public void registerSystemUiListener() {
+        mDesktopTaskListener = new IDesktopTaskListener.Stub() {
+            @Override
+            public void onVisibilityChanged(int displayId, boolean visible) {
+                MAIN_EXECUTOR.execute(() -> {
+                    if (displayId == mLauncher.getDisplayId()) {
+                        if (DEBUG) {
+                            Log.d(TAG, "desktop visibility changed value=" + visible);
+                        }
+                        setFreeformTasksVisible(visible);
+                    }
+                });
+            }
+
+            @Override
+            public void onStashedChanged(int displayId, boolean stashed) {
+                if (!IS_STASHING_ENABLED) {
+                    return;
+                }
+                MAIN_EXECUTOR.execute(() -> {
+                    if (displayId == mLauncher.getDisplayId()) {
+                        if (DEBUG) {
+                            Log.d(TAG, "desktop stashed changed value=" + stashed);
+                        }
+                        if (stashed) {
+                            showSelectAppToast();
+                        } else {
+                            hideSelectAppToast();
+                        }
+                    }
+                });
+            }
+        };
+        SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(mDesktopTaskListener);
+    }
+
+    /**
+     * Clear listener from System UI that was set with {@link #registerSystemUiListener()}
+     */
+    public void unregisterSystemUiListener() {
+        SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(null);
+    }
+
+    /**
      * Whether desktop mode is supported.
      */
     private boolean isDesktopModeSupported() {
@@ -68,6 +127,7 @@
         if (!isDesktopModeSupported()) {
             return;
         }
+
         if (freeformTasksVisible != mFreeformTasksVisible) {
             mFreeformTasksVisible = freeformTasksVisible;
             if (mFreeformTasksVisible) {
@@ -111,25 +171,54 @@
     /**
      * Whether recents gesture is currently in progress.
      */
-    public boolean isGestureInProgress() {
+    public boolean isRecentsGestureInProgress() {
         return mGestureInProgress;
     }
 
     /**
-     * Sets whether recents gesture is in progress.
+     * Notify controller that recents gesture has started.
      */
-    public void setGestureInProgress(boolean gestureInProgress) {
-        if (DEBUG) {
-            Log.d(TAG, "setGestureInProgress: inProgress=" + gestureInProgress);
-        }
+    public void setRecentsGestureStart() {
         if (!isDesktopModeSupported()) {
             return;
         }
+        setRecentsGestureInProgress(true);
+    }
+
+    /**
+     * Notify controller that recents gesture finished with the given
+     * {@link com.android.quickstep.GestureState.GestureEndTarget}
+     */
+    public void setRecentsGestureEnd(@Nullable GestureState.GestureEndTarget endTarget) {
+        if (!isDesktopModeSupported()) {
+            return;
+        }
+        setRecentsGestureInProgress(false);
+
+        if (endTarget == null) {
+            // Gesture did not result in a new end target. Ensure launchers gets paused again.
+            markLauncherPaused();
+        }
+    }
+
+    private void setRecentsGestureInProgress(boolean gestureInProgress) {
+        if (DEBUG) {
+            Log.d(TAG, "setGestureInProgress: inProgress=" + gestureInProgress);
+        }
         if (gestureInProgress != mGestureInProgress) {
             mGestureInProgress = gestureInProgress;
         }
     }
 
+    /**
+     * Handle launcher moving to home due to home gesture or home button press.
+     */
+    public void onHomeActionTriggered() {
+        if (IS_STASHING_ENABLED && areFreeformTasksVisible()) {
+            SystemUiProxy.INSTANCE.get(mLauncher).stashDesktopApps(mLauncher.getDisplayId());
+        }
+    }
+
     private void setLauncherViewsVisibility(int visibility) {
         if (DEBUG) {
             Log.d(TAG, "setLauncherViewsVisibility: visibility=" + visibility);
@@ -168,4 +257,28 @@
             activity.setResumed();
         }
     }
+
+    private void showSelectAppToast() {
+        if (mSelectAppToast != null) {
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "show toast to select desktop apps");
+        }
+        Runnable onCloseCallback = () -> {
+            SystemUiProxy.INSTANCE.get(mLauncher).hideStashedDesktopApps(mLauncher.getDisplayId());
+        };
+        mSelectAppToast = DesktopAppSelectView.show(mLauncher, onCloseCallback);
+    }
+
+    private void hideSelectAppToast() {
+        if (mSelectAppToast == null) {
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "hide toast to select desktop apps");
+        }
+        mSelectAppToast.hide();
+        mSelectAppToast = null;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 7f655cf..072fc30 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -22,9 +22,13 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.R;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
+import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.RecentsModel;
+import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.views.DesktopTaskView;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
@@ -102,21 +106,74 @@
         mQuickSwitchViewController = new KeyboardQuickSwitchViewController(
                 mControllers, overlayContext, keyboardQuickSwitchView, mControllerCallbacks);
 
+        DesktopVisibilityController desktopController =
+                LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
+        final boolean onDesktop =
+                DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED
+                        && desktopController != null
+                        && desktopController.areFreeformTasksVisible();
+
         if (mModel.isTaskListValid(mTaskListChangeId)) {
-            mQuickSwitchViewController.openQuickSwitchView(
-                    mTasks, mNumHiddenTasks, /* updateTasks= */ false, currentFocusedIndex);
+            mQuickSwitchViewController.openQuickSwitchView(mTasks,
+                    mNumHiddenTasks, /* updateTasks= */ false, currentFocusedIndex, onDesktop);
             return;
         }
+
         mTaskListChangeId = mModel.getTasks((tasks) -> {
-            // Only store MAX_TASK tasks, from most to least recent
-            Collections.reverse(tasks);
-            mTasks = tasks.stream().limit(MAX_TASKS).collect(Collectors.toList());
-            mNumHiddenTasks = Math.max(0, tasks.size() - MAX_TASKS);
-            mQuickSwitchViewController.openQuickSwitchView(
-                    mTasks, mNumHiddenTasks, /* updateTasks= */ true, currentFocusedIndex);
+            if (onDesktop) {
+                processLoadedTasksOnDesktop(tasks);
+            } else {
+                processLoadedTasks(tasks);
+            }
+            mQuickSwitchViewController.openQuickSwitchView(mTasks,
+                    mNumHiddenTasks, /* updateTasks= */ true, currentFocusedIndex, onDesktop);
         });
     }
 
+    private void processLoadedTasks(ArrayList<GroupTask> tasks) {
+        // Only store MAX_TASK tasks, from most to least recent
+        Collections.reverse(tasks);
+
+        // Hide all desktop tasks and show them on the hidden tile
+        int hiddenDesktopTasks = 0;
+        if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) {
+            DesktopTask desktopTask = findDesktopTask(tasks);
+            if (desktopTask != null) {
+                hiddenDesktopTasks = desktopTask.tasks.size();
+                tasks = tasks.stream()
+                        .filter(t -> !(t instanceof DesktopTask))
+                        .collect(Collectors.toCollection(ArrayList<GroupTask>::new));
+            }
+        }
+        mTasks = tasks.stream()
+                .limit(MAX_TASKS)
+                .collect(Collectors.toList());
+        mNumHiddenTasks = Math.max(0, tasks.size() - MAX_TASKS) + hiddenDesktopTasks;
+    }
+
+    private void processLoadedTasksOnDesktop(ArrayList<GroupTask> tasks) {
+        // Find the single desktop task that contains a grouping of desktop tasks
+        DesktopTask desktopTask = findDesktopTask(tasks);
+
+        if (desktopTask != null) {
+            mTasks = desktopTask.tasks.stream().map(GroupTask::new).collect(Collectors.toList());
+            // All other tasks, apart from the grouped desktop task, are hidden
+            mNumHiddenTasks = Math.max(0, tasks.size() - 1);
+        } else {
+            // Desktop tasks were visible, but the recents entry is missing. Fall back to empty list
+            mTasks = Collections.emptyList();
+            mNumHiddenTasks = tasks.size();
+        }
+    }
+
+    @Nullable
+    private DesktopTask findDesktopTask(ArrayList<GroupTask> tasks) {
+        return (DesktopTask) tasks.stream()
+                .filter(t -> t instanceof DesktopTask)
+                .findFirst()
+                .orElse(null);
+    }
+
     void closeQuickSwitchView() {
         if (mQuickSwitchViewController == null) {
             return;
@@ -169,7 +226,7 @@
     class ControllerCallbacks {
 
         int getTaskCount() {
-            return mNumHiddenTasks == 0 ? mTasks.size() : MAX_TASKS + 1;
+            return mTasks.size() + (mNumHiddenTasks == 0 ? 0 : 1);
         }
 
         @Nullable
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index 8a11b57..3e1a6ae 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -80,7 +80,7 @@
         setWillNotDraw(false);
 
         mBorderColor = ta.getColor(
-                R.styleable.TaskView_borderColor, DEFAULT_BORDER_COLOR);
+                R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR);
         ta.recycle();
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 2cdfb18..4e9e301 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -42,10 +42,10 @@
 import androidx.annotation.Nullable;
 import androidx.constraintlayout.widget.ConstraintLayout;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.anim.Interpolators;
 import com.android.quickstep.util.GroupTask;
 
 import java.util.HashMap;
@@ -190,8 +190,12 @@
 
         ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(
                 width, mTaskViewHeight);
-        lp.endToEnd = PARENT_ID;
-        lp.startToEnd = previousView.getId();
+        if (previousView == null) {
+            lp.startToStart = PARENT_ID;
+        } else {
+            lp.endToEnd = PARENT_ID;
+            lp.startToEnd = previousView.getId();
+        }
         lp.topToTop = PARENT_ID;
         lp.bottomToBottom = PARENT_ID;
         lp.setMarginEnd(mSpacing);
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 3230c66..a293f74 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
+import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.GroupTask;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -52,6 +53,8 @@
 
     private int mCurrentFocusIndex = -1;
 
+    private boolean mOnDesktop;
+
     protected KeyboardQuickSwitchViewController(
             @NonNull TaskbarControllers controllers,
             @NonNull TaskbarOverlayContext overlayContext,
@@ -71,10 +74,12 @@
             @NonNull List<GroupTask> tasks,
             int numHiddenTasks,
             boolean updateTasks,
-            int currentFocusIndexOverride) {
+            int currentFocusIndexOverride,
+            boolean onDesktop) {
         TaskbarOverlayDragLayer dragLayer = mOverlayContext.getDragLayer();
         dragLayer.addView(mKeyboardQuickSwitchView);
         dragLayer.runOnClickOnce(v -> closeQuickSwitchView(true));
+        mOnDesktop = onDesktop;
 
         mKeyboardQuickSwitchView.applyLoadPlan(
                 mOverlayContext,
@@ -136,6 +141,10 @@
         GroupTask task = mControllerCallbacks.getTaskAt(index);
         if (task == null) {
             return Math.max(0, index);
+        } else if (mOnDesktop) {
+            UI_HELPER_EXECUTOR.execute(() ->
+                    SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
+                            .showDesktopApp(task.task1.key.id));
         } else if (task.task2 == null) {
             UI_HELPER_EXECUTOR.execute(() ->
                     ActivityManagerWrapper.getInstance().startActivityFromRecents(
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index abf49eb..4e834ec 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -317,6 +317,8 @@
                     .getTaskbarNavButtonTranslationYForInAppDisplay()
                     .updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY()
                             * mTaskbarInAppDisplayProgress.value);
+            mControllers.navbarButtonsViewController
+                    .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress);
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 9c463cb..fcd8c80 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -88,6 +88,7 @@
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory;
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter;
 import com.android.launcher3.util.DimensionUtils;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.TouchController;
@@ -148,8 +149,8 @@
     // Used for IME+A11Y buttons
     private final ViewGroup mEndContextualContainer;
     private final ViewGroup mStartContextualContainer;
-    private final int mLightIconColor;
-    private final int mDarkIconColor;
+    private final int mLightIconColorOnHome;
+    private final int mDarkIconColorOnHome;
     /** Color to use for navigation bar buttons, if they are on on a Taskbar surface background. */
     private final int mOnBackgroundIconColor;
 
@@ -197,6 +198,7 @@
             this::onComputeInsetsForSeparateWindow;
     private final RecentsHitboxExtender mHitboxExtender = new RecentsHitboxExtender();
     private ImageView mRecentsButton;
+    private DisplayController mDisplayController;
 
     public NavbarButtonsViewController(TaskbarActivityContext context, FrameLayout navButtonsView) {
         mContext = context;
@@ -205,9 +207,11 @@
         mEndContextualContainer = mNavButtonsView.findViewById(R.id.end_contextual_buttons);
         mStartContextualContainer = mNavButtonsView.findViewById(R.id.start_contextual_buttons);
 
-        mLightIconColor = context.getColor(R.color.taskbar_nav_icon_light_color);
-        mDarkIconColor = context.getColor(R.color.taskbar_nav_icon_dark_color);
-        mOnBackgroundIconColor = Utilities.isDarkTheme(context) ? mLightIconColor : mDarkIconColor;
+        mLightIconColorOnHome = context.getColor(R.color.taskbar_nav_icon_light_color_on_home);
+        mDarkIconColorOnHome = context.getColor(R.color.taskbar_nav_icon_dark_color_on_home);
+        mOnBackgroundIconColor = Utilities.isDarkTheme(context)
+                ? context.getColor(R.color.taskbar_nav_icon_light_color)
+                : context.getColor(R.color.taskbar_nav_icon_dark_color);
     }
 
     /**
@@ -224,6 +228,8 @@
                         TaskbarManager.isPhoneMode(deviceProfile));
         mNavButtonsView.getLayoutParams().height = p.y;
 
+        mDisplayController = DisplayController.INSTANCE.get(mContext);
+
         mIsImeRenderingNavButtons =
                 InputMethodService.canImeRenderGesturalNavButtons() && mContext.imeDrawsImeNavBar();
         if (!mIsImeRenderingNavButtons) {
@@ -630,18 +636,20 @@
 
     private void updateNavButtonColor() {
         final ArgbEvaluator argbEvaluator = ArgbEvaluator.getInstance();
-        final int sysUiNavButtonIconColor = (int) argbEvaluator.evaluate(
+        final int sysUiNavButtonIconColorOnHome = (int) argbEvaluator.evaluate(
                 mTaskbarNavButtonDarkIntensity.value,
-                mLightIconColor,
-                mDarkIconColor);
+                mLightIconColorOnHome,
+                mDarkIconColorOnHome);
+
         // Override the color from framework if nav buttons are over an opaque Taskbar surface.
         final int iconColor = (int) argbEvaluator.evaluate(
                 mOnBackgroundNavButtonColorOverrideMultiplier.value
                         * Math.max(
                                 mOnTaskbarBackgroundNavButtonColorOverride.value,
                                 mSlideInViewVisibleNavButtonColorOverride.value),
-                sysUiNavButtonIconColor,
+                sysUiNavButtonIconColorOnHome,
                 mOnBackgroundIconColor);
+
         for (ImageView button : mAllButtons) {
             button.setImageTintList(ColorStateList.valueOf(iconColor));
         }
@@ -723,15 +731,12 @@
         boolean isInKidsMode = mContext.isNavBarKidsModeActive();
 
         if (TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW) {
-            if (!isThreeButtonNav) {
-                return;
-            }
-
             NavButtonLayoutter navButtonLayoutter =
                     NavButtonLayoutFactory.Companion.getUiLayoutter(
                             dp, mNavButtonsView, res, isInKidsMode, isInSetup, isThreeButtonNav,
-                            TaskbarManager.isPhoneMode(dp));
+                            TaskbarManager.isPhoneMode(dp), mDisplayController.getInfo().rotation);
             navButtonLayoutter.layoutButtons(dp, isContextualButtonShowing());
+            updateNavButtonColor();
             return;
         }
 
@@ -937,8 +942,6 @@
         pw.println(prefix + "NavbarButtonsViewController:");
 
         pw.println(prefix + "\tmState=" + getStateString(mState));
-        pw.println(prefix + "\tmLightIconColor=" + Integer.toHexString(mLightIconColor));
-        pw.println(prefix + "\tmDarkIconColor=" + Integer.toHexString(mDarkIconColor));
         pw.println(prefix + "\tmFloatingRotationButtonBounds=" + mFloatingRotationButtonBounds);
         pw.println(prefix + "\tmSysuiStateFlags=" + QuickStepContract.getSystemUiStateString(
                 mSysuiStateFlags));
@@ -949,6 +952,14 @@
                 + mTaskbarNavButtonTranslationYForInAppDisplay.value);
         pw.println(prefix + "\t\tmTaskbarNavButtonTranslationYForIme="
                 + mTaskbarNavButtonTranslationYForIme.value);
+        pw.println(prefix + "\t\tmTaskbarNavButtonDarkIntensity="
+                + mTaskbarNavButtonDarkIntensity.value);
+        pw.println(prefix + "\t\tmSlideInViewVisibleNavButtonColorOverride="
+                + mSlideInViewVisibleNavButtonColorOverride.value);
+        pw.println(prefix + "\t\tmOnTaskbarBackgroundNavButtonColorOverride="
+                + mOnTaskbarBackgroundNavButtonColorOverride.value);
+        pw.println(prefix + "\t\tmOnBackgroundNavButtonColorOverrideMultiplier="
+                + mOnBackgroundNavButtonColorOverrideMultiplier.value);
     }
 
     private static String getStateString(int flags) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index a1390ae..b15dda2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -49,14 +49,15 @@
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
 import android.os.Process;
-import android.os.SystemProperties;
 import android.os.Trace;
 import android.provider.Settings;
 import android.util.Log;
 import android.view.Display;
 import android.view.Gravity;
 import android.view.RoundedCorner;
+import android.view.Surface;
 import android.view.View;
+import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.widget.FrameLayout;
 import android.widget.Toast;
@@ -90,6 +91,8 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarView;
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
+import com.android.launcher3.taskbar.bubbles.BubbleDismissController;
+import com.android.launcher3.taskbar.bubbles.BubbleDragController;
 import com.android.launcher3.taskbar.bubbles.BubbleStashController;
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
@@ -117,6 +120,7 @@
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
 
 import java.io.PrintWriter;
+import java.util.Collections;
 import java.util.Optional;
 
 /**
@@ -128,8 +132,6 @@
 
     private static final String IME_DRAWS_IME_NAV_BAR_RES_NAME = "config_imeDrawsImeNavBar";
 
-    private static final boolean ENABLE_THREE_BUTTON_TASKBAR =
-            SystemProperties.getBoolean("persist.debug.taskbar_three_button", false);
     private static final String TAG = "TaskbarActivityContext";
 
     private static final String WINDOW_TITLE = "Taskbar";
@@ -146,7 +148,7 @@
     private int mLastRequestedNonFullscreenHeight;
 
     private NavigationMode mNavMode;
-    private final boolean mImeDrawsImeNavBar;
+    private boolean mImeDrawsImeNavBar;
     private final ViewCache mViewCache = new ViewCache();
 
     private final boolean mIsSafeModeEnabled;
@@ -169,30 +171,27 @@
             TaskbarNavButtonController buttonController, ScopedUnfoldTransitionProgressProvider
             unfoldTransitionProgressProvider) {
         super(windowContext);
+
+        applyDeviceProfile(launcherDp);
+
         final Resources resources = getResources();
 
-        matchDeviceProfile(launcherDp, getResources());
-
-        mNavMode = DisplayController.getNavigationMode(windowContext);
         mImeDrawsImeNavBar = getBoolByName(IME_DRAWS_IME_NAV_BAR_RES_NAME, resources, false);
         mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
                 () -> getPackageManager().isSafeMode());
+
+        // TODO(b/244231596) For shared Taskbar window, update this value in applyDeviceProfile()
+        //  instead so to get correct value when recreating the taskbar
         SettingsCache settingsCache = SettingsCache.INSTANCE.get(this);
         mIsUserSetupComplete = settingsCache.getValue(
                 Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE), 0);
-        mIsNavBarForceVisible = settingsCache.getValue(
-                Settings.Secure.getUriFor(Settings.Secure.NAV_BAR_KIDS_MODE), 0);
-
-        // TODO(b/244231596) For shared Taskbar window, update this value in init() instead so
-        //  to get correct value when recreating the taskbar
         mIsNavBarKidsMode = settingsCache.getValue(
                 Settings.Secure.getUriFor(Settings.Secure.NAV_BAR_KIDS_MODE), 0);
+        mIsNavBarForceVisible = mIsNavBarKidsMode;
 
         // Get display and corners first, as views might use them in constructor.
         Display display = windowContext.getDisplay();
-        Context c = display.getDisplayId() == Display.DEFAULT_DISPLAY
-                ? windowContext.getApplicationContext()
-                : windowContext.getApplicationContext().createDisplayContext(display);
+        Context c = getApplicationContext();
         mWindowManager = c.getSystemService(WindowManager.class);
         mLeftCorner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT);
         mRightCorner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT);
@@ -216,29 +215,34 @@
 
         // If Bubble bar is present, TaskbarControllers depends on it so build it first.
         Optional<BubbleControllers> bubbleControllersOptional = Optional.empty();
-        if (BubbleBarController.BUBBLE_BAR_ENABLED) {
+        if (BubbleBarController.BUBBLE_BAR_ENABLED && bubbleBarView != null) {
             bubbleControllersOptional = Optional.of(new BubbleControllers(
                     new BubbleBarController(this, bubbleBarView),
                     new BubbleBarViewController(this, bubbleBarView),
                     new BubbleStashController(this),
-                    new BubbleStashedHandleViewController(this, bubbleHandleView)));
+                    new BubbleStashedHandleViewController(this, bubbleHandleView),
+                    new BubbleDragController(this),
+                    new BubbleDismissController(this, mDragLayer)));
         }
 
         // Construct controllers.
+        RotationButtonController rotationButtonController = new RotationButtonController(this,
+                c.getColor(R.color.floating_rotation_button_light_color),
+                c.getColor(R.color.floating_rotation_button_dark_color),
+                R.drawable.ic_sysbar_rotate_button_ccw_start_0,
+                R.drawable.ic_sysbar_rotate_button_ccw_start_90,
+                R.drawable.ic_sysbar_rotate_button_cw_start_0,
+                R.drawable.ic_sysbar_rotate_button_cw_start_90,
+                () -> getDisplay().getRotation());
+        rotationButtonController.setBgExecutor(Executors.THREAD_POOL_EXECUTOR);
+
         mControllers = new TaskbarControllers(this,
                 new TaskbarDragController(this),
                 buttonController,
                 isDesktopMode
                         ? new DesktopNavbarButtonsViewController(this, navButtonsView)
                         : new NavbarButtonsViewController(this, navButtonsView),
-                new RotationButtonController(this,
-                        c.getColor(R.color.floating_rotation_button_light_color),
-                        c.getColor(R.color.floating_rotation_button_dark_color),
-                        R.drawable.ic_sysbar_rotate_button_ccw_start_0,
-                        R.drawable.ic_sysbar_rotate_button_ccw_start_90,
-                        R.drawable.ic_sysbar_rotate_button_cw_start_0,
-                        R.drawable.ic_sysbar_rotate_button_cw_start_90,
-                        () -> getDisplay().getRotation()),
+                rotationButtonController,
                 new TaskbarDragLayerController(this, mDragLayer),
                 new TaskbarViewController(this, taskbarView),
                 new TaskbarScrimViewController(this, taskbarScrimView),
@@ -267,13 +271,48 @@
                 bubbleControllersOptional);
     }
 
+    /** Updates {@link DeviceProfile} instances for any Taskbar windows. */
+    public void updateDeviceProfile(DeviceProfile launcherDp) {
+        applyDeviceProfile(launcherDp);
+
+        mControllers.taskbarOverlayController.updateLauncherDeviceProfile(launcherDp);
+        AbstractFloatingView.closeAllOpenViewsExcept(this, false, TYPE_REBIND_SAFE);
+        // Reapply fullscreen to take potential new screen size into account.
+        setTaskbarWindowFullscreen(mIsFullscreen);
+
+        dispatchDeviceProfileChanged();
+    }
+
+    /**
+     * Copy the original DeviceProfile, match the number of hotseat icons and qsb width and update
+     * the icon size
+     */
+    private void applyDeviceProfile(DeviceProfile originDeviceProfile) {
+        mDeviceProfile = originDeviceProfile.toBuilder(this)
+                .withDimensionsOverride(deviceProfile -> {
+                    // Taskbar should match the number of icons of hotseat
+                    deviceProfile.numShownHotseatIcons = originDeviceProfile.numShownHotseatIcons;
+                    // Same QSB width to have a smooth animation
+                    deviceProfile.hotseatQsbWidth = originDeviceProfile.hotseatQsbWidth;
+
+                    // Update icon size
+                    deviceProfile.iconSizePx = deviceProfile.taskbarIconSize;
+                    deviceProfile.updateIconSize(1f, getResources());
+                }).build();
+        mNavMode = DisplayController.getNavigationMode(this);
+    }
+
+
     public void init(@NonNull TaskbarSharedState sharedState) {
+        mImeDrawsImeNavBar = getBoolByName(IME_DRAWS_IME_NAV_BAR_RES_NAME, getResources(), false);
         mLastRequestedNonFullscreenHeight = getDefaultTaskbarWindowHeight();
-        mWindowLayoutParams =
-                createDefaultWindowLayoutParams(TYPE_NAVIGATION_BAR_PANEL, WINDOW_TITLE);
+        mWindowLayoutParams = createAllWindowParams();
 
         // Initialize controllers after all are constructed.
         mControllers.init(sharedState);
+        // This may not be necessary and can be reverted once we move towards recreating all
+        // controllers without re-creating the window
+        mControllers.rotationButtonController.onNavigationModeChanged(mNavMode.resValue);
         updateSysuiStateFlags(sharedState.sysuiStateFlags, true /* fromInit */);
         disableNavBarElements(sharedState.disableNavBarDisplayId, sharedState.disableNavBarState1,
                 sharedState.disableNavBarState2, false /* animate */);
@@ -303,24 +342,16 @@
         mControllers.taskbarStashController.showTaskbarFromBroadcast();
     }
 
+    /** Toggles Taskbar All Apps overlay. */
+    public void toggleAllApps() {
+        mControllers.taskbarAllAppsController.toggle();
+    }
+
     @Override
     public DeviceProfile getDeviceProfile() {
         return mDeviceProfile;
     }
 
-    /** Updates {@link DeviceProfile} instances for any Taskbar windows. */
-    public void updateDeviceProfile(DeviceProfile launcherDp, NavigationMode navMode) {
-        mNavMode = navMode;
-        mControllers.taskbarOverlayController.updateLauncherDeviceProfile(launcherDp);
-        matchDeviceProfile(launcherDp, getResources());
-
-        AbstractFloatingView.closeAllOpenViewsExcept(this, false, TYPE_REBIND_SAFE);
-        // Reapply fullscreen to take potential new screen size into account.
-        setTaskbarWindowFullscreen(mIsFullscreen);
-
-        dispatchDeviceProfileChanged();
-    }
-
     @Override
     public void dispatchDeviceProfileChanged() {
         super.dispatchDeviceProfileChanged();
@@ -329,24 +360,6 @@
     }
 
     /**
-     * Copy the original DeviceProfile, match the number of hotseat icons and qsb width and update
-     * the icon size
-     */
-    private void matchDeviceProfile(DeviceProfile originDeviceProfile, Resources resources) {
-        mDeviceProfile = originDeviceProfile.toBuilder(this)
-                .withDimensionsOverride(deviceProfile -> {
-                    // Taskbar should match the number of icons of hotseat
-                    deviceProfile.numShownHotseatIcons = originDeviceProfile.numShownHotseatIcons;
-                    // Same QSB width to have a smooth animation
-                    deviceProfile.hotseatQsbWidth = originDeviceProfile.hotseatQsbWidth;
-
-                    // Update icon size
-                    deviceProfile.iconSizePx = deviceProfile.taskbarIconSize;
-                    deviceProfile.updateIconSize(1f, resources);
-                }).build();
-    }
-
-    /**
      * Returns the View bounds of transient taskbar.
      */
     public Rect getTransientTaskbarBounds() {
@@ -365,11 +378,6 @@
      * @param title The window title to pass to the created WindowManager.LayoutParams.
      */
     public WindowManager.LayoutParams createDefaultWindowLayoutParams(int type, String title) {
-        DeviceProfile deviceProfile = getDeviceProfile();
-        // Taskbar is on the logical bottom of the screen
-        boolean isVerticalBarLayout = TaskbarManager.isPhoneButtonNavMode(this) &&
-                deviceProfile.isLandscape;
-
         int windowFlags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                 | WindowManager.LayoutParams.FLAG_SLIPPERY
                 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
@@ -378,17 +386,14 @@
                     | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
         }
         WindowManager.LayoutParams windowLayoutParams = new WindowManager.LayoutParams(
-                isVerticalBarLayout ? mLastRequestedNonFullscreenHeight : MATCH_PARENT,
-                isVerticalBarLayout ? MATCH_PARENT : mLastRequestedNonFullscreenHeight,
+                MATCH_PARENT,
+                mLastRequestedNonFullscreenHeight,
                 type,
                 windowFlags,
                 PixelFormat.TRANSLUCENT);
         windowLayoutParams.setTitle(title);
         windowLayoutParams.packageName = getPackageName();
-        windowLayoutParams.gravity = !isVerticalBarLayout ?
-                Gravity.BOTTOM :
-                Gravity.END; // TODO(b/230394142): seascape
-
+        windowLayoutParams.gravity = Gravity.BOTTOM;
         windowLayoutParams.setFitInsetsTypes(0);
         windowLayoutParams.receiveInsetsIgnoringZOrder = true;
         windowLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
@@ -399,6 +404,64 @@
                 TaskbarManager.isPhoneMode(mDeviceProfile)
                         ? R.string.taskbar_phone_a11y_title
                         : R.string.taskbar_a11y_title);
+
+        return windowLayoutParams;
+    }
+
+    /**
+     * Creates {@link WindowManager.LayoutParams} for Taskbar, and also sets LP.paramsForRotation
+     * for taskbar showing as navigation bar
+     */
+    private WindowManager.LayoutParams createAllWindowParams() {
+        WindowManager.LayoutParams windowLayoutParams =
+                createDefaultWindowLayoutParams(TYPE_NAVIGATION_BAR_PANEL,
+                        TaskbarActivityContext.WINDOW_TITLE);
+        boolean isPhoneNavMode = TaskbarManager.isPhoneButtonNavMode(this);
+        if (!isPhoneNavMode) {
+            return windowLayoutParams;
+        }
+
+        // Provide WM layout params for all rotations to cache, see NavigationBar#getBarLayoutParams
+        int width = WindowManager.LayoutParams.MATCH_PARENT;
+        int height = WindowManager.LayoutParams.MATCH_PARENT;
+        int gravity = Gravity.BOTTOM;
+        windowLayoutParams.paramsForRotation = new WindowManager.LayoutParams[4];
+        for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) {
+            WindowManager.LayoutParams lp =
+                    createDefaultWindowLayoutParams(TYPE_NAVIGATION_BAR_PANEL,
+                            TaskbarActivityContext.WINDOW_TITLE);
+            switch (rot) {
+                case Surface.ROTATION_0, Surface.ROTATION_180 -> {
+                    // Defaults are fine
+                    width = WindowManager.LayoutParams.MATCH_PARENT;
+                    height = mLastRequestedNonFullscreenHeight;
+                    gravity = Gravity.BOTTOM;
+                }
+                case Surface.ROTATION_90 -> {
+                    width = mLastRequestedNonFullscreenHeight;
+                    height = WindowManager.LayoutParams.MATCH_PARENT;
+                    gravity = Gravity.END;
+                }
+                case Surface.ROTATION_270 -> {
+                    width = mLastRequestedNonFullscreenHeight;
+                    height = WindowManager.LayoutParams.MATCH_PARENT;
+                    gravity = Gravity.START;
+                }
+
+            }
+            lp.width = width;
+            lp.height = height;
+            lp.gravity = gravity;
+            windowLayoutParams.paramsForRotation[rot] = lp;
+        }
+
+        // Override current layout params
+        WindowManager.LayoutParams currentParams =
+                windowLayoutParams.paramsForRotation[getDisplay().getRotation()];
+        windowLayoutParams.width = currentParams.width;
+        windowLayoutParams.height = currentParams.height;
+        windowLayoutParams.gravity = currentParams.gravity;
+
         return windowLayoutParams;
     }
 
@@ -448,6 +511,11 @@
         return mControllers.taskbarDragController;
     }
 
+    @Nullable
+    public BubbleControllers getBubbleControllers() {
+        return mControllers.bubbleControllers.orElse(null);
+    }
+
     @Override
     public ViewCache getViewCache() {
         return mViewCache;
@@ -579,6 +647,8 @@
                 Executors.MAIN_EXECUTOR.getHandler(), null,
                 elapsedRealTime -> callbacks.executeAllAndDestroy());
         options.setSplashScreenStyle(splashScreenStyle);
+        options.setPendingIntentBackgroundActivityStartMode(
+                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
         return new ActivityOptionsWrapper(options, callbacks);
     }
 
@@ -638,8 +708,12 @@
         mControllers.taskbarForceVisibleImmersiveController.updateSysuiFlags(systemUiStateFlags);
         mControllers.voiceInteractionWindowController.setIsVoiceInteractionWindowVisible(
                 (systemUiStateFlags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0, fromInit);
-
         mControllers.uiController.updateStateForSysuiFlags(systemUiStateFlags);
+        mControllers.bubbleControllers.ifPresent(controllers -> {
+            controllers.bubbleBarController.updateStateForSysuiFlags(systemUiStateFlags);
+            controllers.bubbleStashedHandleViewController.setIsHomeButtonDisabled(
+                    mControllers.navbarButtonsViewController.isHomeDisabled());
+        });
     }
 
     /**
@@ -735,7 +809,7 @@
             }
         }
         mWindowLayoutParams.height = height;
-        mControllers.taskbarInsetsController.onTaskbarWindowHeightOrInsetsChanged();
+        mControllers.taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
         mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams);
     }
 
@@ -783,6 +857,21 @@
     }
 
     /**
+     * Applies forcibly show flag to taskbar window iff transient taskbar is unstashed.
+     */
+    public void applyForciblyShownFlagWhileTransientTaskbarUnstashed(boolean shouldForceShow) {
+        if (!DisplayController.isTransientTaskbar(this)) {
+            return;
+        }
+        if (shouldForceShow) {
+            mWindowLayoutParams.forciblyShownTypes |= WindowInsets.Type.navigationBars();
+        } else {
+            mWindowLayoutParams.forciblyShownTypes &= ~WindowInsets.Type.navigationBars();
+        }
+        notifyUpdateLayoutParams();
+    }
+
+    /**
      * Either adds or removes {@link WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} on the taskbar
      * window. If we're now focusable, also move nav buttons to a separate window above IME.
      */
@@ -888,8 +977,8 @@
                         }
 
                     } catch (NullPointerException
-                            | ActivityNotFoundException
-                            | SecurityException e) {
+                             | ActivityNotFoundException
+                             | SecurityException e) {
                         Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT)
                                 .show();
                         Log.e(TAG, "Unable to launch. tag=" + info + " intent=" + intent, e);
@@ -936,9 +1025,10 @@
         if (recents == null) {
             return;
         }
-        recents.getSplitSelectController().findLastActiveTaskAndRunCallback(
-                info.getComponentKey(),
-                foundTask -> {
+        recents.getSplitSelectController().findLastActiveTasksAndRunCallback(
+                Collections.singletonList(info.getComponentKey()),
+                foundTasks -> {
+                    @Nullable Task foundTask = foundTasks.get(0);
                     if (foundTask != null) {
                         TaskView foundTaskView =
                                 recents.getTaskViewByTaskId(foundTask.key.id);
@@ -982,6 +1072,7 @@
 
     /**
      * Called when we detect a long press in the nav region before passing the gesture slop.
+     *
      * @return Whether taskbar handled the long press, and thus should cancel the gesture.
      */
     public boolean onLongPressToUnstashTaskbar() {
@@ -992,10 +1083,19 @@
      * Called when we want to unstash taskbar when user performs swipes up gesture.
      */
     public void onSwipeToUnstashTaskbar() {
-        mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(false);
+        mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(/* stash= */ false);
         mControllers.taskbarEduTooltipController.hide();
     }
 
+    /**
+     * Called when we want to open bubblebar when user performs swipes up gesture.
+     */
+    public void onSwipeToOpenBubblebar() {
+        mControllers.bubbleControllers.ifPresent(controllers -> {
+            controllers.bubbleStashController.showBubbleBar(/* expandBubbles= */ true);
+        });
+    }
+
     /** Returns {@code true} if Taskbar All Apps is open. */
     public boolean isTaskbarAllAppsOpen() {
         return mControllers.taskbarAllAppsController.isOpen();
@@ -1041,7 +1141,7 @@
      * Called when we detect a motion down or up/cancel in the nav region while stashed.
      *
      * @param animateForward Whether to animate towards the unstashed hint state or back to stashed.
-     * @param forceUnstash Whether we force the unstash hint.
+     * @param forceUnstash   Whether we force the unstash hint.
      */
     public void startTaskbarUnstashHint(boolean animateForward, boolean forceUnstash) {
         // TODO(b/270395798): Clean up forceUnstash after removing long-press unstashing code.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
index 70999e7..8ab2ffa 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
@@ -45,6 +45,8 @@
     public static final int FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER = 1 << 4;
     // Transient Taskbar is temporarily unstashed (pending a timeout).
     public static final int FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR = 1 << 5;
+    // User has hovered the taskbar.
+    public static final int FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS = 1 << 6;
 
     @IntDef(flag = true, value = {
             FLAG_AUTOHIDE_SUSPEND_FULLSCREEN,
@@ -53,6 +55,7 @@
             FLAG_AUTOHIDE_SUSPEND_EDU_OPEN,
             FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER,
             FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR,
+            FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AutohideSuspendFlag {}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index ca2d1f8..d237c1f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -22,12 +22,12 @@
 import android.graphics.Paint
 import android.graphics.Path
 import android.graphics.RectF
+import com.android.app.animation.Interpolators
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
 import com.android.launcher3.Utilities.mapRange
 import com.android.launcher3.Utilities.mapToRange
-import com.android.launcher3.anim.Interpolators
 import com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound
 import com.android.launcher3.util.DisplayController
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index d3f80e3..d82f501 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -239,6 +239,7 @@
         taskbarPopupController.onDestroy();
         taskbarForceVisibleImmersiveController.onDestroy();
         taskbarOverlayController.onDestroy();
+        taskbarAllAppsController.onDestroy();
         navButtonController.onDestroy();
         taskbarInsetsController.onDestroy();
         voiceInteractionWindowController.onDestroy();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
index a347908..b0d2c3c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
@@ -17,7 +17,6 @@
 
 import android.annotation.SuppressLint
 import android.content.Context
-import android.content.Intent
 import android.graphics.Rect
 import android.graphics.drawable.GradientDrawable
 import android.util.AttributeSet
@@ -44,9 +43,6 @@
     companion object {
         private const val TAG = "TaskbarDividerPopupView"
         private const val DIVIDER_POPUP_CLOSING_DELAY = 500L
-        private const val SETTINGS_PACKAGE_NAME = "com.android.settings"
-        private const val CHANGE_NAVIGATION_MODE_ACTION =
-            "com.android.settings.NAVIGATION_MODE_SETTINGS"
 
         @JvmStatic
         fun createAndPopulate(
@@ -103,21 +99,12 @@
         super.onFinishInflate()
         val taskbarSwitchOption = findViewById<LinearLayout>(R.id.taskbar_switch_option)
         val alwaysShowTaskbarSwitch = findViewById<Switch>(R.id.taskbar_pinning_switch)
-        val navigationModeChangeOption =
-            findViewById<LinearLayout>(R.id.navigation_mode_switch_option)
         alwaysShowTaskbarSwitch.isChecked = alwaysShowTaskbarOn
         taskbarSwitchOption.setOnClickListener {
             alwaysShowTaskbarSwitch.isClickable = true
             alwaysShowTaskbarSwitch.isChecked = !alwaysShowTaskbarOn
             onClickAlwaysShowTaskbarSwitchOption()
         }
-        navigationModeChangeOption.setOnClickListener {
-            context.startActivity(
-                Intent(CHANGE_NAVIGATION_MODE_ACTION)
-                    .setPackage(SETTINGS_PACKAGE_NAME)
-                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-            )
-        }
     }
 
     /** Orient object as usual and then center object horizontally. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index 040b8f7..3c7196a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -15,16 +15,18 @@
  */
 package com.android.launcher3.taskbar;
 
+import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_ALL_APPS;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SEARCH_ACTION;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
+import android.app.PendingIntent;
 import android.content.ClipData;
 import android.content.ClipDescription;
 import android.content.Intent;
@@ -47,6 +49,7 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.app.animation.Interpolators;
 import com.android.internal.logging.InstanceId;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BubbleTextView;
@@ -55,7 +58,6 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragDriver;
 import com.android.launcher3.dragndrop.DragOptions;
@@ -73,6 +75,7 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.views.BubbleTextHolder;
 import com.android.quickstep.util.LogUtils;
 import com.android.quickstep.util.MultiValueUpdateListener;
 import com.android.systemui.shared.recents.model.Task;
@@ -149,6 +152,9 @@
             View view,
             @Nullable DragPreviewProvider dragPreviewProvider,
             @Nullable Point iconShift) {
+        if (view instanceof BubbleTextHolder) {
+            view = ((BubbleTextHolder) view).getBubbleText();
+        }
         if (!(view instanceof BubbleTextView) || mDisallowLongClick) {
             return false;
         }
@@ -193,13 +199,21 @@
         dragLayerY += dragRect.top;
 
         DragOptions dragOptions = new DragOptions();
-        dragOptions.preDragCondition = null;
-        PopupContainerWithArrow<BaseTaskbarContext> popupContainer =
-                mControllers.taskbarPopupController.showForIcon(btv);
-        if (popupContainer != null) {
-            dragOptions.preDragCondition = popupContainer.createPreDragCondition(false);
-        }
+        // First, see if view is a search result that needs custom pre-drag conditions.
+        dragOptions.preDragCondition =
+                mControllers.taskbarAllAppsController.createPreDragConditionForSearch(btv);
+
         if (dragOptions.preDragCondition == null) {
+            // See if view supports a popup container.
+            PopupContainerWithArrow<BaseTaskbarContext> popupContainer =
+                    mControllers.taskbarPopupController.showForIcon(btv);
+            if (popupContainer != null) {
+                dragOptions.preDragCondition = popupContainer.createPreDragCondition(false);
+            }
+        }
+
+        if (dragOptions.preDragCondition == null) {
+            // Fallback pre-drag condition.
             dragOptions.preDragCondition = new DragOptions.PreDragCondition() {
                 private DragView mDragView;
 
@@ -213,13 +227,8 @@
                     mDragView = dragObject.dragView;
 
                     if (!shouldStartDrag(0)) {
-                        mDragView.setOnAnimationEndCallback(() -> {
-                            // Drag might be cancelled during the DragView animation, so check
-                            // mIsPreDrag again.
-                            if (mIsInPreDrag) {
-                                callOnDragStart();
-                            }
-                        });
+                        mDragView.setOnScaleAnimEndCallback(
+                                TaskbarDragController.this::onPreDragAnimationEnd);
                     }
                 }
 
@@ -230,12 +239,13 @@
             };
         }
 
+        Point dragOffset = dragOptions.preDragCondition.getDragOffset();
         return startDrag(
                 drawable,
                 /* view = */ null,
                 /* originalView = */ btv,
-                dragLayerX,
-                dragLayerY,
+                dragLayerX + dragOffset.x,
+                dragLayerY + dragOffset.y,
                 (View target, DropTarget.DragObject d, boolean success) -> {} /* DragSource */,
                 (ItemInfo) btv.getTag(),
                 dragRect,
@@ -290,6 +300,11 @@
         mDragObject.dragInfo = dragInfo;
         mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy();
 
+        if (mOptions.preDragCondition != null) {
+            dragView.setHasDragOffset(mOptions.preDragCondition.getDragOffset().x != 0
+                    || mOptions.preDragCondition.getDragOffset().y != 0);
+        }
+
         if (dragRegion != null) {
             dragView.setDragRegion(new Rect(dragRegion));
         }
@@ -308,6 +323,14 @@
         return dragView;
     }
 
+    /** Invoked when an animation running as part of pre-drag finishes. */
+    public void onPreDragAnimationEnd() {
+        // Drag might be cancelled during the DragView animation, so check mIsPreDrag again.
+        if (mIsInPreDrag) {
+            callOnDragStart();
+        }
+    }
+
     @Override
     protected void callOnDragStart() {
         super.callOnDragStart();
@@ -383,6 +406,17 @@
                                 item.user));
                 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, item.getIntent().getPackage());
                 intent.putExtra(Intent.EXTRA_SHORTCUT_ID, deepShortcutId);
+            } else if (item.itemType == ITEM_TYPE_SEARCH_ACTION) {
+                // TODO(b/289261756): Buggy behavior when split opposite to an existing search pane.
+                intent.putExtra(
+                        ClipDescription.EXTRA_PENDING_INTENT,
+                        PendingIntent.getActivityAsUser(
+                                mActivity,
+                                /* requestCode= */ 0,
+                                item.getIntent(),
+                                PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
+                                /* options= */ null,
+                                item.user));
             } else {
                 intent.putExtra(ClipDescription.EXTRA_PENDING_INTENT,
                         launcherApps.getMainActivityLaunchIntent(item.getIntent().getComponent(),
@@ -642,7 +676,7 @@
             final FloatProp mScale = new FloatProp(1f, toScale, 0,
                     ANIM_DURATION_RETURN_ICON_TO_TASKBAR, FAST_OUT_SLOW_IN);
             final FloatProp mAlpha = new FloatProp(1f, toAlpha, 0,
-                    ANIM_DURATION_RETURN_ICON_TO_TASKBAR, Interpolators.ACCEL_2);
+                    ANIM_DURATION_RETURN_ICON_TO_TASKBAR, Interpolators.ACCELERATE_2);
             @Override
             public void onUpdate(float percent, boolean initOnly) {
                 animListener.updateDragShadow(mDx.value, mDy.value, mScale.value, mAlpha.value);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index f6de926..e521154 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.RectF;
+import android.media.permission.SafeCloseable;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.view.KeyEvent;
@@ -31,6 +32,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.app.viewcapture.SettingsAwareViewCapture;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -68,6 +70,7 @@
 
     // Initialized in init.
     private TaskbarDragLayerController.TaskbarDragLayerCallbacks mControllerCallbacks;
+    private SafeCloseable mViewCaptureCloseable;
 
     private float mTaskbarBackgroundOffset;
 
@@ -128,12 +131,14 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         getViewTreeObserver().addOnComputeInternalInsetsListener(mTaskbarInsetsComputer);
+        mViewCaptureCloseable = SettingsAwareViewCapture.getInstance(getContext())
+                .startCapture(getRootView(), ".Taskbar");
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-
+        mViewCaptureCloseable.close();
         onDestroy(true);
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
new file mode 100644
index 0000000..c3ec1e5
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static android.view.MotionEvent.ACTION_HOVER_ENTER;
+import static android.view.MotionEvent.ACTION_HOVER_EXIT;
+import static android.view.View.ALPHA;
+import static android.view.View.SCALE_Y;
+import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT;
+
+import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_EXCEPT_ON_BOARD_POPUP;
+import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS;
+import static com.android.launcher3.views.ArrowTipView.TEXT_ALPHA;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.ContextThemeWrapper;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.app.animation.Interpolators;
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.views.ArrowTipView;
+
+/**
+ * Controls showing a tooltip in the taskbar above each icon when it is hovered.
+ */
+public class TaskbarHoverToolTipController implements View.OnHoverListener {
+
+    @VisibleForTesting protected static final int HOVER_TOOL_TIP_REVEAL_START_DELAY = 400;
+    private static final int HOVER_TOOL_TIP_REVEAL_DURATION = 300;
+    private static final int HOVER_TOOL_TIP_EXIT_DURATION = 150;
+
+    private final Handler mHoverToolTipHandler = new Handler(Looper.getMainLooper());
+    private final Runnable mRevealHoverToolTipRunnable = this::revealHoverToolTip;
+    private final Runnable mHideHoverToolTipRunnable = this::hideHoverToolTip;
+
+    private final TaskbarActivityContext mActivity;
+    private final TaskbarView mTaskbarView;
+    private final View mHoverView;
+    private final ArrowTipView mHoverToolTipView;
+    private final String mToolTipText;
+
+    public TaskbarHoverToolTipController(TaskbarActivityContext activity, TaskbarView taskbarView,
+            View hoverView) {
+        mActivity = activity;
+        mTaskbarView = taskbarView;
+        mHoverView = hoverView;
+
+        if (mHoverView instanceof BubbleTextView) {
+            mToolTipText = ((BubbleTextView) mHoverView).getText().toString();
+        } else if (mHoverView instanceof FolderIcon
+                && ((FolderIcon) mHoverView).mInfo.title != null) {
+            mToolTipText = ((FolderIcon) mHoverView).mInfo.title.toString();
+        } else {
+            mToolTipText = null;
+        }
+
+        ContextThemeWrapper arrowContextWrapper = new ContextThemeWrapper(mActivity,
+                R.style.ArrowTipTaskbarStyle);
+        mHoverToolTipView = new ArrowTipView(arrowContextWrapper, /* isPointingUp = */ false,
+                R.layout.arrow_toast);
+
+        AnimatorSet hoverCloseAnimator = new AnimatorSet();
+        ObjectAnimator textCloseAnimator = ObjectAnimator.ofInt(mHoverToolTipView, TEXT_ALPHA, 0);
+        textCloseAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0, 0.33f));
+        ObjectAnimator alphaCloseAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, ALPHA, 0);
+        alphaCloseAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0.33f, 0.66f));
+        ObjectAnimator scaleCloseAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, SCALE_Y, 0);
+        scaleCloseAnimator.setInterpolator(Interpolators.STANDARD);
+        hoverCloseAnimator.playTogether(
+                textCloseAnimator,
+                alphaCloseAnimator,
+                scaleCloseAnimator);
+        hoverCloseAnimator.setStartDelay(0);
+        hoverCloseAnimator.setDuration(HOVER_TOOL_TIP_EXIT_DURATION);
+        mHoverToolTipView.setCustomCloseAnimation(hoverCloseAnimator);
+
+        AnimatorSet hoverOpenAnimator = new AnimatorSet();
+        ObjectAnimator textOpenAnimator = ObjectAnimator.ofInt(mHoverToolTipView, TEXT_ALPHA, 255);
+        textOpenAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0.33f, 1f));
+        ObjectAnimator scaleOpenAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, SCALE_Y, 1f);
+        scaleOpenAnimator.setInterpolator(Interpolators.EMPHASIZED);
+        ObjectAnimator alphaOpenAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, ALPHA, 1f);
+        alphaOpenAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0.1f, 0.33f));
+        hoverOpenAnimator.playTogether(
+                scaleOpenAnimator,
+                textOpenAnimator,
+                alphaOpenAnimator);
+        hoverOpenAnimator.setStartDelay(HOVER_TOOL_TIP_REVEAL_START_DELAY);
+        hoverOpenAnimator.setDuration(HOVER_TOOL_TIP_REVEAL_DURATION);
+        mHoverToolTipView.setCustomOpenAnimation(hoverOpenAnimator);
+
+        mHoverToolTipView.addOnLayoutChangeListener(
+                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                    mHoverToolTipView.setPivotY(bottom);
+                    mHoverToolTipView.setY(mTaskbarView.getTop() - (bottom - top));
+                });
+        mHoverToolTipView.setScaleY(0f);
+        mHoverToolTipView.setAlpha(0f);
+    }
+
+    @Override
+    public boolean onHover(View v, MotionEvent event) {
+        boolean isAnyOtherFloatingViewOpen =
+                AbstractFloatingView.hasOpenView(mActivity, TYPE_ALL_EXCEPT_ON_BOARD_POPUP);
+        if (isAnyOtherFloatingViewOpen) {
+            mHoverToolTipHandler.removeCallbacksAndMessages(null);
+        }
+        // If hover leaves a taskbar icon animate the tooltip closed.
+        if (event.getAction() == ACTION_HOVER_EXIT) {
+            startHideHoverToolTip();
+            mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, false);
+            return true;
+        } else if (!isAnyOtherFloatingViewOpen && event.getAction() == ACTION_HOVER_ENTER) {
+            // If hovering above a taskbar icon starts, animate the tooltip open. Do not
+            // reveal if any floating views such as folders or edu pop-ups are open.
+            startRevealHoverToolTip();
+            mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, true);
+            return true;
+        }
+        return false;
+    }
+
+    private void startRevealHoverToolTip() {
+        mHoverToolTipHandler.postDelayed(mRevealHoverToolTipRunnable,
+                HOVER_TOOL_TIP_REVEAL_START_DELAY);
+    }
+
+    private void revealHoverToolTip() {
+        if (mHoverView == null || mToolTipText == null) {
+            return;
+        }
+        if (mHoverView instanceof FolderIcon && !((FolderIcon) mHoverView).getIconVisible()) {
+            return;
+        }
+        mActivity.setTaskbarWindowFullscreen(true);
+        Rect iconViewBounds = Utilities.getViewBounds(mHoverView);
+        mHoverToolTipView.showAtLocation(mToolTipText, iconViewBounds.centerX(),
+                mTaskbarView.getTop(), /* shouldAutoClose= */ false);
+    }
+
+    private void startHideHoverToolTip() {
+        mHoverToolTipHandler.removeCallbacks(mRevealHoverToolTipRunnable);
+        int accessibilityHideTimeout = AccessibilityManagerCompat.getRecommendedTimeoutMillis(
+                mActivity, /* originalTimeout= */ 0, FLAG_CONTENT_TEXT);
+        mHoverToolTipHandler.postDelayed(mHideHoverToolTipRunnable, accessibilityHideTimeout);
+    }
+
+    private void hideHoverToolTip() {
+        mHoverToolTipView.close(/* animate = */ true);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index d6e559a..5a4534e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -15,12 +15,15 @@
  */
 package com.android.launcher3.taskbar
 
+import android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR
 import android.graphics.Insets
 import android.graphics.Region
 import android.os.Binder
 import android.os.IBinder
+import android.view.Gravity
 import android.view.InsetsFrameProvider
 import android.view.InsetsFrameProvider.SOURCE_DISPLAY
+import android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER
 import android.view.InsetsSource.FLAG_SUPPRESS_SCRIM
 import android.view.ViewTreeObserver
 import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME
@@ -41,6 +44,7 @@
 import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
 import com.android.launcher3.util.DisplayController
 import java.io.PrintWriter
+import kotlin.jvm.optionals.getOrNull
 
 /** Handles the insets that Taskbar provides to underlying apps and the IME. */
 class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTaskbarController {
@@ -55,13 +59,13 @@
     private val touchableRegion: Region = Region()
     private val insetsOwner: IBinder = Binder()
     private val deviceProfileChangeListener = { _: DeviceProfile ->
-        onTaskbarWindowHeightOrInsetsChanged()
+        onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
     }
     private val gestureNavSettingsObserver =
         GestureNavigationSettingsObserver(
             context.mainThreadHandler,
             context,
-            this::onTaskbarWindowHeightOrInsetsChanged
+            this::onTaskbarOrBubblebarWindowHeightOrInsetsChanged
         )
 
     // Initialized in init.
@@ -71,7 +75,7 @@
     fun init(controllers: TaskbarControllers) {
         this.controllers = controllers
         windowLayoutParams = context.windowLayoutParams
-        onTaskbarWindowHeightOrInsetsChanged()
+        onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
 
         context.addOnDeviceProfileChangeListener(deviceProfileChangeListener)
         gestureNavSettingsObserver.registerForCallingUser()
@@ -82,109 +86,165 @@
         gestureNavSettingsObserver.unregister()
     }
 
-    fun onTaskbarWindowHeightOrInsetsChanged() {
-        if (context.isGestureNav) {
-            windowLayoutParams.providedInsets =
-                arrayOf(
-                    InsetsFrameProvider(insetsOwner, 0, navigationBars())
-                        .setFlags(FLAG_SUPPRESS_SCRIM, FLAG_SUPPRESS_SCRIM),
-                    InsetsFrameProvider(insetsOwner, 0, tappableElement()),
-                    InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()),
-                    InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures())
-                        .setSource(SOURCE_DISPLAY),
-                    InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures())
-                        .setSource(SOURCE_DISPLAY)
-                )
-        } else {
-            windowLayoutParams.providedInsets =
-                arrayOf(
-                    InsetsFrameProvider(insetsOwner, 0, navigationBars()),
-                    InsetsFrameProvider(insetsOwner, 0, tappableElement()),
-                    InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures())
-                )
-        }
-
-        val touchableHeight = controllers.taskbarStashController.touchableHeight
-        touchableRegion.set(
-            0,
-            windowLayoutParams.height - touchableHeight,
-            context.deviceProfile.widthPx,
-            windowLayoutParams.height
-        )
-        val contentHeight = controllers.taskbarStashController.contentHeightToReportToApps
+    fun onTaskbarOrBubblebarWindowHeightOrInsetsChanged() {
         val tappableHeight = controllers.taskbarStashController.tappableHeightToReportToApps
-        val res = context.resources
-        for (provider in windowLayoutParams.providedInsets) {
-            if (provider.type == navigationBars() || provider.type == mandatorySystemGestures()) {
-                provider.insetsSize = getInsetsByNavMode(contentHeight)
-            } else if (provider.type == tappableElement()) {
-                provider.insetsSize = getInsetsByNavMode(tappableHeight)
-            } else if (provider.type == systemGestures() && provider.index == INDEX_LEFT) {
-                provider.insetsSize =
-                    Insets.of(
-                        gestureNavSettingsObserver.getLeftSensitivityForCallingUser(res),
-                        0,
-                        0,
-                        0
-                    )
-            } else if (provider.type == systemGestures() && provider.index == INDEX_RIGHT) {
-                provider.insetsSize =
-                    Insets.of(
-                        0,
-                        0,
-                        gestureNavSettingsObserver.getRightSensitivityForCallingUser(res),
-                        0
-                    )
-            }
-        }
-
-        val imeInsetsSize = getInsetsByNavMode(taskbarHeightForIme)
-        val insetsSizeOverride =
-            arrayOf(
-                InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
-            )
-        // Use 0 tappableElement insets for the VoiceInteractionWindow when gesture nav is enabled.
-        val visInsetsSizeForGestureNavTappableElement = getInsetsByNavMode(0)
-        val insetsSizeOverrideForGestureNavTappableElement =
-            arrayOf(
-                InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
-                InsetsFrameProvider.InsetsSizeOverride(
-                    TYPE_VOICE_INTERACTION,
-                    visInsetsSizeForGestureNavTappableElement
-                ),
-            )
-        for (provider in windowLayoutParams.providedInsets) {
-            if (context.isGestureNav && provider.type == tappableElement()) {
-                provider.insetsSizeOverrides = insetsSizeOverrideForGestureNavTappableElement
-            } else if (provider.type != systemGestures()) {
-                // We only override insets at the bottom of the screen
-                provider.insetsSizeOverrides = insetsSizeOverride
-            }
-        }
-
         // We only report tappableElement height for unstashed, persistent taskbar,
         // which is also when we draw the rounded corners above taskbar.
-        windowLayoutParams.insetsRoundedCornerFrame = tappableHeight > 0
+        val insetsRoundedCornerFlag =
+            if (tappableHeight > 0) {
+                FLAG_INSETS_ROUNDED_CORNER
+            } else {
+                0
+            }
+
+        windowLayoutParams.providedInsets = getProvidedInsets(insetsRoundedCornerFlag)
+        if (!context.isGestureNav) {
+            if (windowLayoutParams.paramsForRotation != null) {
+                for (layoutParams in windowLayoutParams.paramsForRotation) {
+                    layoutParams.providedInsets = getProvidedInsets(insetsRoundedCornerFlag)
+                }
+            }
+        }
+
+        val taskbarTouchableHeight = controllers.taskbarStashController.touchableHeight
+        val bubblesTouchableHeight =
+            if (controllers.bubbleControllers.isPresent) {
+                controllers.bubbleControllers.get().bubbleStashController.touchableHeight
+            } else {
+                0
+            }
+        val touchableHeight = Math.max(taskbarTouchableHeight, bubblesTouchableHeight)
+
+        if (
+            controllers.bubbleControllers.isPresent &&
+                controllers.bubbleControllers.get().bubbleStashController.isBubblesShowingOnHome
+        ) {
+            val iconBounds =
+                controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds
+            touchableRegion.set(
+                iconBounds.left,
+                iconBounds.top,
+                iconBounds.right,
+                iconBounds.bottom
+            )
+        } else {
+            touchableRegion.set(
+                0,
+                windowLayoutParams.height - touchableHeight,
+                context.deviceProfile.widthPx,
+                windowLayoutParams.height
+            )
+        }
+
+        val gravity = windowLayoutParams.gravity
+        for (provider in windowLayoutParams.providedInsets) {
+            setProviderInsets(provider, gravity)
+        }
+
+        if (windowLayoutParams.paramsForRotation != null) {
+            // Add insets for navbar rotated params
+            for (layoutParams in windowLayoutParams.paramsForRotation) {
+                for (provider in layoutParams.providedInsets) {
+                    setProviderInsets(provider, layoutParams.gravity)
+                }
+            }
+        }
 
         context.notifyUpdateLayoutParams()
     }
 
     /**
-     * @return [Insets] where the [bottomInset] is either used as a bottom inset or
-     *
-     * ```
-     *         right/left inset if using 3 button nav
-     * ```
+     * The inset types and number of insets provided have to match for both gesture nav and button
+     * nav. The values and the order of the elements in array are allowed to differ.
+     * Reason being WM does not allow types and number of insets changing for a given window once it
+     * is added into the hierarchy for performance reasons.
      */
-    private fun getInsetsByNavMode(bottomInset: Int): Insets {
-        val devicePortrait = !context.deviceProfile.isLandscape
-        if (!TaskbarManager.isPhoneButtonNavMode(context) || devicePortrait) {
+    private fun getProvidedInsets(insetsRoundedCornerFlag: Int): Array<InsetsFrameProvider> {
+        val navBarsFlag =
+                (if (context.isGestureNav) FLAG_SUPPRESS_SCRIM else 0) or insetsRoundedCornerFlag
+        return arrayOf(
+                InsetsFrameProvider(insetsOwner, 0, navigationBars())
+                        .setFlags(
+                                navBarsFlag,
+                                FLAG_SUPPRESS_SCRIM or FLAG_INSETS_ROUNDED_CORNER
+                        ),
+                InsetsFrameProvider(insetsOwner, 0, tappableElement()),
+                InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()),
+                InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures())
+                        .setSource(SOURCE_DISPLAY),
+                InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures())
+                        .setSource(SOURCE_DISPLAY)
+        )
+    }
+
+    private fun setProviderInsets(provider: InsetsFrameProvider, gravity: Int) {
+        val contentHeight = controllers.taskbarStashController.contentHeightToReportToApps
+        val tappableHeight = controllers.taskbarStashController.tappableHeightToReportToApps
+        val res = context.resources
+        if (provider.type == navigationBars() || provider.type == mandatorySystemGestures()) {
+            provider.insetsSize = getInsetsForGravity(contentHeight, gravity)
+        } else if (provider.type == tappableElement()) {
+            provider.insetsSize = getInsetsForGravity(tappableHeight, gravity)
+        } else if (provider.type == systemGestures() && provider.index == INDEX_LEFT) {
+            val leftIndexInset =
+                    if (context.isThreeButtonNav) 0
+                    else gestureNavSettingsObserver.getLeftSensitivityForCallingUser(res)
+            provider.insetsSize = Insets.of(leftIndexInset, 0, 0, 0)
+        } else if (provider.type == systemGestures() && provider.index == INDEX_RIGHT) {
+            val rightIndexInset =
+                    if (context.isThreeButtonNav) 0
+                    else gestureNavSettingsObserver.getRightSensitivityForCallingUser(res)
+            provider.insetsSize = Insets.of(0, 0, rightIndexInset, 0)
+        }
+
+
+        // When in gesture nav, report the stashed height to the IME, to allow hiding the
+        // IME navigation bar.
+        val imeInsetsSize = if (ENABLE_HIDE_IME_CAPTION_BAR && context.isGestureNav) {
+            getInsetsForGravity(controllers.taskbarStashController.stashedHeight, gravity);
+        } else {
+            getInsetsForGravity(taskbarHeightForIme, gravity)
+        }
+        val imeInsetsSizeOverride =
+                arrayOf(
+                        InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
+                )
+        // Use 0 tappableElement insets for the VoiceInteractionWindow when gesture nav is enabled.
+        val visInsetsSizeForTappableElement =
+                if (context.isGestureNav) getInsetsForGravity(0, gravity)
+                else getInsetsForGravity(tappableHeight, gravity)
+        val insetsSizeOverrideForTappableElement =
+                arrayOf(
+                        InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
+                        InsetsFrameProvider.InsetsSizeOverride(
+                                TYPE_VOICE_INTERACTION,
+                                visInsetsSizeForTappableElement
+                        ),
+                )
+        if ((context.isGestureNav || TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW)
+                && provider.type == tappableElement()) {
+            provider.insetsSizeOverrides = insetsSizeOverrideForTappableElement
+        } else if (provider.type != systemGestures()) {
+            // We only override insets at the bottom of the screen
+            provider.insetsSizeOverrides = imeInsetsSizeOverride
+        }
+    }
+
+    /**
+     * @return [Insets] where the [inset] is either used as a bottom inset or
+     * right/left inset if using 3 button nav
+     */
+    private fun getInsetsForGravity(inset: Int, gravity: Int): Insets {
+        if ((gravity and Gravity.BOTTOM) == Gravity.BOTTOM) {
             // Taskbar or portrait phone mode
-            return Insets.of(0, 0, 0, bottomInset)
+            return Insets.of(0, 0, 0, inset)
         }
 
         // TODO(b/230394142): seascape
-        return Insets.of(0, 0, bottomInset, 0)
+        val isSeascape = (gravity and Gravity.START) == Gravity.START
+        val leftInset = if (isSeascape) inset else 0
+        val rightInset = if (isSeascape) 0 else inset
+        return Insets.of(leftInset , 0, rightInset, 0)
     }
 
     /**
@@ -199,6 +259,9 @@
             context.dragLayer,
             insetsInfo.touchableRegion
         )
+        val bubbleBarVisible =
+            controllers.bubbleControllers.isPresent &&
+                controllers.bubbleControllers.get().bubbleBarViewController.isBubbleBarVisible()
         var insetsIsTouchableRegion = true
         if (context.dragLayer.alpha < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
             // Let touches pass through us.
@@ -219,16 +282,33 @@
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME)
             insetsIsTouchableRegion = false
         } else if (
-            controllers.taskbarViewController.areIconsVisible() || context.isNavBarKidsModeActive
+            controllers.taskbarViewController.areIconsVisible() ||
+                context.isNavBarKidsModeActive ||
+                bubbleBarVisible
         ) {
             // Taskbar has some touchable elements, take over the full taskbar area
             if (
                 controllers.uiController.isInOverview &&
                     DisplayController.isTransientTaskbar(context)
             ) {
-                insetsInfo.touchableRegion.set(
+                val region =
                     controllers.taskbarActivityContext.dragLayer.lastDrawnTransientRect.toRegion()
-                )
+                val bubbleBarBounds =
+                    controllers.bubbleControllers.getOrNull()?.let { bubbleControllers ->
+                        if (!bubbleControllers.bubbleStashController.isBubblesShowingOnOverview) {
+                            return@let null
+                        }
+                        if (!bubbleControllers.bubbleBarViewController.isBubbleBarVisible) {
+                            return@let null
+                        }
+                        bubbleControllers.bubbleBarViewController.bubbleBarBounds
+                    }
+
+                // Include the bounds of the bubble bar in the touchable region if they exist.
+                if (bubbleBarBounds != null) {
+                    region.op(bubbleBarBounds, Region.Op.UNION)
+                }
+                insetsInfo.touchableRegion.set(region)
             } else {
                 insetsInfo.touchableRegion.set(touchableRegion)
             }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index ddea51f..90f7bea 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -204,7 +204,7 @@
                     updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, false);
                     // TODO(b/279514548) Cleans up bad state that can occur when user interacts with
                     // taskbar on top of transparent activity.
-                    if (finalState == LauncherState.NORMAL && mLauncher.isResumed()) {
+                    if (finalState == LauncherState.NORMAL && mLauncher.hasBeenResumed()) {
                         updateStateForFlag(FLAG_RESUMED, true);
                     }
                     applyState();
@@ -249,7 +249,6 @@
 
         mIconAlignment.finishAnimation();
 
-        Log.d("b/260135164", "onDestroy - updateIconAlphaForHome(1)");
         mLauncher.getHotseat().setIconsAlpha(1f);
         mLauncher.getStateManager().removeStateListener(mStateListener);
 
@@ -409,6 +408,14 @@
                     + ", mLauncherState: " + mLauncherState
                     + ", toAlignment: " + toAlignment);
         }
+        mControllers.bubbleControllers.ifPresent(controllers -> {
+            // Show the bubble bar when on launcher home or in overview.
+            boolean onHome = isInLauncher && mLauncherState == LauncherState.NORMAL;
+            boolean onOverview = mLauncherState == LauncherState.OVERVIEW;
+            controllers.bubbleStashController.setBubblesShowingOnHome(onHome);
+            controllers.bubbleStashController.setBubblesShowingOnOverview(onOverview);
+        });
+
         AnimatorSet animatorSet = new AnimatorSet();
 
         if (hasAnyFlag(changedFlags, FLAG_LAUNCHER_IN_STATE_TRANSITION)) {
@@ -479,7 +486,8 @@
                     public void onAnimationEnd(Animator animation) {
                         TaskbarStashController stashController =
                                 mControllers.taskbarStashController;
-                        stashController.updateAndAnimateTransientTaskbar(/* stash */ true);
+                        stashController.updateAndAnimateTransientTaskbar(
+                                /* stash */ true, /* bubblesShouldFollow */ true);
                     }
                 });
             } else {
@@ -636,8 +644,6 @@
                 public void onAnimationEnd(Animator animation) {
                     if (isInStashedState && committed) {
                         // Reset hotseat alpha to default
-                        Log.d("b/260135164",
-                                "playStateTransitionAnim#onAnimationEnd - setIconsAlpha(1)");
                         mLauncher.getHotseat().setIconsAlpha(1);
                     }
                 }
@@ -705,8 +711,6 @@
 
     private void updateIconAlphaForHome(float alpha) {
         if (mControllers.taskbarActivityContext.isDestroyed()) {
-            Log.e("b/260135164", "updateIconAlphaForHome is called after Taskbar is destroyed",
-                    new Exception());
             return;
         }
         mIconAlphaForHome.setValue(alpha);
@@ -717,9 +721,6 @@
          * Hide Launcher Hotseat icons when Taskbar icons have opacity. Both icon sets
          * should not be visible at the same time.
          */
-        Log.d("b/260135164",
-                "updateIconAlphaForHome - setIconsAlpha(" + (hotseatVisible ? 1 : 0)
-                        + "), isTaskbarPresent: " + mLauncher.getDeviceProfile().isTaskbarPresent);
         mLauncher.getHotseat().setIconsAlpha(hotseatVisible ? 1 : 0);
         if (mIsQsbInline) {
             mLauncher.getHotseat().setQsbAlpha(hotseatVisible ? 1 : 0);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 738ff87..b115ca8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -22,13 +22,13 @@
 
 import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING;
 import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING_KEY;
-import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
-import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
+import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.util.DisplayController.TASKBAR_NOT_DESTROYED_TAG;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
 
 import android.annotation.SuppressLint;
+import android.app.Activity;
 import android.app.PendingIntent;
 import android.content.ComponentCallbacks;
 import android.content.Context;
@@ -41,6 +41,7 @@
 import android.net.Uri;
 import android.os.Handler;
 import android.os.SystemProperties;
+import android.os.Trace;
 import android.provider.Settings;
 import android.util.Log;
 import android.view.Display;
@@ -50,19 +51,20 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.taskbar.unfold.NonDestroyableScopedUnfoldTransitionProgressProvider;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
-import com.android.launcher3.util.DisplayController;
-import com.android.launcher3.util.NavigationMode;
+import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
 import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TouchInteractionService;
+import com.android.quickstep.util.AssistUtils;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
@@ -77,6 +79,22 @@
     private static final String TAG = "TaskbarManager";
     private static final boolean DEBUG = false;
 
+    /**
+     * All the configurations which do not initiate taskbar recreation.
+     * This includes all the configurations defined in Launcher's manifest entry and
+     * ActivityController#filterConfigChanges
+     */
+    private static final int SKIP_RECREATE_CONFIG_CHANGES = ActivityInfo.CONFIG_WINDOW_CONFIGURATION
+            | ActivityInfo.CONFIG_KEYBOARD
+            | ActivityInfo.CONFIG_KEYBOARD_HIDDEN
+            | ActivityInfo.CONFIG_MCC
+            | ActivityInfo.CONFIG_MNC
+            | ActivityInfo.CONFIG_NAVIGATION
+            | ActivityInfo.CONFIG_ORIENTATION
+            | ActivityInfo.CONFIG_SCREEN_SIZE
+            | ActivityInfo.CONFIG_SCREEN_LAYOUT
+            | ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
+
     public static final boolean FLAG_HIDE_NAVBAR_WINDOW =
             SystemProperties.getBoolean("persist.wm.debug.hide_navbar_window", false);
 
@@ -87,12 +105,11 @@
             Settings.Secure.NAV_BAR_KIDS_MODE);
 
     private final Context mContext;
-    private final DisplayController mDisplayController;
     private final TaskbarNavButtonController mNavButtonController;
-    private final SettingsCache.OnChangeListener mUserSetupCompleteListener;
-    private final SettingsCache.OnChangeListener mNavBarKidsModeListener;
     private final ComponentCallbacks mComponentCallbacks;
-    private final SimpleBroadcastReceiver mShutdownReceiver;
+
+    private final SimpleBroadcastReceiver mShutdownReceiver =
+            new SimpleBroadcastReceiver(i -> destroyExistingTaskbar());
 
     // The source for this provider is set when Launcher is available
     // We use 'non-destroyable' version here so the original provider won't be destroyed
@@ -100,7 +117,6 @@
     // It's destruction/creation will be managed by the activity.
     private final ScopedUnfoldTransitionProgressProvider mUnfoldProgressProvider =
             new NonDestroyableScopedUnfoldTransitionProgressProvider();
-    private NavigationMode mNavMode;
 
     private TaskbarActivityContext mTaskbarActivityContext;
     private StatefulActivity mActivity;
@@ -111,19 +127,11 @@
     private final TaskbarSharedState mSharedState = new TaskbarSharedState();
 
     /**
-     * We use WindowManager's ComponentCallbacks() for most of the config changes, however for
-     * navigation mode, that callback gets called too soon, before it's internal navigation mode
-     * reflects the current one.
-     * DisplayController's callback is delayed enough to get the correct nav mode value
-     *
-     * We also use density change here because DeviceProfile has had a chance to update it's state
-     * whereas density for component callbacks registered in this class don't update DeviceProfile.
-     * Confused? Me too. Make it less confusing (TODO: b/227669780)
-     *
-     * Flags used with {@link #mDispInfoChangeListener}
+     * We use WindowManager's ComponentCallbacks() for internal UI changes (similar to an Activity)
+     * which comes via a different channel
      */
-    private static final int CHANGE_FLAGS = CHANGE_NAVIGATION_MODE | CHANGE_DENSITY;
-    private final DisplayController.DisplayInfoChangeListener mDispInfoChangeListener;
+    private final OnIDPChangeListener mIdpChangeListener = c -> recreateTaskbar();
+    private final SettingsCache.OnChangeListener mOnSettingsChangeListener = c -> recreateTaskbar();
 
     private boolean mUserUnlocked = false;
 
@@ -144,101 +152,121 @@
                 }
             };
 
+    private final ActivityLifecycleCallbacksAdapter mLifecycleCallbacks =
+            new ActivityLifecycleCallbacksAdapter() {
+                @Override
+                public void onActivityDestroyed(Activity activity) {
+                    if (mActivity != activity) return;
+                    if (mActivity != null) {
+                        mActivity.removeOnDeviceProfileChangeListener(
+                                mDebugActivityDeviceProfileChanged);
+                        Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                                "unregistering activity lifecycle callbacks from "
+                                        + "onActivityDestroyed.");
+                        mActivity.unregisterActivityLifecycleCallbacks(this);
+                    }
+                    mActivity = null;
+                    debugWhyTaskbarNotDestroyed("clearActivity");
+                    if (mTaskbarActivityContext != null) {
+                        mTaskbarActivityContext.setUIController(TaskbarUIController.DEFAULT);
+                    }
+                    mUnfoldProgressProvider.setSourceProvider(null);
+                }
+            };
+
+    UnfoldTransitionProgressProvider.TransitionProgressListener mUnfoldTransitionProgressListener =
+            new UnfoldTransitionProgressProvider.TransitionProgressListener() {
+                @Override
+                public void onTransitionStarted() {
+                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                            "fold/unfold transition started getting called.");
+                }
+
+                @Override
+                public void onTransitionProgress(float progress) {
+                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                            "fold/unfold transition progress : " + progress);
+                }
+
+                @Override
+                public void onTransitionFinishing() {
+                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                            "fold/unfold transition finishing getting called.");
+
+                }
+
+                @Override
+                public void onTransitionFinished() {
+                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                            "fold/unfold transition finished getting called.");
+
+                }
+            };
+
     @SuppressLint("WrongConstant")
     public TaskbarManager(TouchInteractionService service) {
-        mDisplayController = DisplayController.INSTANCE.get(service);
         Display display =
                 service.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY);
         mContext = service.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null);
         mNavButtonController = new TaskbarNavButtonController(service,
-                SystemUiProxy.INSTANCE.get(mContext), new Handler());
-        mUserSetupCompleteListener = isUserSetupComplete -> recreateTaskbar();
-        mNavBarKidsModeListener = isNavBarKidsMode -> recreateTaskbar();
-        // TODO(b/227669780): Consolidate this w/ DisplayController callbacks
+                SystemUiProxy.INSTANCE.get(mContext), new Handler(),
+                AssistUtils.newInstance(mContext));
         mComponentCallbacks = new ComponentCallbacks() {
             private Configuration mOldConfig = mContext.getResources().getConfiguration();
 
             @Override
             public void onConfigurationChanged(Configuration newConfig) {
+                Trace.instantForTrack(Trace.TRACE_TAG_APP, "TaskbarManager",
+                        "onConfigurationChanged: " + newConfig);
                 debugWhyTaskbarNotDestroyed(
                         "TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
                 DeviceProfile dp = mUserUnlocked
                         ? LauncherAppState.getIDP(mContext).getDeviceProfile(mContext)
                         : null;
-                int configDiff = mOldConfig.diff(newConfig);
-                int configDiffForRecreate = configDiff;
-                int configsRequiringRecreate = ActivityInfo.CONFIG_ASSETS_PATHS
-                        | ActivityInfo.CONFIG_LAYOUT_DIRECTION | ActivityInfo.CONFIG_UI_MODE
-                        | ActivityInfo.CONFIG_SCREEN_SIZE;
-                if ((configDiff & ActivityInfo.CONFIG_SCREEN_SIZE) != 0
-                        && mTaskbarActivityContext != null && dp != null
-                        && !isPhoneMode(dp)) {
-                    // Additional check since this callback gets fired multiple times w/o
-                    // screen size changing, or when simply rotating the device.
-                    // In the case of phone device rotation, we do want to call recreateTaskbar()
-                    DeviceProfile oldDp = mTaskbarActivityContext.getDeviceProfile();
-                    boolean isOrientationChange =
-                            (configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0;
+                int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES;
 
-                    int newOrientation = newConfig.windowConfiguration.getRotation();
-                    int oldOrientation = mOldConfig.windowConfiguration.getRotation();
-                    int oldWidth = isOrientationChange ? oldDp.heightPx : oldDp.widthPx;
-                    int oldHeight = isOrientationChange ? oldDp.widthPx : oldDp.heightPx;
-
-                    if ((dp.widthPx == oldWidth && dp.heightPx == oldHeight)
-                            || (newOrientation == oldOrientation)) {
-                        configDiffForRecreate &= ~ActivityInfo.CONFIG_SCREEN_SIZE;
-                    }
-                }
                 if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0) {
                     // Only recreate for theme changes, not other UI mode changes such as docking.
                     int oldUiNightMode = (mOldConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
                     int newUiNightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
                     if (oldUiNightMode == newUiNightMode) {
-                        configDiffForRecreate &= ~ActivityInfo.CONFIG_UI_MODE;
+                        configDiff &= ~ActivityInfo.CONFIG_UI_MODE;
                     }
                 }
 
                 debugWhyTaskbarNotDestroyed("ComponentCallbacks#onConfigurationChanged() "
-                        + "configDiffForRecreate="
-                        + Configuration.configurationDiffToString(configDiffForRecreate));
-                if ((configDiffForRecreate & configsRequiringRecreate) != 0) {
+                        + "configDiff=" + Configuration.configurationDiffToString(configDiff));
+                if (configDiff != 0 || mTaskbarActivityContext == null) {
                     recreateTaskbar();
                 } else {
                     // Config change might be handled without re-creating the taskbar
-                    if (mTaskbarActivityContext != null) {
-                        if (dp != null && !isTaskbarPresent(dp)) {
-                            destroyExistingTaskbar();
-                        } else {
-                            if (dp != null && isTaskbarPresent(dp)) {
-                                mTaskbarActivityContext.updateDeviceProfile(dp, mNavMode);
+                    if (dp != null && !isTaskbarPresent(dp)) {
+                        destroyExistingTaskbar();
+                    } else {
+                        if (dp != null && isTaskbarPresent(dp)) {
+                            if (FLAG_HIDE_NAVBAR_WINDOW) {
+                                // Re-initialize for screen size change? Should this be done
+                                // by looking at screen-size change flag in configDiff in the
+                                // block above?
+                                recreateTaskbar();
+                            } else {
+                                mTaskbarActivityContext.updateDeviceProfile(dp);
                             }
-                            mTaskbarActivityContext.onConfigurationChanged(configDiff);
                         }
+                        mTaskbarActivityContext.onConfigurationChanged(configDiff);
                     }
                 }
-                mOldConfig = newConfig;
+                mOldConfig = new Configuration(newConfig);
             }
 
             @Override
             public void onLowMemory() { }
         };
-        mShutdownReceiver = new SimpleBroadcastReceiver(i ->
-                destroyExistingTaskbar());
-        mDispInfoChangeListener = (context, info, flags) -> {
-            if ((flags & CHANGE_FLAGS) != 0) {
-                mNavMode = info.navigationMode;
-                recreateTaskbar();
-            }
-            debugWhyTaskbarNotDestroyed("DisplayInfoChangeListener#"
-                    + mDisplayController.getChangeFlagsString(flags));
-        };
-        mNavMode = mDisplayController.getInfo().navigationMode;
-        mDisplayController.addChangeListener(mDispInfoChangeListener);
-        SettingsCache.INSTANCE.get(mContext).register(USER_SETUP_COMPLETE_URI,
-                mUserSetupCompleteListener);
-        SettingsCache.INSTANCE.get(mContext).register(NAV_BAR_KIDS_MODE,
-                mNavBarKidsModeListener);
+        SettingsCache.INSTANCE.get(mContext)
+                .register(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
+        SettingsCache.INSTANCE.get(mContext)
+                .register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
+        Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
         mContext.registerComponentCallbacks(mComponentCallbacks);
         mShutdownReceiver.register(mContext, Intent.ACTION_SHUTDOWN);
         UI_HELPER_EXECUTOR.execute(() -> {
@@ -279,6 +307,25 @@
     }
 
     /**
+     * Toggles All Apps for Taskbar or Launcher depending on the current state.
+     *
+     * @param homeAllAppsIntent Intent used if Taskbar is not enabled or Launcher is resumed.
+     */
+    public void toggleAllApps(Intent homeAllAppsIntent) {
+        if (mTaskbarActivityContext == null) {
+            mContext.startActivity(homeAllAppsIntent);
+            return;
+        }
+
+        if (mActivity != null && mActivity.isResumed() && !mActivity.isInState(OVERVIEW)) {
+            mContext.startActivity(homeAllAppsIntent);
+            return;
+        }
+
+        mTaskbarActivityContext.toggleAllApps();
+    }
+
+    /**
      * Displays a frame of the first Launcher reveal animation.
      *
      * This should be used to run a first Launcher reveal animation whose progress matches a swipe
@@ -294,6 +341,7 @@
      */
     public void onUserUnlocked() {
         mUserUnlocked = true;
+        LauncherAppState.getIDP(mContext).addOnChangeListener(mIdpChangeListener);
         recreateTaskbar();
     }
 
@@ -304,14 +352,18 @@
         if (mActivity == activity) {
             return;
         }
-        if (mActivity != null) {
-            mActivity.removeOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
-        }
+        removeActivityCallbacksAndListeners();
         mActivity = activity;
         debugWhyTaskbarNotDestroyed("Set mActivity=" + mActivity);
         mActivity.addOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
+        Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                "registering activity lifecycle callbacks from setActivity().");
+        mActivity.registerActivityLifecycleCallbacks(mLifecycleCallbacks);
         UnfoldTransitionProgressProvider unfoldTransitionProgressProvider =
                 getUnfoldTransitionProgressProviderForActivity(activity);
+        if (unfoldTransitionProgressProvider != null) {
+            unfoldTransitionProgressProvider.addCallback(mUnfoldTransitionProgressListener);
+        }
         mUnfoldProgressProvider.setSourceProvider(unfoldTransitionProgressProvider);
 
         if (mTaskbarActivityContext != null) {
@@ -349,59 +401,50 @@
     }
 
     /**
-     * Clears a previously set {@link StatefulActivity}
-     */
-    public void clearActivity(@NonNull StatefulActivity activity) {
-        if (mActivity == activity) {
-            mActivity.removeOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
-            mActivity = null;
-            debugWhyTaskbarNotDestroyed("clearActivity");
-            if (mTaskbarActivityContext != null) {
-                mTaskbarActivityContext.setUIController(TaskbarUIController.DEFAULT);
-            }
-            mUnfoldProgressProvider.setSourceProvider(null);
-        }
-    }
-
-    /**
      * This method is called multiple times (ex. initial init, then when user unlocks) in which case
      * we fully want to destroy an existing taskbar and create a new one.
      * In other case (folding/unfolding) we don't need to remove and add window.
      */
     @VisibleForTesting
     public void recreateTaskbar() {
-        DeviceProfile dp = mUserUnlocked ?
+        Trace.beginSection("recreateTaskbar");
+        try {
+            DeviceProfile dp = mUserUnlocked ?
                 LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null;
 
-        destroyExistingTaskbar();
+            destroyExistingTaskbar();
 
-        boolean isTaskbarEnabled = dp != null && isTaskbarPresent(dp);
-        debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled
+            boolean isTaskbarEnabled = dp != null && isTaskbarPresent(dp);
+            debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled
                 + " [dp != null (i.e. mUserUnlocked)]=" + (dp != null)
                 + " FLAG_HIDE_NAVBAR_WINDOW=" + FLAG_HIDE_NAVBAR_WINDOW
                 + " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent));
-        if (!isTaskbarEnabled) {
-            SystemUiProxy.INSTANCE.get(mContext)
+            if (!isTaskbarEnabled) {
+                SystemUiProxy.INSTANCE.get(mContext)
                     .notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
-            return;
-        }
+                return;
+            }
 
-        if (mTaskbarActivityContext == null) {
-            mTaskbarActivityContext = new TaskbarActivityContext(mContext, dp, mNavButtonController,
+            if (mTaskbarActivityContext == null) {
+                mTaskbarActivityContext = new TaskbarActivityContext(mContext, dp,
+                    mNavButtonController,
                     mUnfoldProgressProvider);
-        } else {
-            mTaskbarActivityContext.updateDeviceProfile(dp, mNavMode);
-        }
-        mTaskbarActivityContext.init(mSharedState);
+            } else {
+                mTaskbarActivityContext.updateDeviceProfile(dp);
+            }
+            mTaskbarActivityContext.init(mSharedState);
 
-        if (mActivity != null) {
-            mTaskbarActivityContext.setUIController(
+            if (mActivity != null) {
+                mTaskbarActivityContext.setUIController(
                     createTaskbarUIControllerForActivity(mActivity));
-        }
+            }
 
-        // We to wait until user unlocks the device to attach listener.
-        LauncherPrefs.get(mContext).addListener(mTaskbarPinningPreferenceChangeListener,
+            // We to wait until user unlocks the device to attach listener.
+            LauncherPrefs.get(mContext).addListener(mTaskbarPinningPreferenceChangeListener,
                 TASKBAR_PINNING);
+        } finally {
+            Trace.endSection();
+        }
     }
 
     public void onSystemUiFlagsChanged(int systemUiStateFlags) {
@@ -481,23 +524,38 @@
         }
     }
 
+    private void removeActivityCallbacksAndListeners() {
+        if (mActivity != null) {
+            mActivity.removeOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
+            Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                    "unregistering activity lifecycle callbacks from "
+                            + "removeActivityCallbackAndListeners().");
+            mActivity.unregisterActivityLifecycleCallbacks(mLifecycleCallbacks);
+            UnfoldTransitionProgressProvider unfoldTransitionProgressProvider =
+                    getUnfoldTransitionProgressProviderForActivity(mActivity);
+            if (unfoldTransitionProgressProvider != null) {
+                unfoldTransitionProgressProvider.removeCallback(mUnfoldTransitionProgressListener);
+            }
+        }
+    }
+
     /**
      * Called when the manager is no longer needed
      */
     public void destroy() {
         debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
-        if (mActivity != null) {
-            mActivity.removeOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
-        }
-
+        removeActivityCallbacksAndListeners();
         UI_HELPER_EXECUTOR.execute(
                 () -> mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext));
         destroyExistingTaskbar();
-        mDisplayController.removeChangeListener(mDispInfoChangeListener);
-        SettingsCache.INSTANCE.get(mContext).unregister(USER_SETUP_COMPLETE_URI,
-                mUserSetupCompleteListener);
-        SettingsCache.INSTANCE.get(mContext).unregister(NAV_BAR_KIDS_MODE,
-                mNavBarKidsModeListener);
+        if (mUserUnlocked) {
+            LauncherAppState.getIDP(mContext).removeOnChangeListener(mIdpChangeListener);
+        }
+        SettingsCache.INSTANCE.get(mContext)
+                .unregister(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
+        SettingsCache.INSTANCE.get(mContext)
+                .unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
+        Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
         mContext.unregisterComponentCallbacks(mComponentCallbacks);
         mContext.unregisterReceiver(mShutdownReceiver);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 6cf63a9..7692760 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -57,7 +57,7 @@
     private final TaskbarView mContainer;
 
     // Initialized in init.
-    private TaskbarControllers mControllers;
+    protected TaskbarControllers mControllers;
 
     // Used to defer any UI updates during the SUW unstash animation.
     private boolean mDeferUpdatesForSUW;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacksFactory.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacksFactory.kt
new file mode 100644
index 0000000..eb03b4a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacksFactory.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar
+
+import android.content.Context
+import com.android.launcher3.R
+import com.android.launcher3.util.ResourceBasedOverride
+import com.android.launcher3.util.ResourceBasedOverride.Overrides
+
+/** Creates [TaskbarModelCallbacks] instances. */
+open class TaskbarModelCallbacksFactory : ResourceBasedOverride {
+
+    open fun create(
+        activityContext: TaskbarActivityContext,
+        container: TaskbarView,
+    ): TaskbarModelCallbacks = TaskbarModelCallbacks(activityContext, container)
+
+    companion object {
+        @JvmStatic
+        fun newInstance(context: Context): TaskbarModelCallbacksFactory {
+            return Overrides.getObject(
+                TaskbarModelCallbacksFactory::class.java,
+                context,
+                R.string.taskbar_model_callbacks_factory_class,
+            )
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index 610efeb..03df036 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -43,12 +43,16 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.OverviewCommandHelper;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.TouchInteractionService;
+import com.android.quickstep.util.AssistUtils;
+import com.android.quickstep.views.DesktopTaskView;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
@@ -105,15 +109,17 @@
     private final TouchInteractionService mService;
     private final SystemUiProxy mSystemUiProxy;
     private final Handler mHandler;
+    private final AssistUtils mAssistUtils;
     @Nullable private StatsLogManager mStatsLogManager;
 
     private final Runnable mResetLongPress = this::resetScreenUnpin;
 
     public TaskbarNavButtonController(TouchInteractionService service,
-            SystemUiProxy systemUiProxy, Handler handler) {
+            SystemUiProxy systemUiProxy, Handler handler, AssistUtils assistUtils) {
         mService = service;
         mSystemUiProxy = systemUiProxy;
         mHandler = handler;
+        mAssistUtils = assistUtils;
     }
 
     public void onButtonClick(@TaskbarButton int buttonType, View view) {
@@ -155,7 +161,7 @@
         switch (buttonType) {
             case BUTTON_HOME:
                 logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS);
-                startAssistant();
+                onLongPressHome();
                 return true;
             case BUTTON_A11Y:
                 logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS);
@@ -267,6 +273,15 @@
 
     private void navigateHome() {
         TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY);
+
+        if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) {
+            DesktopVisibilityController desktopVisibilityController =
+                    LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
+            if (desktopVisibilityController != null) {
+                desktopVisibilityController.onHomeActionTriggered();
+            }
+        }
+
         mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_HOME);
     }
 
@@ -295,13 +310,16 @@
         }
     }
 
-    private void startAssistant() {
+    private void onLongPressHome() {
         if (mScreenPinned || !mAssistantLongPressEnabled) {
             return;
         }
-        Bundle args = new Bundle();
-        args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
-        mSystemUiProxy.startAssistant(args);
+        // Attempt to start Assist with AssistUtils, otherwise fall back to SysUi's implementation.
+        if (!mAssistUtils.tryStartAssistOverride(INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) {
+            Bundle args = new Bundle();
+            args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
+            mSystemUiProxy.startAssistant(args);
+        }
     }
 
     private void showQuickSettings() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
index 5ea00cf..1c250bf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.taskbar;
 
+import static com.android.launcher3.taskbar.bubbles.BubbleBarController.BUBBLE_BAR_ENABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
 
@@ -23,6 +24,7 @@
 import android.view.animation.PathInterpolator;
 
 import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.SystemUiProxy;
 
 import java.io.PrintWriter;
@@ -63,6 +65,10 @@
      * Updates the scrim state based on the flags.
      */
     public void updateStateForSysuiFlags(int stateFlags, boolean skipAnim) {
+        if (BUBBLE_BAR_ENABLED && DisplayController.isTransientTaskbar(mActivity)) {
+            // These scrims aren't used if bubble bar & transient taskbar are active.
+            return;
+        }
         final boolean bubblesExpanded = (stateFlags & SYSUI_STATE_BUBBLES_EXPANDED) != 0;
         final boolean manageMenuExpanded =
                 (stateFlags & SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index daf9c47..a920dfa 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -18,11 +18,11 @@
 import static android.view.HapticFeedbackConstants.LONG_PRESS;
 import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS;
 
+import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.app.animation.Interpolators.FINAL_FRAME;
+import static com.android.app.animation.Interpolators.INSTANT;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING_KEY;
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED;
-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.config.FeatureFlags.ENABLE_TASKBAR_PINNING;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_LONGPRESS_HIDE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_LONGPRESS_SHOW;
@@ -257,14 +257,15 @@
     private boolean mEnableBlockingTimeoutDuringTests = false;
 
     // Evaluate whether the handle should be stashed
+    private final IntPredicate mIsStashedPredicate = flags -> {
+        boolean inApp = hasAnyFlag(flags, FLAGS_IN_APP);
+        boolean stashedInApp = hasAnyFlag(flags, FLAGS_STASHED_IN_APP);
+        boolean stashedLauncherState = hasAnyFlag(flags, FLAG_IN_STASHED_LAUNCHER_STATE);
+        boolean forceStashed = hasAnyFlag(flags, FLAGS_FORCE_STASHED);
+        return (inApp && stashedInApp) || (!inApp && stashedLauncherState) || forceStashed;
+    };
     private final StatePropertyHolder mStatePropertyHolder = new StatePropertyHolder(
-            flags -> {
-                boolean inApp = hasAnyFlag(flags, FLAGS_IN_APP);
-                boolean stashedInApp = hasAnyFlag(flags, FLAGS_STASHED_IN_APP);
-                boolean stashedLauncherState = hasAnyFlag(flags, FLAG_IN_STASHED_LAUNCHER_STATE);
-                boolean forceStashed = hasAnyFlag(flags, FLAGS_FORCE_STASHED);
-                return (inApp && stashedInApp) || (!inApp && stashedLauncherState) || forceStashed;
-            });
+            mIsStashedPredicate);
 
     private boolean mIsTaskbarSystemActionRegistered = false;
     private TaskbarSharedState mTaskbarSharedState;
@@ -337,7 +338,6 @@
         // us that we're paused until a bit later. This avoids flickering upon recreating taskbar.
         updateStateForFlag(FLAG_IN_APP, true);
         applyState(/* duration = */ 0);
-
         notifyStashChange(/* visible */ false, /* stashed */ isStashedInApp());
     }
 
@@ -504,9 +504,19 @@
     }
 
     /**
-     * Stash or unstashes the transient taskbar.
+     * Stash or unstashes the transient taskbar, using the default TASKBAR_STASH_DURATION.
+     * If bubble bar exists, it will match taskbars stashing behavior.
      */
     public void updateAndAnimateTransientTaskbar(boolean stash) {
+        updateAndAnimateTransientTaskbar(stash, /* shouldBubblesFollow= */ true);
+    }
+
+    /**
+     * Stash or unstashes the transient taskbar.
+     * @param stash whether transient taskbar should be stashed.
+     * @param shouldBubblesFollow whether bubbles should match taskbars behavior.
+     */
+    public void updateAndAnimateTransientTaskbar(boolean stash,  boolean shouldBubblesFollow) {
         if (!DisplayController.isTransientTaskbar(mActivity)) {
             return;
         }
@@ -525,6 +535,34 @@
             updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, stash);
             applyState();
         }
+
+        mControllers.bubbleControllers.ifPresent(controllers -> {
+            if (shouldBubblesFollow) {
+                final boolean willStash = mIsStashedPredicate.test(mState);
+                if (willStash != controllers.bubbleStashController.isStashed()) {
+                    // Typically bubbles gets stashed / unstashed along with Taskbar, however, if
+                    // taskbar is becoming stashed because bubbles is being expanded, we don't want
+                    // to stash bubbles.
+                    if (willStash) {
+                        controllers.bubbleStashController.stashBubbleBar();
+                    } else {
+                        controllers.bubbleStashController.showBubbleBar(false /* expandBubbles */);
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Stashes transient taskbar after it has timed out.
+     */
+    private void updateAndAnimateTransientTaskbarForTimeout() {
+        // If bubbles are expanded we shouldn't stash them when taskbar is hidden
+        // for the timeout.
+        boolean bubbleBarExpanded = mControllers.bubbleControllers.isPresent()
+                && mControllers.bubbleControllers.get().bubbleBarViewController.isExpanded();
+        updateAndAnimateTransientTaskbar(/* stash= */ true,
+                /* shouldBubblesFollow= */ !bubbleBarExpanded);
     }
 
     /**
@@ -636,7 +674,10 @@
                     .setDuration(duration));
             mAnimator.play(mTaskbarImeBgAlpha.animateToValue(
                     hasAnyFlag(FLAG_STASHED_IN_APP_IME) ? 0 : 1).setDuration(duration));
-            mAnimator.addListener(AnimatorListeners.forEndCallback(() -> mAnimator = null));
+            mAnimator.addListener(AnimatorListeners.forEndCallback(() -> {
+                mAnimator = null;
+                mIsStashed = isStashed;
+            }));
             return;
         }
 
@@ -830,8 +871,11 @@
                 .setDuration(isStashed ? duration / 2 : duration));
     }
 
-    private static void play(AnimatorSet as, Animator a, long startDelay, long duration,
+    private static void play(AnimatorSet as, @Nullable Animator a, long startDelay, long duration,
             Interpolator interpolator) {
+        if (a == null) {
+            return;
+        }
         a.setDuration(duration);
         a.setStartDelay(startDelay);
         a.setInterpolator(interpolator);
@@ -897,7 +941,7 @@
     private void onIsStashedChanged(boolean isStashed) {
         mControllers.runAfterInit(() -> {
             mControllers.stashedHandleViewController.onIsStashedChanged(isStashed);
-            mControllers.taskbarInsetsController.onTaskbarWindowHeightOrInsetsChanged();
+            mControllers.taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
         });
     }
 
@@ -1053,6 +1097,7 @@
                     TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR,
                     !hasAnyFlag(FLAG_STASHED_IN_APP_AUTO));
         }
+        mActivity.applyForciblyShownFlagWhileTransientTaskbarUnstashed(!isStashedInApp());
     }
 
     private void notifyStashChange(boolean visible, boolean stashed) {
@@ -1147,7 +1192,7 @@
         if (mControllers.taskbarAutohideSuspendController.isTransientTaskbarStashingSuspended()) {
             return;
         }
-        updateAndAnimateTransientTaskbar(true);
+        updateAndAnimateTransientTaskbarForTimeout();
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
index 1cc6672..deaf024 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
@@ -16,9 +16,9 @@
 package com.android.launcher3.taskbar
 
 import android.view.MotionEvent
+import com.android.app.animation.Interpolators.LINEAR
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
-import com.android.launcher3.anim.Interpolators.LINEAR
 import com.android.launcher3.testing.shared.ResourceUtils
 import com.android.launcher3.touch.SingleAxisSwipeDetector
 import com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE
@@ -108,7 +108,17 @@
         }
 
     override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean {
-        if (!enabled || controllers.taskbarStashController.isStashed) {
+        if (!enabled) {
+            return false
+        }
+        val bubbleControllers = controllers.bubbleControllers.orElse(null)
+        if (bubbleControllers != null && bubbleControllers.bubbleBarViewController.isExpanded) {
+            return false
+        }
+        if (
+            (bubbleControllers == null || bubbleControllers.bubbleStashController.isStashed) &&
+                controllers.taskbarStashController.isStashed
+        ) {
             return false
         }
 
@@ -122,7 +132,12 @@
                 return true
             }
         } else if (ev.action == MotionEvent.ACTION_DOWN) {
-            if (screenCoordinatesEv.y < gestureHeightYThreshold) {
+            val isDownOnBubbleBar =
+                (bubbleControllers != null &&
+                    bubbleControllers.bubbleBarViewController.isEventOverAnyItem(
+                        screenCoordinatesEv
+                    ))
+            if (!isDownOnBubbleBar && screenCoordinatesEv.y < gestureHeightYThreshold) {
                 controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true)
             }
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java
new file mode 100644
index 0000000..5b6fbef
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static com.android.launcher3.Utilities.dpToPx;
+import static com.android.launcher3.Utilities.dpiFromPx;
+
+import android.content.res.Resources;
+import android.util.DisplayMetrics;
+
+import androidx.core.content.res.ResourcesCompat;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
+
+/**
+ * Utility class that contains the different taskbar thresholds logic.
+ */
+public class TaskbarThresholdUtils {
+
+    // We divide the screen into this many parts, and use the result to scale the thresholds to
+    // any size device. Note that this value was calculated arbitrarily by using two tablet devices
+    // as data points.
+    private static final float SCREEN_UNITS = 1 / 80f;
+
+    private static int getThreshold(Resources r, DeviceProfile dp, int thresholdDimen,
+            int multiplierDimen) {
+        if (!FeatureFlags.ENABLE_DYNAMIC_TASKBAR_THRESHOLDS.get()) {
+            return r.getDimensionPixelSize(thresholdDimen);
+        }
+
+        float landscapeScreenHeight = dp.isLandscape ? dp.heightPx : dp.widthPx;
+        float screenPart = (landscapeScreenHeight * SCREEN_UNITS);
+        float defaultDp = dpiFromPx(screenPart, DisplayMetrics.DENSITY_DEVICE_STABLE);
+        float thisDp = dpToPx(defaultDp);
+        float multiplier = ResourcesCompat.getFloat(r, multiplierDimen);
+        float value = (thisDp) * multiplier;
+
+        return Math.round(value);
+    }
+
+    /**
+     * Returns the threshold that determines if we should show taskbar.
+     */
+    public static int getFromNavThreshold(Resources r, DeviceProfile dp) {
+        return getThreshold(r, dp, R.dimen.taskbar_from_nav_threshold,
+                R.dimen.taskbar_nav_threshold_mult);
+    }
+
+    /**
+     * Returns the threshold that we start moving the app window.
+     */
+    public static int getAppWindowThreshold(Resources r, DeviceProfile dp) {
+        return getThreshold(r, dp, R.dimen.taskbar_app_window_threshold,
+                R.dimen.taskbar_app_window_threshold_mult);
+    }
+
+    /**
+     * Returns the threshold for whether we land in home or overview.
+     */
+    public static int getHomeOverviewThreshold(Resources r, DeviceProfile dp) {
+        return getThreshold(r, dp, R.dimen.taskbar_home_overview_threshold,
+                R.dimen.taskbar_home_overview_threshold_mult);
+    }
+
+    /**
+     * Returns the threshold that we use to allow swipe to catch up to finger.
+     */
+    public static int getCatchUpThreshold(Resources r, DeviceProfile dp) {
+        return getThreshold(r, dp, R.dimen.taskbar_catch_up_threshold,
+                R.dimen.taskbar_catch_up_threshold_mult);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
index 065d111..916b1e6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
@@ -26,8 +26,8 @@
 import androidx.annotation.Nullable;
 import androidx.dynamicanimation.animation.SpringForce;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.SpringAnimationBuilder;
 import com.android.launcher3.util.DisplayController;
 
@@ -92,6 +92,10 @@
         mControllers.stashedHandleViewController.setTranslationYForSwipe(transY);
         mControllers.taskbarViewController.setTranslationYForSwipe(transY);
         mControllers.taskbarDragLayerController.setTranslationYForSwipe(transY);
+        mControllers.bubbleControllers.ifPresent(controllers -> {
+            controllers.bubbleBarViewController.setTranslationYForSwipe(transY);
+            controllers.bubbleStashedHandleViewController.setTranslationYForSwipe(transY);
+        });
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 7154731..6fad655 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -40,8 +40,11 @@
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
+import com.android.systemui.shared.recents.model.Task;
 
 import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.stream.Stream;
 
 /**
@@ -204,9 +207,10 @@
             return;
         }
 
-        recentsView.getSplitSelectController().findLastActiveTaskAndRunCallback(
-                splitSelectSource.itemInfo.getComponentKey(),
-                foundTask -> {
+        recentsView.getSplitSelectController().findLastActiveTasksAndRunCallback(
+                Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
+                foundTasks -> {
+                    @Nullable Task foundTask = foundTasks.get(0);
                     splitSelectSource.alreadyRunningTaskId = foundTask == null
                             ? INVALID_TASK_ID
                             : foundTask.key.id;
@@ -221,9 +225,10 @@
      */
     public void triggerSecondAppForSplit(ItemInfoWithIcon info, Intent intent, View startingView) {
         RecentsView recents = getRecentsView();
-        recents.getSplitSelectController().findLastActiveTaskAndRunCallback(
-                info.getComponentKey(),
-                foundTask -> {
+        recents.getSplitSelectController().findLastActiveTasksAndRunCallback(
+                Collections.singletonList(info.getComponentKey()),
+                foundTasks -> {
+                    @Nullable Task foundTask = foundTasks.get(0);
                     if (foundTask != null) {
                         TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id);
                         // TODO (b/266482558): This additional null check is needed because there
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index bf3b932..fa5a1ae 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -18,6 +18,7 @@
 import static android.content.pm.PackageManager.FEATURE_PC;
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
 
 import android.content.Context;
@@ -87,7 +88,7 @@
     private @Nullable IconButtonView mAllAppsButton;
 
     // Only non-null when device supports having an All Apps button.
-    private @Nullable View mTaskbarDivider;
+    private @Nullable IconButtonView mTaskbarDivider;
 
     private View mQsb;
 
@@ -157,8 +158,12 @@
                     mActivityContext.getColor(R.color.all_apps_button_color));
 
             if (FeatureFlags.ENABLE_TASKBAR_PINNING.get()) {
-                mTaskbarDivider = LayoutInflater.from(context).inflate(R.layout.taskbar_divider,
+                mTaskbarDivider = (IconButtonView) LayoutInflater.from(context).inflate(
+                        R.layout.taskbar_divider,
                         this, false);
+                mTaskbarDivider.setIconDrawable(
+                        resources.getDrawable(R.drawable.taskbar_divider_button));
+                mTaskbarDivider.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
             }
         }
 
@@ -319,6 +324,9 @@
                 }
             }
             setClickAndLongClickListenersForIcon(hotseatView);
+            if (ENABLE_CURSOR_HOVER_STATES.get()) {
+                setHoverListenerForIcon(hotseatView);
+            }
             nextViewIndex++;
         }
         // Remove remaining views
@@ -366,16 +374,18 @@
         icon.setOnLongClickListener(mIconLongClickListener);
     }
 
+    /**
+     * Sets OnHoverListener for the given view.
+     */
+    private void setHoverListenerForIcon(View icon) {
+        icon.setOnHoverListener(mControllerCallbacks.getIconOnHoverListener(icon));
+    }
+
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         int count = getChildCount();
         DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
         int spaceNeeded = getIconLayoutWidth();
-        // We are removing the margin from taskbar divider item in taskbar,
-        // so remove it from spacing also.
-        if (FeatureFlags.ENABLE_TASKBAR_PINNING.get() && count > 1) {
-            spaceNeeded -= mIconTouchSize;
-        }
         int navSpaceNeeded = deviceProfile.hotseatBarEndOffset;
         boolean layoutRtl = isLayoutRtl();
         int centerAlignIconEnd = right - (right - left - spaceNeeded) / 2;
@@ -496,7 +506,15 @@
         if (deviceProfile.isQsbInline) {
             countExcludingQsb--;
         }
-        return countExcludingQsb * (mItemMarginLeftRight * 2 + mIconTouchSize);
+        int iconLayoutBoundsWidth =
+                countExcludingQsb * (mItemMarginLeftRight * 2 + mIconTouchSize);
+
+        if (FeatureFlags.ENABLE_TASKBAR_PINNING.get() && countExcludingQsb > 1) {
+            // We are removing 4 * mItemMarginLeftRight as there should be no space between
+            // All Apps icon, divider icon, and first app icon in taskbar
+            iconLayoutBoundsWidth -= mItemMarginLeftRight * 4;
+        }
+        return iconLayoutBoundsWidth;
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 4abd995..3d22e78 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.taskbar;
 
+import static com.android.app.animation.Interpolators.FINAL_FRAME;
+import static com.android.app.animation.Interpolators.LINEAR;
 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;
@@ -22,9 +24,8 @@
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.anim.AnimatedFloat.VALUE;
 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
-import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP;
+import static com.android.launcher3.taskbar.TaskbarManager.isPhoneButtonNavMode;
 import static com.android.launcher3.taskbar.TaskbarManager.isPhoneMode;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_ALIGNMENT_ANIM;
@@ -45,6 +46,7 @@
 import androidx.core.graphics.ColorUtils;
 import androidx.core.view.OneShotPreDrawListener;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
@@ -53,7 +55,6 @@
 import com.android.launcher3.anim.AlphaUpdateListener;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.RevealOutlineAnimation;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
@@ -133,7 +134,8 @@
         mTaskbarView = taskbarView;
         mTaskbarIconAlpha = new MultiValueAlpha(mTaskbarView, NUM_ALPHA_CHANNELS);
         mTaskbarIconAlpha.setUpdateVisibility(true);
-        mModelCallbacks = new TaskbarModelCallbacks(activity, mTaskbarView);
+        mModelCallbacks = TaskbarModelCallbacksFactory.newInstance(mActivity)
+                .create(mActivity, mTaskbarView);
         mTaskbarBottomMargin = activity.getDeviceProfile().taskbarBottomMargin;
         mStashedHandleHeight = activity.getResources()
                 .getDimensionPixelSize(R.dimen.taskbar_stashed_handle_height);
@@ -170,6 +172,13 @@
                 .getTaskbarNavButtonTranslationYForInAppDisplay();
 
         mActivity.addOnDeviceProfileChangeListener(mDeviceProfileChangeListener);
+
+        if (TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW) {
+            // This gets modified in NavbarButtonsViewController, but the initial value it reads
+            // may be incorrect since it's state gets destroyed on taskbar recreate, so reset here
+            mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN)
+                    .animateToValue(isPhoneButtonNavMode(mActivity) ? 0 : 1).start();
+        }
     }
 
     /**
@@ -443,8 +452,13 @@
      * Creates an animation for aligning the Taskbar icons with the provided Launcher device profile
      */
     private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) {
-        mOnControllerPreCreateCallback.run();
         PendingAnimation setter = new PendingAnimation(100);
+        if (TaskbarManager.isPhoneButtonNavMode(mActivity)) {
+            // No animation for icons in small-screen
+            return setter.createPlaybackController();
+        }
+
+        mOnControllerPreCreateCallback.run();
         DeviceProfile taskbarDp = mActivity.getDeviceProfile();
         Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(mActivity);
         float scaleUp = ((float) launcherDp.iconSizePx) / taskbarDp.taskbarIconSize;
@@ -676,6 +690,11 @@
                     .updateAndAnimateIsManuallyStashedInApp(true);
         }
 
+        /** Gets the hover listener for the provided icon view. */
+        public View.OnHoverListener getIconOnHoverListener(View icon) {
+            return new TaskbarHoverToolTipController(mActivity, mTaskbarView, icon);
+        }
+
         /**
          * Get the first chance to handle TaskbarView#onTouchEvent, and return whether we want to
          * consume the touch so TaskbarView treats it as an ACTION_CANCEL.
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java
index b4b83f6..5d91acd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java
@@ -47,9 +47,9 @@
     }
 
     @Override
-    protected View inflateSearchBox() {
+    protected View inflateSearchBar() {
         if (isSearchSupported()) {
-            return super.inflateSearchBox();
+            return super.inflateSearchBar();
         }
 
         // Remove top padding of header, since we do not have any search
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
index e0a502b..d786d94 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
@@ -15,11 +15,17 @@
  */
 package com.android.launcher3.taskbar.allapps;
 
+import static com.android.launcher3.model.data.AppInfo.EMPTY_ARRAY;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
 import com.android.launcher3.appprediction.PredictionRowView;
+import com.android.launcher3.dragndrop.DragOptions.PreDragCondition;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.taskbar.TaskbarControllers;
@@ -30,7 +36,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.function.Predicate;
-
 /**
  * Handles the all apps overlay window initialization, updates, and its data.
  * <p>
@@ -45,13 +50,16 @@
 public final class TaskbarAllAppsController {
 
     private TaskbarControllers mControllers;
+    private @Nullable TaskbarOverlayContext mOverlayContext;
     private @Nullable TaskbarAllAppsSlideInView mSlideInView;
     private @Nullable TaskbarAllAppsContainerView mAppsView;
+    private @Nullable TaskbarSearchSessionController mSearchSessionController;
 
     // Application data models.
-    private AppInfo[] mApps;
+    private @NonNull AppInfo[] mApps = EMPTY_ARRAY;
     private int mAppsModelFlags;
-    private List<ItemInfo> mPredictedApps;
+    private @NonNull List<ItemInfo> mPredictedApps = Collections.emptyList();
+    private @Nullable List<ItemInfo> mZeroStateSearchSuggestions;
     private boolean mDisallowGlobalDrag;
     private boolean mDisallowLongClick;
 
@@ -70,9 +78,14 @@
         }
     }
 
+    /** Clean up the controller. */
+    public void onDestroy() {
+        cleanUpOverlay();
+    }
+
     /** Updates the current {@link AppInfo} instances. */
-    public void setApps(AppInfo[] apps, int flags, Map<PackageUserKey, Integer> map) {
-        mApps = apps;
+    public void setApps(@Nullable AppInfo[] apps, int flags, Map<PackageUserKey, Integer> map) {
+        mApps = apps == null ? EMPTY_ARRAY : apps;
         mAppsModelFlags = flags;
         mPackageUserKeytoUidMap = map;
         if (mAppsView != null) {
@@ -96,6 +109,14 @@
                     .findFixedRowByType(PredictionRowView.class)
                     .setPredictedApps(mPredictedApps);
         }
+        if (mSearchSessionController != null) {
+            mSearchSessionController.setZeroStatePredictedItems(predictedApps);
+        }
+    }
+
+    /** Updates the current search suggestions. */
+    public void setZeroStateSearchSuggestions(List<ItemInfo> zeroStateSearchSuggestions) {
+        mZeroStateSearchSuggestions = zeroStateSearchSuggestions;
     }
 
     /** Updates the current notification dots. */
@@ -127,20 +148,28 @@
         // to catch invalid states.
         mControllers.getSharedState().allAppsVisible = true;
 
-        TaskbarOverlayContext overlayContext =
-                mControllers.taskbarOverlayController.requestWindow();
-        mSlideInView = (TaskbarAllAppsSlideInView) overlayContext.getLayoutInflater().inflate(
-                R.layout.taskbar_all_apps, overlayContext.getDragLayer(), false);
+        mOverlayContext = mControllers.taskbarOverlayController.requestWindow();
+
+        // Initialize search session for All Apps.
+        mSearchSessionController = TaskbarSearchSessionController.newInstance(mOverlayContext);
+        mOverlayContext.setSearchSessionController(mSearchSessionController);
+        mSearchSessionController.setZeroStatePredictedItems(mPredictedApps);
+        if (mZeroStateSearchSuggestions != null) {
+            mSearchSessionController.setZeroStateSearchSuggestions(mZeroStateSearchSuggestions);
+        }
+        mSearchSessionController.startLifecycle();
+
+        mSlideInView = (TaskbarAllAppsSlideInView) mOverlayContext.getLayoutInflater().inflate(
+                R.layout.taskbar_all_apps_sheet, mOverlayContext.getDragLayer(), false);
         mSlideInView.addOnCloseListener(() -> {
             mControllers.getSharedState().allAppsVisible = false;
-            mSlideInView = null;
-            mAppsView = null;
+            cleanUpOverlay();
         });
         TaskbarAllAppsViewController viewController = new TaskbarAllAppsViewController(
-                overlayContext, mSlideInView, mControllers);
+                mOverlayContext, mSlideInView, mControllers, mSearchSessionController);
 
         viewController.show(animate);
-        mAppsView = overlayContext.getAppsView();
+        mAppsView = mOverlayContext.getAppsView();
         mAppsView.getAppsStore().setApps(mApps, mAppsModelFlags, mPackageUserKeytoUidMap);
         mAppsView.getFloatingHeaderView()
                 .findFixedRowByType(PredictionRowView.class)
@@ -149,8 +178,28 @@
         // Create a shared drag layer between taskbar and taskbarAllApps so that when dragging
         // starts and taskbarAllApps can close, but the drag layer that the view is being dragged in
         // doesn't also close
-        overlayContext.getDragController().setDisallowGlobalDrag(mDisallowGlobalDrag);
-        overlayContext.getDragController().setDisallowLongClick(mDisallowLongClick);
+        mOverlayContext.getDragController().setDisallowGlobalDrag(mDisallowGlobalDrag);
+        mOverlayContext.getDragController().setDisallowLongClick(mDisallowLongClick);
+    }
+
+    private void cleanUpOverlay() {
+        // Floating search bar is added to the drag layer in ActivityAllAppsContainerView onAttach;
+        // removed here as this is a special case that we remove the all apps panel.
+        if (mAppsView != null && mOverlayContext != null
+                && mAppsView.getSearchUiDelegate().isSearchBarFloating()) {
+            mOverlayContext.getDragLayer().removeView(mAppsView.getSearchView());
+            mAppsView.getSearchUiDelegate().onDestroySearchBar();
+        }
+        if (mSearchSessionController != null) {
+            mSearchSessionController.onDestroy();
+            mSearchSessionController = null;
+        }
+        if (mOverlayContext != null) {
+            mOverlayContext.setSearchSessionController(null);
+            mOverlayContext = null;
+        }
+        mSlideInView = null;
+        mAppsView = null;
     }
 
     @VisibleForTesting
@@ -164,4 +213,12 @@
         // Allow null-pointer since this should only be null if the apps view is not showing.
         return mAppsView.getActiveRecyclerView().computeVerticalScrollOffset();
     }
+
+    /** @see TaskbarSearchSessionController#createPreDragConditionForSearch(View) */
+    @Nullable
+    public PreDragCondition createPreDragConditionForSearch(View view) {
+        return mSearchSessionController != null
+                ? mSearchSessionController.createPreDragConditionForSearch(view)
+                : null;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
index cfa1027..001c3bc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
@@ -15,20 +15,27 @@
  */
 package com.android.launcher3.taskbar.allapps;
 
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED;
+import static com.android.app.animation.Interpolators.EMPHASIZED;
 
-import android.animation.PropertyValuesHolder;
+import android.animation.Animator;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
+import android.view.View;
 import android.view.animation.Interpolator;
 import android.window.OnBackInvokedDispatcher;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.taskbar.allapps.TaskbarAllAppsViewController.TaskbarAllAppsCallbacks;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
@@ -37,8 +44,11 @@
 /** Wrapper for taskbar all apps with slide-in behavior. */
 public class TaskbarAllAppsSlideInView extends AbstractSlideInView<TaskbarOverlayContext>
         implements Insettable, DeviceProfile.OnDeviceProfileChangeListener {
+    private final Handler mHandler;
+
     private TaskbarAllAppsContainerView mAppsView;
     private float mShiftRange;
+    private @Nullable Runnable mShowOnFullyAttachedToWindowRunnable;
 
     // Initialized in init.
     private TaskbarAllAppsCallbacks mAllAppsCallbacks;
@@ -50,6 +60,7 @@
     public TaskbarAllAppsSlideInView(Context context, AttributeSet attrs,
             int defStyleAttr) {
         super(context, attrs, defStyleAttr);
+        mHandler = new Handler(Looper.myLooper());
     }
 
     void init(TaskbarAllAppsCallbacks callbacks) {
@@ -58,20 +69,51 @@
 
     /** Opens the all apps view. */
     void show(boolean animate) {
-        if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+        if (mIsOpen || mOpenCloseAnimation.getAnimationPlayer().isRunning()) {
             return;
         }
         mIsOpen = true;
-        attachToContainer();
 
-        if (animate) {
-            mOpenCloseAnimator.setValues(
-                    PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-            mOpenCloseAnimator.setInterpolator(EMPHASIZED);
-            mOpenCloseAnimator.setDuration(mAllAppsCallbacks.getOpenDuration()).start();
-        } else {
+        addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
+            @Override
+            public void onViewAttachedToWindow(View v) {
+                removeOnAttachStateChangeListener(this);
+                // Wait for view and its descendants to be fully attached before starting open.
+                mShowOnFullyAttachedToWindowRunnable = () -> showOnFullyAttachedToWindow(animate);
+                mHandler.post(mShowOnFullyAttachedToWindowRunnable);
+            }
+
+            @Override
+            public void onViewDetachedFromWindow(View v) {
+                removeOnAttachStateChangeListener(this);
+            }
+        });
+        attachToContainer();
+    }
+
+    private void showOnFullyAttachedToWindow(boolean animate) {
+        mAllAppsCallbacks.onAllAppsTransitionStart(true);
+        if (!animate) {
+            mAllAppsCallbacks.onAllAppsTransitionEnd(true);
             mTranslationShift = TRANSLATION_SHIFT_OPENED;
+            return;
         }
+
+        setUpOpenAnimation(mAllAppsCallbacks.getOpenDuration());
+        Animator animator = mOpenCloseAnimation.getAnimationPlayer();
+        animator.setInterpolator(EMPHASIZED);
+        animator.addListener(AnimatorListeners.forEndCallback(() -> {
+            if (mIsOpen) {
+                mAllAppsCallbacks.onAllAppsTransitionEnd(true);
+            }
+        }));
+        animator.start();
+    }
+
+    @Override
+    protected void onOpenCloseAnimationPending(PendingAnimation animation) {
+        mAllAppsCallbacks.onAllAppsAnimationPending(
+                animation, mToTranslationShift == TRANSLATION_SHIFT_OPENED);
     }
 
     /** The apps container inside this view. */
@@ -81,10 +123,23 @@
 
     @Override
     protected void handleClose(boolean animate) {
+        if (mShowOnFullyAttachedToWindowRunnable != null) {
+            mHandler.removeCallbacks(mShowOnFullyAttachedToWindowRunnable);
+            mShowOnFullyAttachedToWindowRunnable = null;
+        }
+        if (mIsOpen) {
+            mAllAppsCallbacks.onAllAppsTransitionStart(false);
+        }
         handleClose(animate, mAllAppsCallbacks.getCloseDuration());
     }
 
     @Override
+    protected void onCloseComplete() {
+        mAllAppsCallbacks.onAllAppsTransitionEnd(false);
+        super.onCloseComplete();
+    }
+
+    @Override
     protected Interpolator getIdleInterpolator() {
         return EMPHASIZED;
     }
@@ -194,4 +249,11 @@
     protected boolean isEventOverContent(MotionEvent ev) {
         return getPopupContainer().isEventOverView(mAppsView.getVisibleContainerView(), ev);
     }
+
+    @Override
+    public void onBackInvoked() {
+        if (!mAllAppsCallbacks.handleSearchBackInvoked()) {
+            super.onBackInvoked();
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
index 01342af..85633e9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
@@ -19,8 +19,9 @@
 import static com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT;
 
 import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.allapps.AllAppsTransitionListener;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.appprediction.AppsDividerView;
-import com.android.launcher3.appprediction.PredictionRowView;
 import com.android.launcher3.taskbar.NavbarButtonsViewController;
 import com.android.launcher3.taskbar.TaskbarControllers;
 import com.android.launcher3.taskbar.TaskbarStashController;
@@ -44,7 +45,8 @@
     TaskbarAllAppsViewController(
             TaskbarOverlayContext context,
             TaskbarAllAppsSlideInView slideInView,
-            TaskbarControllers taskbarControllers) {
+            TaskbarControllers taskbarControllers,
+            TaskbarSearchSessionController searchSessionController) {
 
         mContext = context;
         mSlideInView = slideInView;
@@ -53,8 +55,7 @@
         mNavbarButtonsViewController = taskbarControllers.navbarButtonsViewController;
         mOverlayController = taskbarControllers.taskbarOverlayController;
 
-        mSlideInView.init(new TaskbarAllAppsCallbacks());
-        setUpIconLongClick();
+        mSlideInView.init(new TaskbarAllAppsCallbacks(searchSessionController));
         setUpAppDivider();
         setUpTaskbarStashing();
     }
@@ -69,15 +70,6 @@
         mSlideInView.close(animate);
     }
 
-    private void setUpIconLongClick() {
-        mAppsView.setOnIconLongClickListener(
-                mContext.getDragController()::startDragOnLongClick);
-        mAppsView.getFloatingHeaderView()
-                .findFixedRowByType(PredictionRowView.class)
-                .setOnIconLongClickListener(
-                        mContext.getDragController()::startDragOnLongClick);
-    }
-
     private void setUpAppDivider() {
         mAppsView.getFloatingHeaderView()
                 .findFixedRowByType(AppsDividerView.class)
@@ -105,7 +97,13 @@
         });
     }
 
-    class TaskbarAllAppsCallbacks {
+    class TaskbarAllAppsCallbacks implements AllAppsTransitionListener {
+        private final TaskbarSearchSessionController mSearchSessionController;
+
+        private TaskbarAllAppsCallbacks(TaskbarSearchSessionController searchSessionController) {
+            mSearchSessionController = searchSessionController;
+        }
+
         int getOpenDuration() {
             return mOverlayController.getOpenDuration();
         }
@@ -113,5 +111,24 @@
         int getCloseDuration() {
             return mOverlayController.getCloseDuration();
         }
+
+        @Override
+        public void onAllAppsTransitionStart(boolean toAllApps) {
+            mSearchSessionController.onAllAppsTransitionStart(toAllApps);
+        }
+
+        @Override
+        public void onAllAppsTransitionEnd(boolean toAllApps) {
+            mSearchSessionController.onAllAppsTransitionEnd(toAllApps);
+        }
+
+        /** Invoked on back press, returning {@code true} if the search session handled it. */
+        boolean handleSearchBackInvoked() {
+            return mSearchSessionController.handleBackInvoked();
+        }
+
+        void onAllAppsAnimationPending(PendingAnimation animation, boolean toAllApps) {
+            mSearchSessionController.onAllAppsAnimationPending(animation, toAllApps);
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt
new file mode 100644
index 0000000..8a2041f
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.allapps
+
+import android.content.Context
+import android.view.View
+import com.android.launcher3.R
+import com.android.launcher3.allapps.AllAppsTransitionListener
+import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.dragndrop.DragOptions.PreDragCondition
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.util.ResourceBasedOverride
+import com.android.launcher3.util.ResourceBasedOverride.Overrides
+
+/** Stub for managing the Taskbar search session. */
+open class TaskbarSearchSessionController : ResourceBasedOverride, AllAppsTransitionListener {
+
+    /** Start the search session lifecycle. */
+    open fun startLifecycle() = Unit
+
+    /** Destroy the search session. */
+    open fun onDestroy() = Unit
+
+    /** Updates the predicted items shown in the zero-state. */
+    open fun setZeroStatePredictedItems(items: List<ItemInfo>) = Unit
+
+    /** Updates the search suggestions shown in the zero-state. */
+    open fun setZeroStateSearchSuggestions(items: List<ItemInfo>) = Unit
+
+    override fun onAllAppsTransitionStart(toAllApps: Boolean) = Unit
+
+    override fun onAllAppsTransitionEnd(toAllApps: Boolean) = Unit
+
+    /** Creates a [PreDragCondition] for [view], if it is a search result that requires one. */
+    open fun createPreDragConditionForSearch(view: View): PreDragCondition? = null
+
+    open fun handleBackInvoked(): Boolean = false
+
+    open fun onAllAppsAnimationPending(animation: PendingAnimation, toAllApps: Boolean) = Unit
+
+    companion object {
+        @JvmStatic
+        fun newInstance(context: Context): TaskbarSearchSessionController {
+            if (!FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) {
+                return TaskbarSearchSessionController()
+            }
+
+            return Overrides.getObject(
+                TaskbarSearchSessionController::class.java,
+                context,
+                R.string.taskbar_search_session_controller_class,
+            )
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
index 7397159..1e3f4f1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
@@ -21,10 +21,10 @@
 import android.graphics.Paint
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.ShapeDrawable
+import com.android.app.animation.Interpolators
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
 import com.android.launcher3.Utilities.mapToRange
-import com.android.launcher3.anim.Interpolators
 import com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound
 import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.wm.shell.common.TriangleShape
@@ -48,6 +48,8 @@
     private var showingArrow: Boolean = false
     private var arrowDrawable: ShapeDrawable
 
+    var width: Float = 0f
+
     init {
         paint.color = context.getColor(R.color.taskbar_background)
         paint.flags = Paint.ANTI_ALIAS_FLAG
@@ -59,8 +61,11 @@
         pointerSize = res.getDimension(R.dimen.bubblebar_pointer_size)
 
         shadowAlpha =
-            if (Utilities.isDarkTheme(context)) DARK_THEME_SHADOW_ALPHA
-            else LIGHT_THEME_SHADOW_ALPHA
+            if (Utilities.isDarkTheme(context)) {
+                DARK_THEME_SHADOW_ALPHA
+            } else {
+                LIGHT_THEME_SHADOW_ALPHA
+            }
 
         arrowDrawable =
             ShapeDrawable(TriangleShape.create(pointerSize, pointerSize, /* pointUp= */ true))
@@ -102,7 +107,7 @@
         // Draw background.
         val radius = backgroundHeight / 2f
         canvas.drawRoundRect(
-            0f,
+            canvas.width.toFloat() - width,
             0f,
             canvas.width.toFloat(),
             canvas.height.toFloat(),
@@ -132,4 +137,8 @@
     override fun setColorFilter(colorFilter: ColorFilter?) {
         paint.colorFilter = colorFilter
     }
+
+    fun setArrowAlpha(alpha: Int) {
+        arrowDrawable.paint.alpha = alpha
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 6d19692..24db380 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -31,18 +31,25 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
 
+import static java.lang.Math.abs;
+
 import android.annotation.BinderThread;
 import android.annotation.Nullable;
+import android.app.Notification;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
+import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.Matrix;
 import android.graphics.Path;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
 import android.os.Bundle;
 import android.os.SystemProperties;
 import android.os.UserHandle;
@@ -51,6 +58,8 @@
 import android.util.PathParser;
 import android.view.LayoutInflater;
 
+import androidx.appcompat.content.res.AppCompatResources;
+
 import com.android.internal.graphics.ColorUtils;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.BitmapInfo;
@@ -112,8 +121,10 @@
     private final Executor mMainExecutor;
     private final LauncherApps mLauncherApps;
     private final BubbleIconFactory mIconFactory;
+    private final SystemUiProxy mSystemUiProxy;
 
-    private BubbleBarBubble mSelectedBubble;
+    private BubbleBarItem mSelectedBubble;
+    private BubbleBarOverflow mOverflowBubble;
 
     private BubbleBarViewController mBubbleBarViewController;
     private BubbleStashController mBubbleStashController;
@@ -152,8 +163,10 @@
         mContext = context;
         mBarView = bubbleView; // Need the view for inflating bubble views.
 
+        mSystemUiProxy = SystemUiProxy.INSTANCE.get(context);
+
         if (BUBBLE_BAR_ENABLED) {
-            SystemUiProxy.INSTANCE.get(context).setBubblesListener(this);
+            mSystemUiProxy.setBubblesListener(this);
         }
         mMainExecutor = MAIN_EXECUTOR;
         mLauncherApps = context.getSystemService(LauncherApps.class);
@@ -166,7 +179,7 @@
     }
 
     public void onDestroy() {
-        SystemUiProxy.INSTANCE.get(mContext).setBubblesListener(null);
+        mSystemUiProxy.setBubblesListener(null);
     }
 
     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
@@ -177,10 +190,32 @@
         bubbleControllers.runAfterInit(() -> {
             mBubbleBarViewController.setHiddenForBubbles(!BUBBLE_BAR_ENABLED);
             mBubbleStashedHandleViewController.setHiddenForBubbles(!BUBBLE_BAR_ENABLED);
+            mBubbleBarViewController.setUpdateSelectedBubbleAfterCollapse(
+                    key -> setSelectedBubble(mBubbles.get(key)));
         });
     }
 
     /**
+     * Creates and adds the overflow bubble to the bubble bar if it hasn't been created yet.
+     *
+     * <p>This should be called on the {@link #BUBBLE_STATE_EXECUTOR} executor to avoid inflating
+     * the overflow multiple times.
+     */
+    private void createAndAddOverflowIfNeeded() {
+        if (mOverflowBubble == null) {
+            BubbleBarOverflow overflow = createOverflow(mContext);
+            mMainExecutor.execute(() -> {
+                // we're on the main executor now, so check that the overflow hasn't been created
+                // again to avoid races.
+                if (mOverflowBubble == null) {
+                    mBubbleBarViewController.addBubble(overflow);
+                    mOverflowBubble = overflow;
+                }
+            });
+        }
+    }
+
+    /**
      * Updates the bubble bar, handle bar, and stash controllers based on sysui state flags.
      */
     public void updateStateForSysuiFlags(int flags) {
@@ -209,18 +244,23 @@
                 || !update.currentBubbleList.isEmpty()) {
             // We have bubbles to load
             BUBBLE_STATE_EXECUTOR.execute(() -> {
+                createAndAddOverflowIfNeeded();
                 if (update.addedBubble != null) {
-                    viewUpdate.addedBubble = populateBubble(update.addedBubble, mContext, mBarView);
+                    viewUpdate.addedBubble = populateBubble(mContext, update.addedBubble, mBarView,
+                            null /* existingBubble */);
                 }
                 if (update.updatedBubble != null) {
+                    BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey());
                     viewUpdate.updatedBubble =
-                            populateBubble(update.updatedBubble, mContext, mBarView);
+                            populateBubble(mContext, update.updatedBubble, mBarView,
+                                    existingBubble);
                 }
                 if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) {
                     List<BubbleBarBubble> currentBubbles = new ArrayList<>();
                     for (int i = 0; i < update.currentBubbleList.size(); i++) {
                         BubbleBarBubble b =
-                                populateBubble(update.currentBubbleList.get(i), mContext, mBarView);
+                                populateBubble(mContext, update.currentBubbleList.get(i), mBarView,
+                                        null /* existingBubble */);
                         currentBubbles.add(b);
                     }
                     viewUpdate.currentBubbles = currentBubbles;
@@ -237,6 +277,7 @@
     private void applyViewChanges(BubbleBarViewUpdate update) {
         final boolean isCollapsed = (update.expandedChanged && !update.expanded)
                 || (!update.expandedChanged && !mBubbleBarViewController.isExpanded());
+        BubbleBarItem previouslySelectedBubble = mSelectedBubble;
         BubbleBarBubble bubbleToSelect = null;
         if (!update.removedBubbles.isEmpty()) {
             for (int i = 0; i < update.removedBubbles.size(); i++) {
@@ -281,10 +322,17 @@
         mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty());
         mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty());
 
+        if (mBubbles.isEmpty()) {
+            // all bubbles were removed. clear the selected bubble
+            mSelectedBubble = null;
+        }
+
         if (update.updatedBubble != null) {
-            // TODO: (b/269670235) handle updates:
-            //  (1) if content / icons change -- requires reload & add back in place
-            //  (2) if showing update dot changes -- tell the view to hide / show the dot
+            // Updates mean the dot state may have changed; any other changes were updated in
+            // the populateBubble step.
+            BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey());
+            // If we're not stashed, we're visible so animate
+            bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */);
         }
         if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) {
             // Create the new list
@@ -301,8 +349,8 @@
             // TODO: (b/273316505) handle suppression
         }
         if (update.selectedBubbleKey != null) {
-            if (mSelectedBubble != null
-                    && !update.selectedBubbleKey.equals(mSelectedBubble.getKey())) {
+            if (mSelectedBubble == null
+                    || !update.selectedBubbleKey.equals(mSelectedBubble.getKey())) {
                 BubbleBarBubble newlySelected = mBubbles.get(update.selectedBubbleKey);
                 if (newlySelected != null) {
                     bubbleToSelect = newlySelected;
@@ -314,7 +362,11 @@
         }
         if (bubbleToSelect != null) {
             setSelectedBubble(bubbleToSelect);
+            if (previouslySelectedBubble == null) {
+                mBubbleStashController.animateToInitialState(update.expanded);
+            }
         }
+
         if (update.expandedChanged) {
             if (update.expanded != mBubbleBarViewController.isExpanded()) {
                 mBubbleBarViewController.setExpandedFromSysui(update.expanded);
@@ -324,12 +376,38 @@
         }
     }
 
+    /** Tells WMShell to show the currently selected bubble. */
+    public void showSelectedBubble() {
+        if (getSelectedBubbleKey() != null) {
+            if (mSelectedBubble instanceof BubbleBarBubble) {
+                // Because we've visited this bubble, we should suppress the notification.
+                // This is updated on WMShell side when we show the bubble, but that update isn't
+                // passed to launcher, instead we apply it directly here.
+                BubbleInfo info = ((BubbleBarBubble) mSelectedBubble).getInfo();
+                info.setFlags(
+                        info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
+                mSelectedBubble.getView().updateDotVisibility(true /* animate */);
+            }
+            mSystemUiProxy.showBubble(getSelectedBubbleKey(),
+                    getBubbleBarOffsetX(), getBubbleBarOffsetY());
+        } else {
+            Log.w(TAG, "Trying to show the selected bubble but it's null");
+        }
+    }
+
+    /** Updates the currently selected bubble for launcher views and tells WMShell to show it. */
+    public void showAndSelectBubble(BubbleBarItem b) {
+        if (DEBUG) Log.w(TAG, "showingSelectedBubble: " + b.getKey());
+        setSelectedBubble(b);
+        showSelectedBubble();
+    }
+
     /**
      * Sets the bubble that should be selected. This notifies the views, it does not notify
-     * WMShell that the selection has changed, that should go through
-     * {@link SystemUiProxy#showBubble}.
+     * WMShell that the selection has changed, that should go through either
+     * {@link #showSelectedBubble()} or {@link #showAndSelectBubble(BubbleBarItem)}.
      */
-    public void setSelectedBubble(BubbleBarBubble b) {
+    private void setSelectedBubble(BubbleBarItem b) {
         if (!Objects.equals(b, mSelectedBubble)) {
             if (DEBUG) Log.w(TAG, "selectingBubble: " + b.getKey());
             mSelectedBubble = b;
@@ -353,7 +431,8 @@
     //
 
     @Nullable
-    private BubbleBarBubble populateBubble(BubbleInfo b, Context context, BubbleBarView bbv) {
+    private BubbleBarBubble populateBubble(Context context, BubbleInfo b, BubbleBarView bbv,
+            @Nullable BubbleBarBubble existingBubble) {
         String appName;
         Bitmap badgeBitmap;
         Bitmap bubbleBitmap;
@@ -422,16 +501,68 @@
         iconPath.transform(matrix);
         dotPath = iconPath;
         dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
-                Color.WHITE, WHITE_SCRIM_ALPHA);
+                Color.WHITE, WHITE_SCRIM_ALPHA / 255f);
 
+        if (existingBubble == null) {
+            LayoutInflater inflater = LayoutInflater.from(context);
+            BubbleView bubbleView = (BubbleView) inflater.inflate(
+                    R.layout.bubblebar_item_view, bbv, false /* attachToRoot */);
 
+            BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView,
+                    badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
+            bubbleView.setBubble(bubble);
+            return bubble;
+        } else {
+            // If we already have a bubble (so it already has an inflated view), update it.
+            existingBubble.setInfo(b);
+            existingBubble.setBadge(badgeBitmap);
+            existingBubble.setIcon(bubbleBitmap);
+            existingBubble.setDotColor(dotColor);
+            existingBubble.setDotPath(dotPath);
+            existingBubble.setAppName(appName);
+            return existingBubble;
+        }
+    }
+
+    private BubbleBarOverflow createOverflow(Context context) {
+        Bitmap bitmap = createOverflowBitmap(context);
         LayoutInflater inflater = LayoutInflater.from(context);
         BubbleView bubbleView = (BubbleView) inflater.inflate(
-                R.layout.bubblebar_item_view, bbv, false /* attachToRoot */);
+                R.layout.bubblebar_item_view, mBarView, false /* attachToRoot */);
+        BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView);
+        bubbleView.setOverflow(overflow, bitmap);
+        return overflow;
+    }
 
-        BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView,
-                badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
-        bubbleView.setBubble(bubble);
-        return bubble;
+    private Bitmap createOverflowBitmap(Context context) {
+        Drawable iconDrawable = AppCompatResources.getDrawable(mContext,
+                R.drawable.bubble_ic_overflow_button);
+
+        final TypedArray ta = mContext.obtainStyledAttributes(
+                new int[]{
+                        com.android.internal.R.attr.materialColorOnPrimaryFixed,
+                        com.android.internal.R.attr.materialColorPrimaryFixed
+                });
+        int overflowIconColor = ta.getColor(0, Color.WHITE);
+        int overflowBackgroundColor = ta.getColor(1, Color.BLACK);
+        ta.recycle();
+
+        iconDrawable.setTint(overflowIconColor);
+
+        int inset = context.getResources().getDimensionPixelSize(R.dimen.bubblebar_overflow_inset);
+        Drawable foreground = new InsetDrawable(iconDrawable, inset);
+        Drawable drawable = new AdaptiveIconDrawable(new ColorDrawable(overflowBackgroundColor),
+                foreground);
+
+        return mIconFactory.createBadgedIconBitmap(drawable).icon;
+    }
+
+    private int getBubbleBarOffsetY() {
+        final int translation = (int) abs(mBubbleStashController.getBubbleBarTranslationY());
+        return translation + mBarView.getHeight();
+    }
+
+    private int getBubbleBarOffsetX() {
+        return mBarView.getWidth() + mBarView.getHorizontalMargin();
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubble.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
similarity index 65%
rename from quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubble.kt
rename to quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
index 3cd5f75..43e21f4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubble.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
@@ -19,16 +19,19 @@
 import android.graphics.Path
 import com.android.wm.shell.common.bubbles.BubbleInfo
 
+/** An entity in the bubble bar. */
+sealed class BubbleBarItem(open var key: String, open var view: BubbleView)
+
 /** Contains state info about a bubble in the bubble bar as well as presentation information. */
 data class BubbleBarBubble(
-    val info: BubbleInfo,
-    val view: BubbleView,
-    val badge: Bitmap,
-    val icon: Bitmap,
-    val dotColor: Int,
-    val dotPath: Path,
-    val appName: String
-) {
+    var info: BubbleInfo,
+    override var view: BubbleView,
+    var badge: Bitmap,
+    var icon: Bitmap,
+    var dotColor: Int,
+    var dotPath: Path,
+    var appName: String
+) : BubbleBarItem(info.key, view)
 
-    val key: String = info.key
-}
+/** Represents the overflow bubble in the bubble bar. */
+data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem("Overflow", view)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 0e1e0e1..c482911 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.taskbar.bubbles;
 
+import android.animation.Animator;
 import android.animation.ValueAnimator;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -31,6 +32,7 @@
 import com.android.launcher3.views.ActivityContext;
 
 import java.util.List;
+import java.util.function.Consumer;
 
 /**
  * The view that holds all the bubble views. Modifying this view should happen through
@@ -66,11 +68,15 @@
     //  if it's smaller than 5.
     private static final int MAX_BUBBLES = 5;
     private static final int ARROW_POSITION_ANIMATION_DURATION_MS = 200;
+    private static final int WIDTH_ANIMATION_DURATION_MS = 200;
 
-    private final TaskbarActivityContext mActivityContext;
     private final BubbleBarBackground mBubbleBarBackground;
 
-    // The current bounds of all the bubble bar.
+    /**
+     * The current bounds of all the bubble bar. Note that these bounds may not account for
+     * translation. The bounds should be retrieved using {@link #getBubbleBarBounds()} which
+     * updates the bounds and accounts for translation.
+     */
     private final Rect mBubbleBarBounds = new Rect();
     // The amount the bubbles overlap when they are stacked in the bubble bar
     private final float mIconOverlapAmount;
@@ -89,6 +95,12 @@
     private View.OnClickListener mOnClickListener;
 
     private final Rect mTempRect = new Rect();
+    private float mRelativePivotX = 1f;
+    private float mRelativePivotY = 1f;
+
+    // An animator that represents the expansion state of the bubble bar, where 0 corresponds to the
+    // collapsed state and 1 to the fully expanded state.
+    private final ValueAnimator mWidthAnimator = ValueAnimator.ofFloat(0, 1);
 
     // We don't reorder the bubbles when they are expanded as it could be jarring for the user
     // this runnable will be populated with any reordering of the bubbles that should be applied
@@ -96,6 +108,12 @@
     @Nullable
     private Runnable mReorderRunnable;
 
+    @Nullable
+    private Consumer<String> mUpdateSelectedBubbleAfterCollapse;
+
+    @Nullable
+    private BubbleView mDraggedBubbleView;
+
     public BubbleBarView(Context context) {
         this(context, null);
     }
@@ -110,7 +128,7 @@
 
     public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        mActivityContext = ActivityContext.lookupContext(context);
+        TaskbarActivityContext activityContext = ActivityContext.lookupContext(context);
 
         mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap);
         mIconSpacing = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_spacing);
@@ -118,9 +136,46 @@
         mBubbleElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_elevation);
         setClipToPadding(false);
 
-        mBubbleBarBackground = new BubbleBarBackground(mActivityContext,
+        mBubbleBarBackground = new BubbleBarBackground(activityContext,
                 getResources().getDimensionPixelSize(R.dimen.bubblebar_size));
         setBackgroundDrawable(mBubbleBarBackground);
+
+        mWidthAnimator.setDuration(WIDTH_ANIMATION_DURATION_MS);
+        mWidthAnimator.addUpdateListener(animation -> {
+            updateChildrenRenderNodeProperties();
+            invalidate();
+        });
+        mWidthAnimator.addListener(new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationCancel(Animator animation) {
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mBubbleBarBackground.showArrow(mIsBarExpanded);
+                if (!mIsBarExpanded && mReorderRunnable != null) {
+                    mReorderRunnable.run();
+                    mReorderRunnable = null;
+                }
+                // If the bar was just collapsed and the overflow was the last bubble that was
+                // selected, set the first bubble as selected.
+                if (!mIsBarExpanded && mUpdateSelectedBubbleAfterCollapse != null
+                        && mSelectedBubbleView.getBubble() instanceof BubbleBarOverflow) {
+                    BubbleView firstBubble = (BubbleView) getChildAt(0);
+                    mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey());
+                }
+                updateWidth();
+            }
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {
+            }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mBubbleBarBackground.showArrow(true);
+            }
+        });
     }
 
     @Override
@@ -131,63 +186,157 @@
         mBubbleBarBounds.right = right;
         mBubbleBarBounds.bottom = bottom;
 
-        // The bubble bar handle is aligned to the bottom edge of the screen so scale towards that.
-        setPivotX(getWidth());
-        setPivotY(getHeight());
+        // The bubble bar handle is aligned according to the relative pivot,
+        // by default it's aligned to the bottom edge of the screen so scale towards that
+        setPivotX(mRelativePivotX * getWidth());
+        setPivotY(mRelativePivotY * getHeight());
 
         // Position the views
         updateChildrenRenderNodeProperties();
     }
 
     /**
-     * Returns the bounds of the bubble bar.
+     * Updates the bounds with translation that may have been applied and returns the result.
      */
     public Rect getBubbleBarBounds() {
+        mBubbleBarBounds.top = getTop() + (int) getTranslationY();
+        mBubbleBarBounds.bottom = getBottom() + (int) getTranslationY();
         return mBubbleBarBounds;
     }
 
-    // TODO: (b/273592694) animate it
+    /**
+     * Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height
+     * respectively. If the value is not in range of 0 to 1 it will be normalized.
+     * @param x relative X pivot value in range 0..1
+     * @param y relative Y pivot value in range 0..1
+     */
+    public void setRelativePivot(float x, float y) {
+        mRelativePivotX = Float.max(Float.min(x, 1), 0);
+        mRelativePivotY = Float.max(Float.min(y, 1), 0);
+        requestLayout();
+    }
+
+    /**
+     * Get current relative pivot for X axis
+     */
+    public float getRelativePivotX() {
+        return mRelativePivotX;
+    }
+
+    /**
+     * Get current relative pivot for Y axis
+     */
+    public float getRelativePivotY() {
+        return mRelativePivotY;
+    }
+
+    // TODO: (b/280605790) animate it
     @Override
     public void addView(View child, int index, ViewGroup.LayoutParams params) {
         if (getChildCount() + 1 > MAX_BUBBLES) {
-            removeViewInLayout(getChildAt(getChildCount() - 1));
+            // the last child view is the overflow bubble and we shouldn't remove that. remove the
+            // second to last child view.
+            removeViewInLayout(getChildAt(getChildCount() - 2));
         }
         super.addView(child, index, params);
+        updateWidth();
+    }
+
+    // TODO: (b/283309949) animate it
+    @Override
+    public void removeView(View view) {
+        super.removeView(view);
+        updateWidth();
+    }
+
+    private void updateWidth() {
+        LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
+        lp.width = (int) (mIsBarExpanded ? expandedWidth() : collapsedWidth());
+        setLayoutParams(lp);
+    }
+
+    /** @return the horizontal margin between the bubble bar and the edge of the screen. */
+    int getHorizontalMargin() {
+        LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
+        return lp.getMarginEnd();
     }
 
     /**
      * Updates the z order, positions, and badge visibility of the bubble views in the bar based
      * on the expanded state.
      */
-    // TODO: (b/273592694) animate it
     private void updateChildrenRenderNodeProperties() {
+        final float widthState = (float) mWidthAnimator.getAnimatedValue();
+        final float currentWidth = getWidth();
+        final float expandedWidth = expandedWidth();
+        final float collapsedWidth = collapsedWidth();
         int bubbleCount = getChildCount();
         final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
+        final boolean animate = getVisibility() == VISIBLE;
         for (int i = 0; i < bubbleCount; i++) {
             BubbleView bv = (BubbleView) getChildAt(i);
             bv.setTranslationY(ty);
+
+            // the position of the bubble when the bar is fully expanded
+            final float expandedX = i * (mIconSize + mIconSpacing);
+            // the position of the bubble when the bar is fully collapsed
+            final float collapsedX = i == 0 ? 0 : mIconOverlapAmount;
+
             if (mIsBarExpanded) {
-                final float tx = i * (mIconSize + mIconSpacing);
-                bv.setTranslationX(tx);
-                bv.setZ(0);
-                bv.showBadge();
+                // where the bubble will end up when the animation ends
+                final float targetX = currentWidth - expandedWidth + expandedX;
+                bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
+                // if we're fully expanded, set the z level to 0 or to bubble elevation if dragged
+                if (widthState == 1f) {
+                    bv.setZ(bv == mDraggedBubbleView ? mBubbleElevation : 0);
+                }
+                // When we're expanded, we're not stacked so we're not behind the stack
+                bv.setBehindStack(false, animate);
+                bv.setAlpha(1);
             } else {
+                final float targetX = currentWidth - collapsedWidth + collapsedX;
+                bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
                 bv.setZ((MAX_BUBBLES * mBubbleElevation) - i);
-                bv.setTranslationX(i * mIconOverlapAmount);
-                if (i > 0) {
-                    bv.hideBadge();
-                } else {
-                    bv.showBadge();
+                // If we're not the first bubble we're behind the stack
+                bv.setBehindStack(i > 0, animate);
+                // If we're fully collapsed, hide all bubbles except for the first 2. If there are
+                // only 2 bubbles, hide the second bubble as well because it's the overflow.
+                if (widthState == 0) {
+                    if (i > 1) {
+                        bv.setAlpha(0);
+                    } else if (i == 1 && bubbleCount == 2) {
+                        bv.setAlpha(0);
+                    }
                 }
             }
         }
+
+        // update the arrow position
+        final float collapsedArrowPosition = arrowPositionForSelectedWhenCollapsed();
+        final float expandedArrowPosition = arrowPositionForSelectedWhenExpanded();
+        final float interpolatedWidth =
+                widthState * (expandedWidth - collapsedWidth) + collapsedWidth;
+        if (mIsBarExpanded) {
+            // when the bar is expanding, the selected bubble is always the first, so the arrow
+            // always shifts with the interpolated width.
+            final float arrowPosition = currentWidth - interpolatedWidth + collapsedArrowPosition;
+            mBubbleBarBackground.setArrowPosition(arrowPosition);
+        } else {
+            final float targetPosition = currentWidth - collapsedWidth + collapsedArrowPosition;
+            final float arrowPosition =
+                    targetPosition + widthState * (expandedArrowPosition - targetPosition);
+            mBubbleBarBackground.setArrowPosition(arrowPosition);
+        }
+
+        mBubbleBarBackground.setArrowAlpha((int) (255 * widthState));
+        mBubbleBarBackground.setWidth(interpolatedWidth);
     }
 
     /**
      * Reorders the views to match the provided list.
      */
     public void reorder(List<BubbleView> viewOrder) {
-        if (isExpanded()) {
+        if (isExpanded() || mWidthAnimator.isRunning()) {
             mReorderRunnable = () -> doReorder(viewOrder);
         } else {
             doReorder(viewOrder);
@@ -199,7 +348,10 @@
         if (!isExpanded()) {
             for (int i = 0; i < viewOrder.size(); i++) {
                 View child = viewOrder.get(i);
-                if (child != null) {
+                // this child view may have already been removed so verify that it still exists
+                // before reordering it, otherwise it will be re-added.
+                int indexOfChild = indexOfChild(child);
+                if (child != null && indexOfChild >= 0) {
                     removeViewInLayout(child);
                     addViewInLayout(child, i, child.getLayoutParams());
                 }
@@ -208,6 +360,11 @@
         }
     }
 
+    public void setUpdateSelectedBubbleAfterCollapse(
+            Consumer<String> updateSelectedBubbleAfterCollapse) {
+        mUpdateSelectedBubbleAfterCollapse = updateSelectedBubbleAfterCollapse;
+    }
+
     /**
      * Sets which bubble view should be shown as selected.
      */
@@ -217,6 +374,14 @@
     }
 
     /**
+     * Sets the dragged bubble view to correctly apply Z order. Dragged view should appear on top
+     */
+    public void setDraggedBubble(@Nullable BubbleView view) {
+        mDraggedBubbleView = view;
+        requestLayout();
+    }
+
+    /**
      * Update the arrow position to match the selected bubble.
      *
      * @param shouldAnimate whether or not to animate the arrow. If the bar was just expanded, this
@@ -247,6 +412,16 @@
         }
     }
 
+    private float arrowPositionForSelectedWhenExpanded() {
+        final int index = indexOfChild(mSelectedBubbleView);
+        return getPaddingStart() + index * (mIconSize + mIconSpacing) + mIconSize / 2f;
+    }
+
+    private float arrowPositionForSelectedWhenCollapsed() {
+        final int index = indexOfChild(mSelectedBubbleView);
+        return getPaddingStart() + index * (mIconOverlapAmount) + mIconSize / 2f;
+    }
+
     @Override
     public void setOnClickListener(View.OnClickListener listener) {
         mOnClickListener = listener;
@@ -264,18 +439,16 @@
     /**
      * Sets whether the bubble bar is expanded or collapsed.
      */
-    // TODO: (b/273592694) animate it
     public void setExpanded(boolean isBarExpanded) {
         if (mIsBarExpanded != isBarExpanded) {
             mIsBarExpanded = isBarExpanded;
             updateArrowForSelected(/* shouldAnimate= */ false);
             setOrUnsetClickListener();
-            if (!isBarExpanded && mReorderRunnable != null) {
-                mReorderRunnable.run();
-                mReorderRunnable = null;
+            if (isBarExpanded) {
+                mWidthAnimator.start();
+            } else {
+                mWidthAnimator.reverse();
             }
-            mBubbleBarBackground.showArrow(mIsBarExpanded);
-            requestLayout(); // trigger layout to reposition views & update size for expansion
         }
     }
 
@@ -286,19 +459,20 @@
         return mIsBarExpanded;
     }
 
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+    private float expandedWidth() {
         final int childCount = getChildCount();
-        final float iconWidth = mIsBarExpanded
-                ? (childCount * (mIconSize + mIconSpacing))
-                : mIconSize + ((childCount - 1) * mIconOverlapAmount);
-        final int totalWidth = (int) iconWidth + getPaddingStart() + getPaddingEnd();
-        setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec));
+        final int horizontalPadding = getPaddingStart() + getPaddingEnd();
+        return childCount * (mIconSize + mIconSpacing) + horizontalPadding;
+    }
 
-        for (int i = 0; i < childCount; i++) {
-            View child = getChildAt(i);
-            measureChild(child, (int) mIconSize, (int) mIconSize);
-        }
+    private float collapsedWidth() {
+        final int childCount = getChildCount();
+        final int horizontalPadding = getPaddingStart() + getPaddingEnd();
+        // If there are more than 2 bubbles, the first 2 should be visible when collapsed.
+        // Otherwise just the first bubble should be visible because we don't show the overflow.
+        return childCount > 2
+                ? mIconSize + mIconOverlapAmount + horizontalPadding
+                : mIconSize + horizontalPadding;
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 82494c6..20b8e3b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -24,16 +24,21 @@
 import android.view.View;
 import android.widget.FrameLayout;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.taskbar.TaskbarInsetsController;
+import com.android.launcher3.taskbar.TaskbarStashController;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.quickstep.SystemUiProxy;
 
 import java.util.List;
 import java.util.Objects;
+import java.util.function.Consumer;
 
 /**
  * Controller for {@link BubbleBarView}. Manages the visibility of the bubble bar as well as
@@ -51,6 +56,9 @@
     // Initialized in init.
     private BubbleStashController mBubbleStashController;
     private BubbleBarController mBubbleBarController;
+    private BubbleDragController mBubbleDragController;
+    private TaskbarStashController mTaskbarStashController;
+    private TaskbarInsetsController mTaskbarInsetsController;
     private View.OnClickListener mBubbleClickListener;
     private View.OnClickListener mBubbleBarClickListener;
 
@@ -80,6 +88,9 @@
     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleBarController = bubbleControllers.bubbleBarController;
+        mBubbleDragController = bubbleControllers.bubbleDragController;
+        mTaskbarStashController = controllers.taskbarStashController;
+        mTaskbarInsetsController = controllers.taskbarInsetsController;
 
         mActivity.addOnDeviceProfileChangeListener(dp ->
                 mBarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarHeight
@@ -88,12 +99,15 @@
         mBubbleBarScale.updateValue(1f);
         mBubbleClickListener = v -> onBubbleClicked(v);
         mBubbleBarClickListener = v -> setExpanded(true);
+        mBubbleDragController.setupBubbleBarView(mBarView);
         mBarView.setOnClickListener(mBubbleBarClickListener);
-        // TODO: when barView layout changes tell taskbarInsetsController the insets have changed.
+        mBarView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) ->
+                mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+        );
     }
 
     private void onBubbleClicked(View v) {
-        BubbleBarBubble bubble = ((BubbleView) v).getBubble();
+        BubbleBarItem bubble = ((BubbleView) v).getBubble();
         if (bubble == null) {
             Log.e(TAG, "bubble click listener, bubble was null");
         }
@@ -103,9 +117,7 @@
             setExpanded(false);
             mBubbleStashController.stashBubbleBar();
         } else {
-            mBubbleBarController.setSelectedBubble(bubble);
-            mSystemUiProxy.showBubble(bubble.getKey(),
-                    mBubbleStashController.isBubblesShowingOnHome());
+            mBubbleBarController.showAndSelectBubble(bubble);
         }
     }
 
@@ -133,6 +145,11 @@
         return mBarView.getVisibility() == VISIBLE;
     }
 
+    /** Whether the bubble bar has bubbles. */
+    public boolean hasBubbles() {
+        return mBubbleBarController.getSelectedBubbleKey() != null;
+    }
+
     /**
      * The bounds of the bubble bar.
      */
@@ -140,6 +157,11 @@
         return mBarView.getBubbleBarBounds();
     }
 
+    /** The horizontal margin of the bubble bar from the edge of the screen. */
+    public int getHorizontalMargin() {
+        return mBarView.getHorizontalMargin();
+    }
+
     /**
      * When the bubble bar is not stashed, it can be collapsed (the icons are in a stack) or
      * expanded (the icons are in a row). This indicates whether the bubble bar is expanded.
@@ -176,6 +198,12 @@
         }
     }
 
+    /** Sets a callback that updates the selected bubble after the bubble bar collapses. */
+    public void setUpdateSelectedBubbleAfterCollapse(
+            Consumer<String> updateSelectedBubbleAfterCollapse) {
+        mBarView.setUpdateSelectedBubbleAfterCollapse(updateSelectedBubbleAfterCollapse);
+    }
+
     /**
      * Sets whether the bubble bar should be hidden due to SysUI state (e.g. on lockscreen).
      */
@@ -188,10 +216,12 @@
 
     // TODO: (b/273592694) animate it
     private void updateVisibilityForStateChange() {
-        if (!mHiddenForSysui && !mBubbleStashController.isStashed() && !mHiddenForNoBubbles) {
+        if (!mHiddenForSysui && !mHiddenForNoBubbles) {
             mBarView.setVisibility(VISIBLE);
         } else {
             mBarView.setVisibility(INVISIBLE);
+            mBarView.setAlpha(0);
+            mBarView.setExpanded(false);
         }
     }
 
@@ -228,7 +258,7 @@
     /**
      * Removes the provided bubble from the bubble bar.
      */
-    public void removeBubble(BubbleBarBubble b) {
+    public void removeBubble(BubbleBarItem b) {
         if (b != null) {
             mBarView.removeView(b.getView());
         } else {
@@ -239,10 +269,11 @@
     /**
      * Adds the provided bubble to the bubble bar.
      */
-    public void addBubble(BubbleBarBubble b) {
+    public void addBubble(BubbleBarItem b) {
         if (b != null) {
             mBarView.addView(b.getView(), 0, new FrameLayout.LayoutParams(mIconSize, mIconSize));
             b.getView().setOnClickListener(mBubbleClickListener);
+            mBubbleDragController.setupBubbleView(b.getView());
         } else {
             Log.w(TAG, "addBubble, bubble was null!");
         }
@@ -260,7 +291,7 @@
     /**
      * Updates the selected bubble.
      */
-    public void updateSelectedBubble(BubbleBarBubble newlySelected) {
+    public void updateSelectedBubble(BubbleBarItem newlySelected) {
         mBarView.setSelectedBubble(newlySelected.getView());
     }
 
@@ -276,14 +307,9 @@
             if (!isExpanded) {
                 mSystemUiProxy.collapseBubbles();
             } else {
-                final String selectedKey = mBubbleBarController.getSelectedBubbleKey();
-                if (selectedKey != null) {
-                    mSystemUiProxy.showBubble(selectedKey,
-                            mBubbleStashController.isBubblesShowingOnHome());
-                } else {
-                    Log.w(TAG, "trying to expand bubbles when there isn't one selected");
-                }
-                // TODO: Tell taskbar stash controller to stash without bubbles following
+                mBubbleBarController.showSelectedBubble();
+                mTaskbarStashController.updateAndAnimateTransientTaskbar(true /* stash */,
+                        false /* shouldBubblesFollow */);
             }
         }
     }
@@ -299,4 +325,46 @@
             mBubbleStashController.showBubbleBar(true /* expand the bubbles */);
         }
     }
+
+    /**
+     * Updates the dragged bubble view in the bubble bar view, and notifies SystemUI
+     * that a bubble is being dragged to dismiss.
+     * @param bubbleView dragged bubble view
+     */
+    public void onDragStart(@NonNull BubbleView bubbleView) {
+        if (bubbleView.getBubble() == null) return;
+        mSystemUiProxy.onBubbleDrag(bubbleView.getBubble().getKey(), /* isBeingDragged = */ true);
+        mBarView.setDraggedBubble(bubbleView);
+    }
+
+    /**
+     * Notifies SystemUI to expand the selected bubble when the bubble is released.
+     * @param bubbleView dragged bubble view
+     */
+    public void onDragRelease(@NonNull BubbleView bubbleView) {
+        if (bubbleView.getBubble() == null) return;
+        mSystemUiProxy.onBubbleDrag(bubbleView.getBubble().getKey(), /* isBeingDragged = */ false);
+    }
+
+    /**
+     * Removes the dragged bubble view in the bubble bar view
+     */
+    public void onDragEnd() {
+        mBarView.setDraggedBubble(null);
+    }
+
+    /**
+     * Called when bubble was dragged into the dismiss target. Notifies System
+     * @param bubble dismissed bubble item
+     */
+    public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) {
+        mSystemUiProxy.removeBubble(bubble.getKey());
+    }
+
+    /**
+     * Called when bubble stack was dragged into the dismiss target
+     */
+    public void onDismissAllBubblesWhileDragging() {
+        mSystemUiProxy.removeAllBubbles();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index 6417f3c..c47427d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -27,6 +27,8 @@
     public final BubbleBarViewController bubbleBarViewController;
     public final BubbleStashController bubbleStashController;
     public final BubbleStashedHandleViewController bubbleStashedHandleViewController;
+    public final BubbleDragController bubbleDragController;
+    public final BubbleDismissController bubbleDismissController;
 
     private final RunnableList mPostInitRunnables = new RunnableList();
 
@@ -39,11 +41,15 @@
             BubbleBarController bubbleBarController,
             BubbleBarViewController bubbleBarViewController,
             BubbleStashController bubbleStashController,
-            BubbleStashedHandleViewController bubbleStashedHandleViewController) {
+            BubbleStashedHandleViewController bubbleStashedHandleViewController,
+            BubbleDragController bubbleDragController,
+            BubbleDismissController bubbleDismissController) {
         this.bubbleBarController = bubbleBarController;
         this.bubbleBarViewController = bubbleBarViewController;
         this.bubbleStashController = bubbleStashController;
         this.bubbleStashedHandleViewController = bubbleStashedHandleViewController;
+        this.bubbleDragController = bubbleDragController;
+        this.bubbleDismissController = bubbleDismissController;
     }
 
     /**
@@ -56,6 +62,8 @@
         bubbleBarViewController.init(taskbarControllers, this);
         bubbleStashedHandleViewController.init(taskbarControllers, this);
         bubbleStashController.init(taskbarControllers, this);
+        bubbleDragController.init(/* bubbleControllers = */ this);
+        bubbleDismissController.init(/* bubbleControllers = */ this);
 
         mPostInitRunnables.executeAllAndDestroy();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
new file mode 100644
index 0000000..41c3dec
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar.bubbles;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+
+import com.android.launcher3.R;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarDragLayer;
+import com.android.wm.shell.common.bubbles.DismissView;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+
+/**
+ * Controls dismiss view presentation for the bubble bar dismiss functionality.
+ * Provides the dragged view snapping to the target dismiss area and animates it.
+ * When the dragged bubble/bubble stack is released inside of the target area, it gets dismissed.
+ *
+ * @see BubbleDragController
+ */
+public class BubbleDismissController {
+    private static final String TAG = BubbleDismissController.class.getSimpleName();
+    private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f;
+    private final TaskbarActivityContext mActivity;
+    private final TaskbarDragLayer mDragLayer;
+    @Nullable
+    private BubbleBarViewController mBubbleBarViewController;
+
+    // Dismiss view that's attached to drag layer. It consists of the scrim view and the circular
+    // dismiss view used as a dismiss target.
+    @Nullable
+    private DismissView mDismissView;
+
+    // The currently magnetized object, which is being dragged and will be attracted to the magnetic
+    // dismiss target. This is either the stack itself, or an individual bubble.
+    @Nullable
+    private MagnetizedObject<View> mMagnetizedObject;
+
+    // The MagneticTarget instance for our circular dismiss view. This is added to the
+    // MagnetizedObject instances for the stack and any dragged-out bubbles.
+    @Nullable
+    private MagnetizedObject.MagneticTarget mMagneticTarget;
+
+    // The bubble drag animator that synchronizes bubble drag and dismiss view animations
+    // A new instance is provided when the dismiss view is setup
+    @Nullable
+    private BubbleDragAnimator mAnimator;
+
+    public BubbleDismissController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer) {
+        mActivity = activity;
+        mDragLayer = dragLayer;
+    }
+
+    /**
+     * Initializes dependencies when bubble controllers are created.
+     * Should be careful to only access things that were created in constructors for now, as some
+     * controllers may still be waiting for init().
+     */
+    public void init(@NonNull BubbleControllers bubbleControllers) {
+        mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
+    }
+
+    /**
+     * Setup the dismiss view and magnetized object that will be attracted to magnetic target.
+     * Should be called before handling events or showing/hiding dismiss view.
+     *
+     * @param magnetizedView the view to be pulled into target dismiss area
+     * @param animator       the bubble animator to be used for the magnetized view, it syncs bubble
+     *                       dragging and dismiss animations with the dismiss view provided.
+     */
+    public void setupDismissView(@NonNull View magnetizedView,
+            @NonNull BubbleDragAnimator animator) {
+        setupDismissView();
+        setupMagnetizedObject(magnetizedView);
+        if (mDismissView != null) {
+            animator.setDismissView(mDismissView);
+            mAnimator = animator;
+        }
+    }
+
+    /**
+     * Handle the touch event and pass it to the magnetized object.
+     * It should be called after {@code setupDismissView}
+     */
+    public boolean handleTouchEvent(@NonNull MotionEvent event) {
+        return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
+    }
+
+    /**
+     * Show dismiss view with animation
+     * It should be called after {@code setupDismissView}
+     */
+    public void showDismissView() {
+        if (mDismissView == null) return;
+        mDismissView.show();
+    }
+
+    /**
+     * Hide dismiss view with animation
+     * It should be called after {@code setupDismissView}
+     */
+    public void hideDismissView() {
+        if (mDismissView == null) return;
+        mDismissView.hide();
+    }
+
+    /**
+     * Dismiss magnetized object when it's released in the dismiss target area
+     */
+    private void dismissMagnetizedObject() {
+        if (mMagnetizedObject == null || mBubbleBarViewController == null) return;
+        if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) {
+            BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject();
+            if (bubbleView.getBubble() != null) {
+                mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble());
+            }
+        } else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) {
+            mBubbleBarViewController.onDismissAllBubblesWhileDragging();
+        }
+    }
+
+    private void setupDismissView() {
+        if (mDismissView != null) return;
+        mDismissView = new DismissView(mActivity.getApplicationContext());
+        BubbleDismissViewUtils.setup(mDismissView);
+        mDragLayer.addView(mDismissView, /* index = */ 0,
+                new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+        mDismissView.setElevation(mDismissView.getResources().getDimensionPixelSize(
+                R.dimen.bubblebar_elevation));
+        setupMagneticTarget(mDismissView.getCircle());
+    }
+
+    private void setupMagneticTarget(@NonNull View view) {
+        int magneticFieldRadius = mActivity.getResources().getDimensionPixelSize(
+                R.dimen.bubblebar_dismiss_target_size);
+        mMagneticTarget = new MagnetizedObject.MagneticTarget(view, magneticFieldRadius);
+    }
+
+    private void setupMagnetizedObject(@NonNull View magnetizedView) {
+        mMagnetizedObject = new MagnetizedObject<>(mActivity.getApplicationContext(),
+                magnetizedView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
+            @Override
+            public float getWidth(@NonNull View underlyingObject) {
+                return underlyingObject.getWidth() * underlyingObject.getScaleX();
+            }
+
+            @Override
+            public float getHeight(@NonNull View underlyingObject) {
+                return underlyingObject.getHeight() * underlyingObject.getScaleY();
+            }
+
+            @Override
+            public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) {
+                underlyingObject.getLocationOnScreen(loc);
+            }
+        };
+
+        mMagnetizedObject.setHapticsEnabled(true);
+        mMagnetizedObject.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+        if (mMagneticTarget != null) {
+            mMagnetizedObject.addTarget(mMagneticTarget);
+        } else {
+            Log.e(TAG,"Requires MagneticTarget to add target to MagnetizedObject!");
+        }
+        mMagnetizedObject.setMagnetListener(new MagnetizedObject.MagnetListener() {
+            @Override
+            public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+                if (mAnimator == null) return;
+                mAnimator.animateDismissCaptured();
+            }
+
+            @Override
+            public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
+                    float velX, float velY, boolean wasFlungOut) {
+                if (mAnimator == null) return;
+                mAnimator.animateDismissReleased();
+            }
+
+            @Override
+            public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+                dismissMagnetizedObject();
+            }
+        });
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
new file mode 100644
index 0000000..4b235a9
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:JvmName("BubbleDismissViewUtils")
+
+package com.android.launcher3.taskbar.bubbles
+
+import com.android.launcher3.R
+import com.android.wm.shell.common.bubbles.DismissView
+
+/**
+ * Dismiss view is shared from WMShell. It requires setup with local resources.
+ *
+ * Usage:
+ * - Kotlin `dismissView.setup()`
+ * - Java `BubbleDismissViewUtils.setup(dismissView)`
+ */
+fun DismissView.setup() {
+    setup(
+        DismissView.Config(
+            targetSizeResId = R.dimen.bubblebar_dismiss_target_size,
+            iconSizeResId = R.dimen.bubblebar_dismiss_target_icon_size,
+            bottomMarginResId = R.dimen.bubblebar_dismiss_target_bottom_margin,
+            floatingGradientHeightResId = R.dimen.bubblebar_dismiss_floating_gradient_height,
+            floatingGradientColorResId = android.R.color.system_neutral1_900,
+            backgroundResId = R.drawable.bg_bubble_dismiss_circle,
+            iconResId = R.drawable.ic_bubble_dismiss_white
+        )
+    )
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
new file mode 100644
index 0000000..24dca5e
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles;
+
+import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY;
+import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW;
+import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM;
+
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+
+import com.android.launcher3.R;
+import com.android.wm.shell.animation.PhysicsAnimator;
+import com.android.wm.shell.common.bubbles.DismissCircleView;
+import com.android.wm.shell.common.bubbles.DismissView;
+
+/**
+ * The animator performs the bubble animations while dragging and coordinates bubble and dismiss
+ * view animations when it gets magnetized, released or dismissed.
+ */
+public class BubbleDragAnimator {
+    private static final float SCALE_BUBBLE_FOCUSED = 1.2f;
+    private static final float SCALE_BUBBLE_CAPTURED = 0.9f;
+    private static final float SCALE_BUBBLE_BAR_FOCUSED = 1.1f;
+
+    private final PhysicsAnimator.SpringConfig mDefaultConfig =
+            new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY);
+    private final PhysicsAnimator.SpringConfig mTranslationConfig =
+            new PhysicsAnimator.SpringConfig(STIFFNESS_MEDIUM, DAMPING_RATIO_LOW_BOUNCY);
+    @NonNull
+    private final View mView;
+    @NonNull
+    private final PhysicsAnimator<View> mBubbleAnimator;
+    @Nullable
+    private DismissView mDismissView;
+    @Nullable
+    private PhysicsAnimator<DismissCircleView> mDismissAnimator;
+    private final float mBubbleFocusedScale;
+    private final float mBubbleCapturedScale;
+    private final float mDismissCapturedScale;
+
+    /**
+     * Should be initialised for each dragged view
+     *
+     * @param view the dragged view to animate
+     */
+    public BubbleDragAnimator(@NonNull View view) {
+        mView = view;
+        mBubbleAnimator = PhysicsAnimator.getInstance(view);
+        mBubbleAnimator.setDefaultSpringConfig(mDefaultConfig);
+
+        Resources resources = view.getResources();
+        final int collapsedSize = resources.getDimensionPixelSize(
+                R.dimen.bubblebar_dismiss_target_small_size);
+        final int expandedSize = resources.getDimensionPixelSize(
+                R.dimen.bubblebar_dismiss_target_size);
+        mDismissCapturedScale = (float) collapsedSize / expandedSize;
+
+        if (view instanceof BubbleBarView) {
+            mBubbleFocusedScale = SCALE_BUBBLE_BAR_FOCUSED;
+            mBubbleCapturedScale = mDismissCapturedScale;
+        } else {
+            mBubbleFocusedScale = SCALE_BUBBLE_FOCUSED;
+            mBubbleCapturedScale = SCALE_BUBBLE_CAPTURED;
+        }
+    }
+
+    /**
+     * Sets dismiss view to be animated alongside the dragged bubble
+     */
+    public void setDismissView(@NonNull DismissView dismissView) {
+        mDismissView = dismissView;
+        mDismissAnimator = PhysicsAnimator.getInstance(dismissView.getCircle());
+        mDismissAnimator.setDefaultSpringConfig(mDefaultConfig);
+    }
+
+    /**
+     * Animates the focused state of the bubble when the dragging starts
+     */
+    public void animateFocused() {
+        mBubbleAnimator.cancel();
+        mBubbleAnimator
+                .spring(DynamicAnimation.SCALE_X, mBubbleFocusedScale)
+                .spring(DynamicAnimation.SCALE_Y, mBubbleFocusedScale)
+                .start();
+    }
+
+    /**
+     * Animates the dragged bubble movement back to the initial position.
+     *
+     * @param initialPosition the position to animate to
+     * @param velocity        the initial velocity to use for the spring animation
+     * @param endActions      gets called when the animation completes or gets cancelled
+     */
+    public void animateToInitialState(@NonNull PointF initialPosition, @NonNull PointF velocity,
+            @Nullable Runnable endActions) {
+        mBubbleAnimator.cancel();
+        mBubbleAnimator
+                .spring(DynamicAnimation.SCALE_X, 1f)
+                .spring(DynamicAnimation.SCALE_Y, 1f)
+                .spring(DynamicAnimation.TRANSLATION_X, initialPosition.x, velocity.x,
+                        mTranslationConfig)
+                .spring(DynamicAnimation.TRANSLATION_Y, initialPosition.y, velocity.y,
+                        mTranslationConfig)
+                .addEndListener((View target, @NonNull FloatPropertyCompat<? super View> property,
+                        boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
+                        boolean allRelevantPropertyAnimationsEnded) -> {
+                    if (canceled || allRelevantPropertyAnimationsEnded) {
+                        resetAnimatedViews(initialPosition);
+                        if (endActions != null) {
+                            endActions.run();
+                        }
+                    }
+                })
+                .start();
+    }
+
+    /**
+     * Animates the dragged view alongside the dismiss view when it gets captured in the dismiss
+     * target area.
+     */
+    public void animateDismissCaptured() {
+        mBubbleAnimator.cancel();
+        mBubbleAnimator
+                .spring(DynamicAnimation.SCALE_X, mBubbleCapturedScale)
+                .spring(DynamicAnimation.SCALE_Y, mBubbleCapturedScale)
+                .spring(DynamicAnimation.ALPHA, mDismissCapturedScale)
+                .start();
+
+        if (mDismissAnimator != null) {
+            mDismissAnimator.cancel();
+            mDismissAnimator
+                    .spring(DynamicAnimation.SCALE_X, mDismissCapturedScale)
+                    .spring(DynamicAnimation.SCALE_Y, mDismissCapturedScale)
+                    .start();
+        }
+    }
+
+    /**
+     * Animates the dragged view alongside the dismiss view when it gets released from the dismiss
+     * target area.
+     */
+    public void animateDismissReleased() {
+        mBubbleAnimator.cancel();
+        mBubbleAnimator
+                .spring(DynamicAnimation.SCALE_X, mBubbleFocusedScale)
+                .spring(DynamicAnimation.SCALE_Y, mBubbleFocusedScale)
+                .spring(DynamicAnimation.ALPHA, 1f)
+                .start();
+
+        if (mDismissAnimator != null) {
+            mDismissAnimator.cancel();
+            mDismissAnimator
+                    .spring(DynamicAnimation.SCALE_X, 1f)
+                    .spring(DynamicAnimation.SCALE_Y, 1f)
+                    .start();
+        }
+    }
+
+    /**
+     * Animates the dragged bubble dismiss when it's released in the dismiss target area.
+     *
+     * @param initialPosition the initial position to move the bubble too after animation finishes
+     * @param endActions      gets called when the animation completes or gets cancelled
+     */
+    public void animateDismiss(@NonNull PointF initialPosition, @Nullable Runnable endActions) {
+        float dismissHeight = mDismissView != null ? mDismissView.getHeight() : 0f;
+        float translationY = mView.getTranslationY() + dismissHeight;
+        mBubbleAnimator
+                .spring(DynamicAnimation.TRANSLATION_Y, translationY)
+                .spring(DynamicAnimation.SCALE_X, 0f)
+                .spring(DynamicAnimation.SCALE_Y, 0f)
+                .spring(DynamicAnimation.ALPHA, 0f)
+                .addEndListener((View target, @NonNull FloatPropertyCompat<? super View> property,
+                        boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
+                        boolean allRelevantPropertyAnimationsEnded) -> {
+                    if (canceled || allRelevantPropertyAnimationsEnded) {
+                        resetAnimatedViews(initialPosition);
+                        if (endActions != null) endActions.run();
+                    }
+                })
+                .start();
+    }
+
+    /**
+     * Reset the animated views to the initial state
+     *
+     * @param initialPosition position of the bubble
+     */
+    private void resetAnimatedViews(@NonNull PointF initialPosition) {
+        mView.setScaleX(1f);
+        mView.setScaleY(1f);
+        mView.setAlpha(1f);
+        mView.setTranslationX(initialPosition.x);
+        mView.setTranslationY(initialPosition.y);
+
+        if (mDismissView != null) {
+            mDismissView.getCircle().setScaleX(1f);
+            mDismissView.getCircle().setScaleY(1f);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
new file mode 100644
index 0000000..08fd681
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar.bubbles;
+
+import android.annotation.SuppressLint;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+
+/**
+ * Controls bubble bar drag to dismiss interaction.
+ * Interacts with {@link BubbleDismissController}, used by {@link BubbleBarViewController}.
+ * Supported interactions:
+ * - Drag a single bubble view into dismiss target to remove it.
+ * - Drag the bubble stack into dismiss target to remove all.
+ * Restores initial position of dragged view if released outside of the dismiss target.
+ */
+public class BubbleDragController {
+    private final TaskbarActivityContext mActivity;
+    private BubbleBarViewController mBubbleBarViewController;
+    private BubbleDismissController mBubbleDismissController;
+
+    public BubbleDragController(TaskbarActivityContext activity) {
+        mActivity = activity;
+    }
+
+    /**
+     * Initializes dependencies when bubble controllers are created.
+     * Should be careful to only access things that were created in constructors for now, as some
+     * controllers may still be waiting for init().
+     */
+    public void init(@NonNull BubbleControllers bubbleControllers) {
+        mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
+        mBubbleDismissController = bubbleControllers.bubbleDismissController;
+    }
+
+    /**
+     * Setup the bubble view for dragging and attach touch listener to it
+     */
+    @SuppressLint("ClickableViewAccessibility")
+    public void setupBubbleView(@NonNull BubbleView bubbleView) {
+        if (!(bubbleView.getBubble() instanceof BubbleBarBubble)) {
+            // Don't setup dragging for overflow bubble view
+            return;
+        }
+
+        bubbleView.setOnTouchListener(new BubbleTouchListener() {
+            @Override
+            void onDragStart() {
+                mBubbleBarViewController.onDragStart(bubbleView);
+            }
+
+            @Override
+            void onDragEnd() {
+                mBubbleBarViewController.onDragEnd();
+            }
+
+            @Override
+            protected void onDragRelease() {
+                mBubbleBarViewController.onDragRelease(bubbleView);
+            }
+        });
+    }
+
+    /**
+     * Setup the bubble bar view for dragging and attach touch listener to it
+     */
+    @SuppressLint("ClickableViewAccessibility")
+    public void setupBubbleBarView(@NonNull BubbleBarView bubbleBarView) {
+        PointF initialRelativePivot = new PointF();
+        bubbleBarView.setOnTouchListener(new BubbleTouchListener() {
+            @Override
+            protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
+                if (bubbleBarView.isExpanded()) return false;
+                return super.onTouchDown(view, event);
+            }
+
+            @Override
+            void onDragStart() {
+                initialRelativePivot.set(bubbleBarView.getRelativePivotX(),
+                        bubbleBarView.getRelativePivotY());
+                // By default the bubble bar view pivot is in bottom right corner, while dragging
+                // it should be centered in order to align it with the dismiss target view
+                bubbleBarView.setRelativePivot(/* x = */ 0.5f, /* y = */ 0.5f);
+            }
+
+            @Override
+            void onDragEnd() {
+                // Restoring the initial pivot for the bubble bar view
+                bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y);
+            }
+        });
+    }
+
+    /**
+     * Bubble touch listener for handling a single bubble view or bubble bar view while dragging.
+     * The dragging starts after "shorter" long click (the long click duration might change):
+     * - When the touch gesture moves out of the {@code ACTION_DOWN} location the dragging
+     * interaction is cancelled.
+     * - When {@code ACTION_UP} happens before long click is registered and there was no significant
+     * movement the view will perform click.
+     * - When the listener registers long click it starts dragging interaction, all the subsequent
+     * {@code ACTION_MOVE} events will drag the view, and the interaction finishes when
+     * {@code ACTION_UP} or {@code ACTION_CANCEL} are received.
+     * Lifecycle methods can be overridden do add extra setup/clean up steps.
+     */
+    private abstract class BubbleTouchListener implements View.OnTouchListener {
+        /**
+         * The internal state of the touch listener
+         */
+        private enum State {
+            // Idle and ready for the touch events.
+            // Changes to:
+            // - TOUCHED, when the {@code ACTION_DOWN} is handled
+            IDLE,
+
+            // Touch down was handled and the lister is recognising the gestures.
+            // Changes to:
+            // - IDLE, when performs the click
+            // - DRAGGING, when registers the long click and starts dragging interaction
+            // - CANCELLED, when the touch events move out of the initial location before the long
+            // click is recognised
+
+            TOUCHED,
+
+            // The long click was registered and the view is being dragged.
+            // Changes to:
+            // - IDLE, when the gesture ends with the {@code ACTION_UP} or {@code ACTION_CANCEL}
+            DRAGGING,
+
+            // The dragging was cancelled.
+            // Changes to:
+            // - IDLE, when the current gesture completes
+            CANCELLED
+        }
+
+        private final PointF mTouchDownLocation = new PointF();
+        private final PointF mViewInitialPosition = new PointF();
+        private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+        private final long mPressToDragTimeout = ViewConfiguration.getLongPressTimeout() / 2;
+        private State mState = State.IDLE;
+        private int mTouchSlop = -1;
+        private BubbleDragAnimator mAnimator;
+        @Nullable
+        private Runnable mLongClickRunnable;
+
+        /**
+         * Called when the dragging interaction has started
+         */
+        abstract void onDragStart();
+
+        /**
+         * Called when the dragging interaction has ended and all the animations have completed
+         */
+        abstract void onDragEnd();
+
+        /**
+         * Called when the dragged bubble is released outside of the dismiss target area and will
+         * move back to its initial position
+         */
+        protected void onDragRelease() {
+        }
+
+        /**
+         * Called when the dragged bubble is released inside of the dismiss target area and will get
+         * dismissed with animation
+         */
+        protected void onDragDismiss() {
+        }
+
+        @Override
+        @SuppressLint("ClickableViewAccessibility")
+        public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) {
+            updateVelocity(event);
+            switch (event.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN:
+                    return onTouchDown(view, event);
+                case MotionEvent.ACTION_MOVE:
+                    onTouchMove(view, event);
+                    break;
+                case MotionEvent.ACTION_UP:
+                    onTouchUp(view, event);
+                    break;
+                case MotionEvent.ACTION_CANCEL:
+                    onTouchCancel(view, event);
+                    break;
+            }
+            return true;
+        }
+
+        /**
+         * The touch down starts the interaction and schedules the long click handler.
+         *
+         * @param view  the view that received the event
+         * @param event the motion event
+         * @return true if the gesture should be intercepted and handled, false otherwise. Note if
+         * the false is returned subsequent events in the gesture won't get reported.
+         */
+        protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
+            mState = State.TOUCHED;
+            mTouchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
+            mTouchDownLocation.set(event.getRawX(), event.getRawY());
+            mViewInitialPosition.set(view.getTranslationX(), view.getTranslationY());
+            setupLongClickHandler(view);
+            return true;
+        }
+
+        /**
+         * The move event drags the view or cancels the interaction if hasn't long clicked yet.
+         *
+         * @param view  the view that received the event
+         * @param event the motion event
+         */
+        protected void onTouchMove(@NonNull View view, @NonNull MotionEvent event) {
+            final float dx = event.getRawX() - mTouchDownLocation.x;
+            final float dy = event.getRawY() - mTouchDownLocation.y;
+            switch (mState) {
+                case TOUCHED:
+                    final boolean movedOut = Math.hypot(dx, dy) > mTouchSlop;
+                    if (movedOut) {
+                        // Moved out of the initial location before the long click was registered
+                        mState = State.CANCELLED;
+                        cleanUpLongClickHandler(view);
+                    }
+                    break;
+                case DRAGGING:
+                    drag(view, event, dx, dy);
+                    break;
+            }
+        }
+
+        /**
+         * On touch up performs click or finishes the dragging depending on the state.
+         *
+         * @param view  the view that received the event
+         * @param event the motion event
+         */
+        protected void onTouchUp(@NonNull View view, @NonNull MotionEvent event) {
+            switch (mState) {
+                case TOUCHED:
+                    view.performClick();
+                    cleanUp(view);
+                    break;
+                case DRAGGING:
+                    stopDragging(view, event);
+                    break;
+                default:
+                    cleanUp(view);
+                    break;
+            }
+        }
+
+        /**
+         * The gesture is cancelled and the interaction should clean up and complete.
+         *
+         * @param view  the view that received the event
+         * @param event the motion event
+         */
+        protected void onTouchCancel(@NonNull View view, @NonNull MotionEvent event) {
+            if (mState == State.DRAGGING) {
+                stopDragging(view, event);
+            } else {
+                cleanUp(view);
+            }
+        }
+
+        private void startDragging(@NonNull View view) {
+            onDragStart();
+            mActivity.setTaskbarWindowFullscreen(true);
+            mAnimator = new BubbleDragAnimator(view);
+            mAnimator.animateFocused();
+            mBubbleDismissController.setupDismissView(view, mAnimator);
+            mBubbleDismissController.showDismissView();
+        }
+
+        private void drag(@NonNull View view, @NonNull MotionEvent event, float dx, float dy) {
+            if (mBubbleDismissController.handleTouchEvent(event)) return;
+            view.setTranslationX(mViewInitialPosition.x + dx);
+            view.setTranslationY(mViewInitialPosition.y + dy);
+        }
+
+        private void stopDragging(@NonNull View view, @NonNull MotionEvent event) {
+            Runnable onComplete = () -> {
+                mActivity.setTaskbarWindowFullscreen(false);
+                cleanUp(view);
+                onDragEnd();
+            };
+
+            if (mBubbleDismissController.handleTouchEvent(event)) {
+                onDragDismiss();
+                mAnimator.animateDismiss(mViewInitialPosition, onComplete);
+            } else {
+                onDragRelease();
+                mAnimator.animateToInitialState(mViewInitialPosition, getCurrentVelocity(),
+                        onComplete);
+            }
+            mBubbleDismissController.hideDismissView();
+        }
+
+        private void setupLongClickHandler(@NonNull View view) {
+            cleanUpLongClickHandler(view);
+            mLongClickRunnable = () -> {
+                // Register long click and start dragging interaction
+                mState = State.DRAGGING;
+                startDragging(view);
+            };
+            view.getHandler().postDelayed(mLongClickRunnable, mPressToDragTimeout);
+        }
+
+        private void cleanUpLongClickHandler(@NonNull View view) {
+            if (mLongClickRunnable == null || view.getHandler() == null) return;
+            view.getHandler().removeCallbacks(mLongClickRunnable);
+            mLongClickRunnable = null;
+        }
+
+        private void cleanUp(@NonNull View view) {
+            cleanUpLongClickHandler(view);
+            mVelocityTracker.clear();
+            mState = State.IDLE;
+        }
+
+        private void updateVelocity(MotionEvent event) {
+            final float deltaX = event.getRawX() - event.getX();
+            final float deltaY = event.getRawY() - event.getY();
+            event.offsetLocation(deltaX, deltaY);
+            mVelocityTracker.addMovement(event);
+            event.offsetLocation(-deltaX, -deltaY);
+        }
+
+        private PointF getCurrentVelocity() {
+            mVelocityTracker.computeCurrentVelocity(/* units = */ 1000);
+            return new PointF(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 0ab53b0..a5ea5a9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.taskbar.bubbles;
 
+import static java.lang.Math.abs;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
@@ -25,6 +27,7 @@
 import com.android.launcher3.taskbar.StashedHandleViewController;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.taskbar.TaskbarInsetsController;
 import com.android.launcher3.taskbar.TaskbarStashController;
 import com.android.launcher3.util.MultiPropertyFactory;
 
@@ -50,6 +53,7 @@
 
     // Initialized in init.
     private TaskbarControllers mControllers;
+    private TaskbarInsetsController mTaskbarInsetsController;
     private BubbleBarViewController mBarViewController;
     private BubbleStashedHandleViewController mHandleViewController;
     private TaskbarStashController mTaskbarStashController;
@@ -67,6 +71,7 @@
     private int mUnstashedHeight;
     private boolean mBubblesShowingOnHome;
     private boolean mBubblesShowingOnOverview;
+    private boolean mIsSysuiLocked;
 
     @Nullable
     private AnimatorSet mAnimator;
@@ -77,6 +82,7 @@
 
     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
         mControllers = controllers;
+        mTaskbarInsetsController = controllers.taskbarInsetsController;
         mBarViewController = bubbleControllers.bubbleBarViewController;
         mHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
         mTaskbarStashController = controllers.taskbarStashController;
@@ -90,14 +96,6 @@
 
         mStashedHeight = mHandleViewController.getStashedHeight();
         mUnstashedHeight = mHandleViewController.getUnstashedHeight();
-
-        bubbleControllers.runAfterInit(() -> {
-            if (mTaskbarStashController.isStashed()) {
-                stashBubbleBar();
-            } else {
-                showBubbleBar(false /* expandBubbles */);
-            }
-        });
     }
 
     /**
@@ -115,13 +113,63 @@
     }
 
     /**
+     * Animates the bubble bar and handle to their initial state, transitioning from the state where
+     * both views are invisible. Called when the first bubble is added or when the device is
+     * unlocked.
+     *
+     * <p>Normally either the bubble bar or the handle is visible,
+     * and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to transition
+     * between these two states. But the transition from the state where both the bar and handle
+     * are invisible is slightly different.
+     *
+     * <p>The initial state will depend on the current state of the device, i.e. overview, home etc
+     * and whether bubbles are requested to be expanded.
+     */
+    public void animateToInitialState(boolean expanding) {
+        AnimatorSet animatorSet = new AnimatorSet();
+        if (expanding || mBubblesShowingOnHome || mBubblesShowingOnOverview) {
+            mIsStashed = false;
+            animatorSet.playTogether(mIconScaleForStash.animateToValue(1),
+                    mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()),
+                    mIconAlphaForStash.animateToValue(1));
+        } else {
+            mIsStashed = true;
+            animatorSet.playTogether(mBubbleStashedHandleAlpha.animateToValue(1));
+        }
+
+        animatorSet.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                onIsStashedChanged();
+            }
+        });
+        animatorSet.setDuration(BAR_STASH_DURATION).start();
+    }
+
+    /**
      * Called when launcher enters or exits the home page. Bubbles are unstashed on home.
      */
     public void setBubblesShowingOnHome(boolean onHome) {
         if (mBubblesShowingOnHome != onHome) {
             mBubblesShowingOnHome = onHome;
+
+            if (!mBarViewController.hasBubbles()) {
+                // if there are no bubbles, there's nothing to show, so just return.
+                return;
+            }
+
             if (mBubblesShowingOnHome) {
                 showBubbleBar(/* expanded= */ false);
+                // When transitioning from app to home the stash animator may already have been
+                // created, so we need to animate the bubble bar here to align with hotseat.
+                if (!mIsStashed) {
+                    mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForHotseat())
+                            .start();
+                }
+                // If the bubble bar is already unstashed, the taskbar touchable region won't be
+                // updated correctly, so force an update here.
+                mControllers.runAfterInit(() ->
+                        mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged());
             } else if (!mBarViewController.isExpanded()) {
                 stashBubbleBar();
             }
@@ -140,18 +188,27 @@
             mBubblesShowingOnOverview = onOverview;
             if (!mBubblesShowingOnOverview && !mBarViewController.isExpanded()) {
                 stashBubbleBar();
+            } else {
+                // When transitioning to overview the stash animator may already have been
+                // created, so we need to animate the bubble bar here to align with taskbar.
+                mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForTaskbar())
+                        .start();
             }
         }
     }
 
+    /** Whether bubbles are showing on Overview. */
+    public boolean isBubblesShowingOnOverview() {
+        return mBubblesShowingOnOverview;
+    }
+
     /** Called when sysui locked state changes, when locked, bubble bar is stashed. */
     public void onSysuiLockedStateChange(boolean isSysuiLocked) {
-        if (isSysuiLocked) {
-            // TODO: should the normal path flip mBubblesOnHome / check if this is needed
-            // If we're locked, we're no longer showing on home.
-            mBubblesShowingOnHome = false;
-            mBubblesShowingOnOverview = false;
-            stashBubbleBar();
+        if (isSysuiLocked != mIsSysuiLocked) {
+            mIsSysuiLocked = isSysuiLocked;
+            if (!mIsSysuiLocked && mBarViewController.hasBubbles()) {
+                animateToInitialState(false /* expanding */);
+            }
         }
     }
 
@@ -230,9 +287,8 @@
             firstHalfDurationScale = 0.5f;
             secondHalfDurationScale = 0.75f;
 
-            // If we're on home, adjust the translation so the bubble bar aligns with hotseat.
-            final float hotseatTransY = mActivity.getDeviceProfile().getTaskbarOffsetY();
-            final float translationY = mBubblesShowingOnHome ? hotseatTransY : 0;
+            final float translationY = getBubbleBarTranslationY();
+
             fullLengthAnimatorSet.playTogether(
                     mIconScaleForStash.animateToValue(1),
                     mIconTranslationYForStash.animateToValue(translationY));
@@ -262,6 +318,7 @@
                     if (isStashed) {
                         mBarViewController.setExpanded(false);
                     }
+                    mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
                 });
             }
         });
@@ -271,7 +328,26 @@
     private void onIsStashedChanged() {
         mControllers.runAfterInit(() -> {
             mHandleViewController.onIsStashedChanged();
-            // TODO: when stash changes tell taskbarInsetsController the insets have changed.
+            mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
         });
     }
+
+    private float getBubbleBarTranslationYForTaskbar() {
+        return -mActivity.getDeviceProfile().taskbarBottomMargin;
+    }
+
+    private float getBubbleBarTranslationYForHotseat() {
+        final float hotseatBottomSpace = mActivity.getDeviceProfile().hotseatBarBottomSpacePx;
+        final float hotseatCellHeight = mActivity.getDeviceProfile().hotseatCellHeightPx;
+        return -hotseatBottomSpace - hotseatCellHeight + mUnstashedHeight - abs(
+                hotseatCellHeight - mUnstashedHeight) / 2;
+    }
+
+    float getBubbleBarTranslationY() {
+        // If we're on home, adjust the translation so the bubble bar aligns with hotseat.
+        // Otherwise we're either showing in an app or in overview. In either case adjust it so
+        // the bubble bar aligns with the taskbar.
+        return mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat()
+                : getBubbleBarTranslationYForTaskbar();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
index 2170a5d..fbab595 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
@@ -52,6 +52,7 @@
     private BubbleStashController mBubbleStashController;
     private RegionSamplingHelper mRegionSamplingHelper;
     private int mBarSize;
+    private int mStashedTaskbarHeight;
     private int mStashedHandleWidth;
     private int mStashedHandleHeight;
 
@@ -92,7 +93,7 @@
 
         mTaskbarStashedHandleAlpha.get(0).setValue(0);
 
-        final int stashedTaskbarHeight = resources.getDimensionPixelSize(
+        mStashedTaskbarHeight = resources.getDimensionPixelSize(
                 R.dimen.bubblebar_stashed_size);
         mStashedHandleView.setOutlineProvider(new ViewOutlineProvider() {
             @Override
@@ -115,22 +116,27 @@
                     }
                 }, Executors.UI_HELPER_EXECUTOR);
 
-        mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) -> {
-            // As more bubbles get added, the icon bounds become larger. To ensure a consistent
-            // handle bar position, we pin it to the edge of the screen.
-            Rect bubblebarRect = mBarViewController.getBubbleBarBounds();
-            final int stashedCenterY = view.getHeight() - stashedTaskbarHeight / 2;
+        mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) ->
+                updateBounds());
+    }
 
-            mStashedHandleBounds.set(
-                    bubblebarRect.right - mStashedHandleWidth,
-                    stashedCenterY - mStashedHandleHeight / 2,
-                    bubblebarRect.right,
-                    stashedCenterY + mStashedHandleHeight / 2);
-            mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
+    private void updateBounds() {
+        // As more bubbles get added, the icon bounds become larger. To ensure a consistent
+        // handle bar position, we pin it to the edge of the screen.
+        final int right =
+                mActivity.getDeviceProfile().widthPx - mBarViewController.getHorizontalMargin();
 
-            view.setPivotX(view.getWidth());
-            view.setPivotY(view.getHeight() - stashedTaskbarHeight / 2f);
-        });
+        final int stashedCenterY = mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2;
+
+        mStashedHandleBounds.set(
+                right - mStashedHandleWidth,
+                stashedCenterY - mStashedHandleHeight / 2,
+                right,
+                stashedCenterY + mStashedHandleHeight / 2);
+        mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
+
+        mStashedHandleView.setPivotX(mStashedHandleView.getWidth());
+        mStashedHandleView.setPivotY(mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2f);
     }
 
     public void onDestroy() {
@@ -188,6 +194,7 @@
             mStashedHandleView.setVisibility(VISIBLE);
         } else {
             mStashedHandleView.setVisibility(INVISIBLE);
+            mStashedHandleView.setAlpha(0);
         }
         updateRegionSampling();
     }
@@ -203,12 +210,14 @@
     private void updateRegionSampling() {
         boolean handleVisible = mStashedHandleView.getVisibility() == VISIBLE
                 && mBubbleStashController.isStashed();
-        mRegionSamplingHelper.setWindowVisible(handleVisible);
-        if (handleVisible) {
-            mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
-            mRegionSamplingHelper.start(mStashedHandleView.getSampledRegion());
-        } else {
-            mRegionSamplingHelper.stop();
+        if (mRegionSamplingHelper != null) {
+            mRegionSamplingHelper.setWindowVisible(handleVisible);
+            if (handleVisible) {
+                mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
+                mRegionSamplingHelper.start(mStashedHandleView.getSampledRegion());
+            } else {
+                mRegionSamplingHelper.stop();
+            }
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index e22e63a..12cb8c5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -18,7 +18,9 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Bitmap;
+import android.graphics.Canvas;
 import android.graphics.Outline;
+import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -28,27 +30,59 @@
 import androidx.constraintlayout.widget.ConstraintLayout;
 
 import com.android.launcher3.R;
+import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconNormalizer;
+import com.android.wm.shell.animation.Interpolators;
+
+import java.util.EnumSet;
 
 // TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
-// TODO: (b/269670235) currently this doesn't show the 'update dot'
+
 /**
  * View that displays a bubble icon, along with an app badge on either the left or
  * right side of the view.
  */
 public class BubbleView extends ConstraintLayout {
 
-    // TODO: (b/269670235) currently we don't render the 'update dot', this will be used for that.
     public static final int DEFAULT_PATH_SIZE = 100;
 
+    /**
+     * Flags that suppress the visibility of the 'new' dot or the app badge, for one reason or
+     * another. If any of these flags are set, the dot will not be shown.
+     * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown.
+     */
+    enum SuppressionFlag {
+        // TODO: (b/277815200) implement flyout
+        // Suppressed because the flyout is visible - it will morph into the dot via animation.
+        FLYOUT_VISIBLE,
+        // Suppressed because this bubble is behind others in the collapsed stack.
+        BEHIND_STACK,
+    }
+
+    private final EnumSet<SuppressionFlag> mSuppressionFlags =
+            EnumSet.noneOf(SuppressionFlag.class);
+
     private final ImageView mBubbleIcon;
     private final ImageView mAppIcon;
     private final int mBubbleSize;
 
+    private DotRenderer mDotRenderer;
+    private DotRenderer.DrawParams mDrawParams;
+    private int mDotColor;
+    private Rect mTempBounds = new Rect();
+
+    // Whether the dot is animating
+    private boolean mDotIsAnimating;
+    // What scale value the dot is animating to
+    private float mAnimatingToDotScale;
+    // The current scale value of the dot
+    private float mDotScale;
+
     // TODO: (b/273310265) handle RTL
+    // Whether the bubbles are positioned on the left or right side of the screen
     private boolean mOnLeft = false;
 
-    private BubbleBarBubble mBubble;
+    private BubbleBarItem mBubble;
 
     public BubbleView(Context context) {
         this(context, null);
@@ -74,6 +108,8 @@
         mBubbleIcon = findViewById(R.id.icon_view);
         mAppIcon = findViewById(R.id.app_icon_view);
 
+        mDrawParams = new DotRenderer.DrawParams();
+
         setFocusable(true);
         setClickable(true);
         setOutlineProvider(new ViewOutlineProvider() {
@@ -90,45 +126,147 @@
         outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
     }
 
+    @Override
+    public void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+
+        if (!shouldDrawDot()) {
+            return;
+        }
+
+        getDrawingRect(mTempBounds);
+
+        mDrawParams.dotColor = mDotColor;
+        mDrawParams.iconBounds = mTempBounds;
+        mDrawParams.leftAlign = mOnLeft;
+        mDrawParams.scale = mDotScale;
+
+        mDotRenderer.draw(canvas, mDrawParams);
+    }
+
     /** Sets the bubble being rendered in this view. */
     void setBubble(BubbleBarBubble bubble) {
         mBubble = bubble;
         mBubbleIcon.setImageBitmap(bubble.getIcon());
         mAppIcon.setImageBitmap(bubble.getBadge());
+        mDotColor = bubble.getDotColor();
+        mDotRenderer = new DotRenderer(mBubbleSize, bubble.getDotPath(), DEFAULT_PATH_SIZE);
+    }
+
+    /**
+     * Sets that this bubble represents the overflow. The overflow appears in the list of bubbles
+     * but does not represent app content, instead it shows recent bubbles that couldn't fit into
+     * the list of bubbles. It doesn't show an app icon because it is part of system UI / doesn't
+     * come from an app.
+     */
+    void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
+        mBubble = overflow;
+        mBubbleIcon.setImageBitmap(bitmap);
+        mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
     }
 
     /** Returns the bubble being rendered in this view. */
     @Nullable
-    BubbleBarBubble getBubble() {
+    BubbleBarItem getBubble() {
         return mBubble;
     }
 
-    /** Shows the app badge on this bubble. */
-    void showBadge() {
-        Bitmap appBadgeBitmap = mBubble.getBadge();
-        if (appBadgeBitmap == null) {
-            mAppIcon.setVisibility(GONE);
+    void updateDotVisibility(boolean animate) {
+        final float targetScale = shouldDrawDot() ? 1f : 0f;
+        if (animate) {
+            animateDotScale();
+        } else {
+            mDotScale = targetScale;
+            mAnimatingToDotScale = targetScale;
+            invalidate();
+        }
+    }
+
+    void updateBadgeVisibility() {
+        if (mBubble instanceof BubbleBarOverflow) {
+            // The overflow bubble does not have a badge, so just bail.
+            return;
+        }
+        BubbleBarBubble bubble = (BubbleBarBubble) mBubble;
+        Bitmap appBadgeBitmap = bubble.getBadge();
+        int translationX = mOnLeft
+                ? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth())
+                : 0;
+        mAppIcon.setTranslationX(translationX);
+        mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE);
+    }
+
+    /** Sets whether this bubble is in the stack & not the first bubble. **/
+    void setBehindStack(boolean behindStack, boolean animate) {
+        if (behindStack) {
+            mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK);
+        } else {
+            mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK);
+        }
+        updateDotVisibility(animate);
+        updateBadgeVisibility();
+    }
+
+    /** Whether this bubble is in the stack & not the first bubble. **/
+    boolean isBehindStack() {
+        return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK);
+    }
+
+    /** Whether the dot indicating unseen content in a bubble should be shown. */
+    private boolean shouldDrawDot() {
+        boolean bubbleHasUnseenContent = mBubble != null
+                && mBubble instanceof BubbleBarBubble
+                && mSuppressionFlags.isEmpty()
+                && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();
+
+        // Always render the dot if it's animating, since it could be animating out. Otherwise, show
+        // it if the bubble wants to show it, and we aren't suppressing it.
+        return bubbleHasUnseenContent || mDotIsAnimating;
+    }
+
+    /** How big the dot should be, fraction from 0 to 1. */
+    private void setDotScale(float fraction) {
+        mDotScale = fraction;
+        invalidate();
+    }
+
+    /**
+     * Animates the dot to the given scale.
+     */
+    private void animateDotScale() {
+        float toScale = shouldDrawDot() ? 1f : 0f;
+        mDotIsAnimating = true;
+
+        // Don't restart the animation if we're already animating to the given value.
+        if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
+            mDotIsAnimating = false;
             return;
         }
 
-        int translationX;
-        if (mOnLeft) {
-            translationX = -(mBubble.getIcon().getWidth() - appBadgeBitmap.getWidth());
-        } else {
-            translationX = 0;
-        }
+        mAnimatingToDotScale = toScale;
 
-        mAppIcon.setTranslationX(translationX);
-        mAppIcon.setVisibility(VISIBLE);
+        final boolean showDot = toScale > 0f;
+
+        // Do NOT wait until after animation ends to setShowDot
+        // to avoid overriding more recent showDot states.
+        clearAnimation();
+        animate()
+                .setDuration(200)
+                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                .setUpdateListener((valueAnimator) -> {
+                    float fraction = valueAnimator.getAnimatedFraction();
+                    fraction = showDot ? fraction : 1f - fraction;
+                    setDotScale(fraction);
+                }).withEndAction(() -> {
+                    setDotScale(showDot ? 1f : 0f);
+                    mDotIsAnimating = false;
+                }).start();
     }
 
-    /** Hides the app badge on this bubble. */
-    void hideBadge() {
-        mAppIcon.setVisibility(GONE);
-    }
 
     @Override
     public String toString() {
-        return "BubbleView{" + mBubble + "}";
+        String toString = mBubble != null ? mBubble.getKey() : "null";
+        return "BubbleView{" + toString + "}";
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/OWNERS b/quickstep/src/com/android/launcher3/taskbar/bubbles/OWNERS
new file mode 100644
index 0000000..7af0389
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/OWNERS
@@ -0,0 +1 @@
+madym@google.com
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
index 27a4988..b682081 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
@@ -17,10 +17,12 @@
 package com.android.launcher3.taskbar.navbutton
 
 import android.content.res.Resources
+import android.graphics.drawable.RotateDrawable
 import android.view.ViewGroup
 import android.widget.ImageView
 import android.widget.LinearLayout
 import com.android.launcher3.R
+import com.android.launcher3.Utilities
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter
 
 /**
@@ -40,7 +42,18 @@
     protected val endContextualContainer: ViewGroup,
     protected val startContextualContainer: ViewGroup
 ) : NavButtonLayoutter {
-    protected val homeButton: ImageView = navButtonContainer.findViewById(R.id.home)
-    protected val recentsButton: ImageView = navButtonContainer.findViewById(R.id.recent_apps)
-    protected val backButton: ImageView = navButtonContainer.findViewById(R.id.back)
+    protected val homeButton: ImageView? = navButtonContainer.findViewById(R.id.home)
+    protected val recentsButton: ImageView? = navButtonContainer.findViewById(R.id.recent_apps)
+    protected val backButton: ImageView? = navButtonContainer.findViewById(R.id.back)
+
+    init {
+        // setup back button drawable
+        if (backButton != null) {
+            val rotateDrawable = RotateDrawable()
+            rotateDrawable.drawable = backButton.context?.getDrawable(R.drawable.ic_sysbar_back)
+            rotateDrawable.fromDegrees = 0f
+            rotateDrawable.toDegrees = if (Utilities.isRtl(backButton.resources)) 90f else -90f
+            backButton.setImageDrawable(rotateDrawable)
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
index 468a1a7..4a53c0c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
@@ -51,10 +51,10 @@
         val paddingTop = (buttonHeight - iconSize) / 2
 
         // Update icons
-        backButton.setImageDrawable(backButton.context.getDrawable(DRAWABLE_SYSBAR_BACK_KIDS))
+        backButton!!.setImageDrawable(backButton.context.getDrawable(DRAWABLE_SYSBAR_BACK_KIDS))
         backButton.scaleType = ImageView.ScaleType.FIT_CENTER
         backButton.setPadding(paddingLeft, paddingTop, paddingLeft, paddingTop)
-        homeButton.setImageDrawable(homeButton.getContext().getDrawable(DRAWABLE_SYSBAR_HOME_KIDS))
+        homeButton!!.setImageDrawable(homeButton.context.getDrawable(DRAWABLE_SYSBAR_HOME_KIDS))
         homeButton.scaleType = ImageView.ScaleType.FIT_CENTER
         homeButton.setPadding(paddingLeft, paddingTop, paddingLeft, paddingTop)
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
index 0668da9..5c7f2be 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
@@ -17,6 +17,8 @@
 package com.android.launcher3.taskbar.navbutton
 
 import android.content.res.Resources
+import android.view.Surface.ROTATION_90
+import android.view.Surface.Rotation
 import android.view.ViewGroup
 import android.widget.FrameLayout
 import android.widget.LinearLayout
@@ -56,7 +58,8 @@
             isKidsMode: Boolean,
             isInSetup: Boolean,
             isThreeButtonNav: Boolean,
-            phoneMode: Boolean
+            phoneMode: Boolean,
+            @Rotation surfaceRotation: Int
         ): NavButtonLayoutter {
             val navButtonContainer = navButtonsView.findViewById<LinearLayout>(ID_END_NAV_BUTTONS)
             val endContextualContainer =
@@ -64,6 +67,7 @@
             val startContextualContainer =
                 navButtonsView.findViewById<ViewGroup>(ID_START_CONTEXTUAL_BUTTONS)
             val isPhoneNavMode = phoneMode && isThreeButtonNav
+            val isPhoneGestureMode = phoneMode && !isThreeButtonNav
             return when {
                 isPhoneNavMode -> {
                     if (!deviceProfile.isLandscape) {
@@ -73,15 +77,30 @@
                             endContextualContainer,
                             startContextualContainer
                         )
-                    } else {
+                    } else if (surfaceRotation == ROTATION_90) {
                         PhoneLandscapeNavLayoutter(
                             resources,
                             navButtonContainer,
                             endContextualContainer,
                             startContextualContainer
                         )
+                    } else {
+                        PhoneSeascapeNavLayoutter(
+                                resources,
+                                navButtonContainer,
+                                endContextualContainer,
+                                startContextualContainer
+                        )
                     }
                 }
+                isPhoneGestureMode ->{
+                    PhoneGestureLayoutter(
+                            resources,
+                            navButtonContainer,
+                            endContextualContainer,
+                            startContextualContainer
+                    )
+                }
                 deviceProfile.isTaskbarPresent -> {
                     return when {
                         isInSetup -> {
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
new file mode 100644
index 0000000..8525c6c
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.launcher3.taskbar.navbutton
+
+import android.content.res.Resources
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import com.android.launcher3.DeviceProfile
+
+/** Layoutter for showing gesture navigation on phone screen. No buttons here, no-op container */
+class PhoneGestureLayoutter(
+        resources: Resources,
+        navBarContainer: LinearLayout,
+        endContextualContainer: ViewGroup,
+        startContextualContainer: ViewGroup
+) :
+        AbstractNavButtonLayoutter(
+                resources,
+                navBarContainer,
+                endContextualContainer,
+                startContextualContainer
+        ) {
+
+    override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
+        // no-op
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
index 201895f..2acd5d4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
@@ -27,7 +27,7 @@
 import com.android.launcher3.taskbar.TaskbarManager
 import com.android.launcher3.util.DimensionUtils
 
-class PhoneLandscapeNavLayoutter(
+open class PhoneLandscapeNavLayoutter(
     resources: Resources,
     navBarContainer: LinearLayout,
     endContextualContainer: ViewGroup,
@@ -42,17 +42,15 @@
 
     override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
         // TODO(b/230395757): Polish pending, this is just to make it usable
-        val navContainerParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams
         val endStartMargins = resources.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size)
-        val taskbarDimensions =
-            DimensionUtils.getTaskbarPhoneDimensions(dp, resources, TaskbarManager.isPhoneMode(dp))
+        val taskbarDimensions = DimensionUtils.getTaskbarPhoneDimensions(dp, resources,
+                TaskbarManager.isPhoneMode(dp))
         navButtonContainer.removeAllViews()
         navButtonContainer.orientation = LinearLayout.VERTICAL
 
+        val navContainerParams = FrameLayout.LayoutParams(
+                taskbarDimensions.x, ViewGroup.LayoutParams.MATCH_PARENT)
         navContainerParams.apply {
-            width = taskbarDimensions.x
-            height = ViewGroup.LayoutParams.MATCH_PARENT
-            gravity = Gravity.CENTER
             topMargin = endStartMargins
             bottomMargin = endStartMargins
             marginEnd = 0
@@ -65,6 +63,7 @@
         navButtonContainer.addView(backButton)
 
         navButtonContainer.layoutParams = navContainerParams
+        navButtonContainer.gravity = Gravity.CENTER
 
         // Add the spaces in between the nav buttons
         val spaceInBetween: Int =
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
index f7ac974..c763115 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
@@ -41,27 +41,31 @@
 
     override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
         // TODO(b/230395757): Polish pending, this is just to make it usable
-        val navContainerParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams
         val taskbarDimensions =
-            DimensionUtils.getTaskbarPhoneDimensions(dp, resources, TaskbarManager.isPhoneMode(dp))
+            DimensionUtils.getTaskbarPhoneDimensions(dp, resources,
+                    TaskbarManager.isPhoneMode(dp))
         val endStartMargins = resources.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size)
-        navContainerParams.width = taskbarDimensions.x
-        navContainerParams.height = ViewGroup.LayoutParams.MATCH_PARENT
-        navContainerParams.gravity = Gravity.CENTER_VERTICAL
 
         // Ensure order of buttons is correct
         navButtonContainer.removeAllViews()
         navButtonContainer.orientation = LinearLayout.HORIZONTAL
-        navContainerParams.topMargin = 0
-        navContainerParams.bottomMargin = 0
-        navContainerParams.marginEnd = endStartMargins
-        navContainerParams.marginStart = endStartMargins
+
+        val navContainerParams = FrameLayout.LayoutParams(
+                taskbarDimensions.x, ViewGroup.LayoutParams.MATCH_PARENT)
+        navContainerParams.apply {
+            topMargin = 0
+            bottomMargin = 0
+            marginEnd = endStartMargins
+            marginStart = endStartMargins
+        }
+
         // Swap recents and back button in case we were landscape prior to this
         navButtonContainer.addView(backButton)
         navButtonContainer.addView(homeButton)
         navButtonContainer.addView(recentsButton)
 
         navButtonContainer.layoutParams = navContainerParams
+        navButtonContainer.gravity = Gravity.CENTER
 
         // Add the spaces in between the nav buttons
         val spaceInBetween =
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
new file mode 100644
index 0000000..f0fe581
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
@@ -0,0 +1,46 @@
+/*
+* Copyright (C) 2023 The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*      http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License
+*/
+
+package com.android.launcher3.taskbar.navbutton
+
+import android.content.res.Resources
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import com.android.launcher3.DeviceProfile
+
+class PhoneSeascapeNavLayoutter(
+        resources: Resources,
+        navBarContainer: LinearLayout,
+        endContextualContainer: ViewGroup,
+        startContextualContainer: ViewGroup
+) :
+        PhoneLandscapeNavLayoutter(
+                resources,
+                navBarContainer,
+                endContextualContainer,
+                startContextualContainer
+        ) {
+
+    override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
+        // TODO(b/230395757): Polish pending, this is just to make it usable
+        super.layoutButtons(dp, isContextualButtonShowing)
+        navButtonContainer.removeAllViews()
+        // Flip ordering of back and recents buttons
+        navButtonContainer.addView(backButton)
+        navButtonContainer.addView(homeButton)
+        navButtonContainer.addView(recentsButton)
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
index 5ec7ca0..8332b7d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
@@ -40,7 +40,6 @@
 
     override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
         // Add spacing after the end of the last nav button
-        val navButtonParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams
         var navMarginEnd = resources.getDimension(dp.inv.inlineNavButtonsEndSpacing).toInt()
         val contextualWidth = endContextualContainer.width
         // If contextual buttons are showing, we check if the end margin is enough for the
@@ -50,10 +49,10 @@
             navMarginEnd += resources.getDimensionPixelSize(R.dimen.taskbar_hotseat_nav_spacing) / 2
         }
 
+        val navButtonParams = FrameLayout.LayoutParams(
+                FrameLayout.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
         navButtonParams.apply {
-            gravity = Gravity.END
-            width = FrameLayout.LayoutParams.WRAP_CONTENT
-            height = ViewGroup.LayoutParams.MATCH_PARENT
+            gravity = Gravity.END or Gravity.CENTER_VERTICAL
             marginEnd = navMarginEnd
         }
         navButtonContainer.orientation = LinearLayout.HORIZONTAL
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
index a642693..64cc47c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
@@ -18,6 +18,8 @@
 import android.content.Context;
 import android.view.View;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.dot.DotInfo;
@@ -29,6 +31,7 @@
 import com.android.launcher3.taskbar.TaskbarDragController;
 import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.taskbar.allapps.TaskbarAllAppsContainerView;
+import com.android.launcher3.taskbar.allapps.TaskbarSearchSessionController;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource;
 
 /**
@@ -47,6 +50,8 @@
     private final int mStashedTaskbarHeight;
     private final TaskbarUIController mUiController;
 
+    private @Nullable TaskbarSearchSessionController mSearchSessionController;
+
     public TaskbarOverlayContext(
             Context windowContext,
             TaskbarActivityContext taskbarContext,
@@ -62,6 +67,15 @@
         mUiController = controllers.uiController;
     }
 
+    public @Nullable TaskbarSearchSessionController getSearchSessionController() {
+        return mSearchSessionController;
+    }
+
+    public void setSearchSessionController(
+            @Nullable TaskbarSearchSessionController searchSessionController) {
+        mSearchSessionController = searchSessionController;
+    }
+
     int getStashedTaskbarHeight() {
         return mStashedTaskbarHeight;
     }
@@ -112,6 +126,11 @@
     }
 
     @Override
+    public View.OnLongClickListener getAllAppsItemLongClickListener() {
+        return mDragController::startDragOnLongClick;
+    }
+
+    @Override
     public PopupDataProvider getPopupDataProvider() {
         return mTaskbarContext.getPopupDataProvider();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
index add7279..ff00560 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
@@ -21,6 +21,7 @@
 
 import android.content.Context;
 import android.graphics.Insets;
+import android.media.permission.SafeCloseable;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
@@ -29,6 +30,7 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.app.viewcapture.SettingsAwareViewCapture;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -44,6 +46,7 @@
         BaseDragLayer<TaskbarOverlayContext> implements
         ViewTreeObserver.OnComputeInternalInsetsListener {
 
+    private SafeCloseable mViewCaptureCloseable;
     private final List<OnClickListener> mOnClickListeners = new CopyOnWriteArrayList<>();
     private final TouchController mClickListenerTouchController = new TouchController() {
         @Override
@@ -77,12 +80,15 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+        mViewCaptureCloseable = SettingsAwareViewCapture.getInstance(getContext())
+                .startCapture(getRootView(), ".TaskbarOverlay");
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+        mViewCaptureCloseable.close();
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
index a53dc15..475f465 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -16,13 +16,16 @@
 
 package com.android.launcher3.uioverrides;
 
+import android.app.ActivityOptions;
 import android.app.Person;
 import android.content.Context;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
+import android.window.RemoteTransition;
 
 import com.android.launcher3.Utilities;
+import com.android.quickstep.util.FadeOutRemoteTransition;
 
 import java.util.Map;
 
@@ -41,4 +44,13 @@
     public static Map<String, LauncherActivityInfo> getActivityOverrides(Context context) {
         return context.getSystemService(LauncherApps.class).getActivityOverrides();
     }
+
+    /**
+     * Creates an ActivityOptions to play fade-out animation on closing targets
+     */
+    public static ActivityOptions createFadeOutAnimOptions(Context context) {
+        ActivityOptions options = ActivityOptions.makeBasic();
+        options.setRemoteTransition(new RemoteTransition(new FadeOutRemoteTransition()));
+        return options;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index 955440b..d78ca88 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -16,40 +16,34 @@
 
 package com.android.launcher3.uioverrides;
 
+import static com.android.app.animation.Interpolators.AGGRESSIVE_EASE_IN_OUT;
+import static com.android.app.animation.Interpolators.FINAL_FRAME;
+import static com.android.app.animation.Interpolators.INSTANT;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME;
-import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
-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.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_SPLIT_SELECT_FLOATING_TASK_TRANSLATE_OFFSCREEN;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_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.SKIP_OVERVIEW;
-import static com.android.quickstep.views.FloatingTaskView.PRIMARY_TRANSLATE_OFFSCREEN;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
 import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA;
 
-import android.graphics.Rect;
-import android.graphics.RectF;
 import android.util.FloatProperty;
 import android.view.animation.Interpolator;
 
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.states.StateAnimationConfig;
-import com.android.quickstep.views.FloatingTaskView;
 import com.android.quickstep.views.RecentsView;
 
 /**
@@ -113,48 +107,19 @@
         setter.setFloat(mRecentsView, TASK_SECONDARY_TRANSLATION, 0f,
                 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
 
-        if (mRecentsView.isSplitSelectionActive()) {
-            // TODO (b/238651489): Refactor state management to avoid need for double check
-            FloatingTaskView floatingTask = mRecentsView.getFirstFloatingTaskView();
-            if (floatingTask != null) {
-                // We are in split selection state currently, transitioning to another state
-                DragLayer dragLayer = mLauncher.getDragLayer();
-                RectF onScreenRectF = new RectF();
-                Utilities.getBoundsForViewInDragLayer(mLauncher.getDragLayer(), floatingTask,
-                        new Rect(0, 0, floatingTask.getWidth(), floatingTask.getHeight()),
-                        false, null, onScreenRectF);
-                // Get the part of the floatingTask that intersects with the DragLayer (i.e. the
-                // on-screen portion)
-                onScreenRectF.intersect(
-                        dragLayer.getLeft(),
-                        dragLayer.getTop(),
-                        dragLayer.getRight(),
-                        dragLayer.getBottom()
-                );
-
-                setter.setFloat(
-                        mRecentsView.getFirstFloatingTaskView(),
-                        PRIMARY_TRANSLATE_OFFSCREEN,
-                        mRecentsView.getPagedOrientationHandler()
-                                .getFloatingTaskOffscreenTranslationTarget(
-                                        floatingTask,
-                                        onScreenRectF,
-                                        floatingTask.getStagePosition(),
-                                        mLauncher.getDeviceProfile()
-                                ),
-                        config.getInterpolator(
-                                ANIM_OVERVIEW_SPLIT_SELECT_FLOATING_TASK_TRANSLATE_OFFSCREEN,
-                                LINEAR
-                        ));
-                setter.setViewAlpha(
-                        mRecentsView.getSplitInstructionsView(),
-                        0,
-                        config.getInterpolator(
-                                ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE,
-                                LINEAR
-                        )
-                );
-            }
+        boolean exitingOverview = !FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()
+                && !toState.overviewUi;
+        if (mRecentsView.isSplitSelectionActive() && exitingOverview) {
+            setter.add(mRecentsView.getSplitSelectController().getSplitAnimationController()
+                    .createPlaceholderDismissAnim(mLauncher));
+            setter.setViewAlpha(
+                    mRecentsView.getSplitInstructionsView(),
+                    0,
+                    config.getInterpolator(
+                            ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE,
+                            LINEAR
+                    )
+            );
         }
 
         setter.setFloat(mRecentsView, getContentAlphaProperty(), toState.overviewUi ? 1 : 0,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java b/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java
deleted file mode 100644
index d8aa235..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/**
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides;
-
-import static android.os.IBinder.FLAG_ONEWAY;
-
-import android.os.Binder;
-import android.os.Build;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.RemoteException;
-import android.util.Log;
-
-import androidx.annotation.MainThread;
-
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.function.BiConsumer;
-import java.util.function.Supplier;
-
-/**
- * A binder proxy transaction listener for tracking non-whitelisted binder calls.
- */
-public class DejankBinderTracker implements Binder.ProxyTransactListener {
-    private static final String TAG = "DejankBinderTracker";
-
-    private static final Object sLock = new Object();
-    private static final HashSet<String> sWhitelistedFrameworkClasses = new HashSet<>();
-    static {
-        // Common IPCs that are ok to block the main thread.
-        sWhitelistedFrameworkClasses.add("android.view.IWindowSession");
-        sWhitelistedFrameworkClasses.add("android.os.IPowerManager");
-    }
-    private static boolean sTemporarilyIgnoreTracking = false;
-
-    // Used by the client to limit binder tracking to specific regions
-    private static boolean sTrackingAllowed = false;
-
-    private BiConsumer<String, Integer> mUnexpectedTransactionCallback;
-    private boolean mIsTracking = false;
-
-    /**
-     * Temporarily ignore blocking binder calls for the duration of this {@link Runnable}.
-     */
-    @MainThread
-    public static void whitelistIpcs(Runnable runnable) {
-        sTemporarilyIgnoreTracking = true;
-        runnable.run();
-        sTemporarilyIgnoreTracking = false;
-    }
-
-    /**
-     * Temporarily ignore blocking binder calls for the duration of this {@link Supplier}.
-     */
-    @MainThread
-    public static <T> T whitelistIpcs(Supplier<T> supplier) {
-        sTemporarilyIgnoreTracking = true;
-        T value = supplier.get();
-        sTemporarilyIgnoreTracking = false;
-        return value;
-    }
-
-    /**
-     * Enables binder tracking during a test.
-     */
-    @MainThread
-    public static void allowBinderTrackingInTests() {
-        sTrackingAllowed = true;
-    }
-
-    /**
-     * Disables binder tracking during a test.
-     */
-    @MainThread
-    public static void disallowBinderTrackingInTests() {
-        sTrackingAllowed = false;
-    }
-
-    public DejankBinderTracker(BiConsumer<String, Integer> unexpectedTransactionCallback) {
-        mUnexpectedTransactionCallback = unexpectedTransactionCallback;
-    }
-
-    @MainThread
-    public void startTracking() {
-        if (!Build.TYPE.toLowerCase(Locale.ROOT).contains("debug")
-                && !Build.TYPE.toLowerCase(Locale.ROOT).equals("eng")) {
-            Log.wtf(TAG, "Unexpected use of binder tracker in non-debug build", new Exception());
-            return;
-        }
-        if (mIsTracking) {
-            return;
-        }
-        mIsTracking = true;
-        Binder.setProxyTransactListener(this);
-    }
-
-    @MainThread
-    public void stopTracking() {
-        if (!mIsTracking) {
-            return;
-        }
-        mIsTracking = false;
-        Binder.setProxyTransactListener(null);
-    }
-
-    // Override the hidden Binder#onTransactStarted method
-    public synchronized Object onTransactStarted(IBinder binder, int transactionCode, int flags) {
-        if (!mIsTracking
-                || !sTrackingAllowed
-                || sTemporarilyIgnoreTracking
-                || (flags & FLAG_ONEWAY) == FLAG_ONEWAY
-                || !isMainThread()) {
-            return null;
-        }
-
-        String descriptor;
-        try {
-            descriptor = binder.getInterfaceDescriptor();
-            if (sWhitelistedFrameworkClasses.contains(descriptor)) {
-                return null;
-            }
-        } catch (RemoteException e) {
-            e.printStackTrace();
-            descriptor = binder.getClass().getSimpleName();
-        }
-
-        mUnexpectedTransactionCallback.accept(descriptor, transactionCode);
-        return null;
-    }
-
-    @Override
-    public Object onTransactStarted(IBinder binder, int transactionCode) {
-        // Do nothing
-        return null;
-    }
-
-    @Override
-    public void onTransactEnded(Object session) {
-        // Do nothing
-    }
-
-    public static boolean isMainThread() {
-        return Thread.currentThread() == Looper.getMainLooper().getThread();
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index a8b7698..2064fe2 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -15,7 +15,7 @@
  */
 package com.android.launcher3.uioverrides;
 
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
 import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
 
@@ -189,7 +189,7 @@
         } else {
             float[] hctPlateColor = new float[3];
             ColorUtils.colorToM3HCT(mDotParams.appColor, hctPlateColor);
-            newPlateColor = ColorUtils.M3HCTtoColor(hctPlateColor[0], 36, 85);
+            newPlateColor = ColorUtils.M3HCTToColor(hctPlateColor[0], 36, 85);
         }
 
         if (!animate) {
@@ -260,8 +260,8 @@
                 Keyframe.ofFloat(0.82f, finalTrans - getOutlineOffsetY() / 2f), // Overshoot
                 Keyframe.ofFloat(1f, finalTrans) // Ease back into the final position
         };
-        keyframes[1].setInterpolator(ACCEL_DEACCEL);
-        keyframes[2].setInterpolator(ACCEL_DEACCEL);
+        keyframes[1].setInterpolator(ACCELERATE_DECELERATE);
+        keyframes[2].setInterpolator(ACCELERATE_DECELERATE);
 
         mSlotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this,
                 PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes));
@@ -337,7 +337,6 @@
         if (getTag() instanceof WorkspaceItemInfo) {
             WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
             isBadged = !Process.myUserHandle().equals(info.user)
-                    || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT
                     || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
         }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index eb9332d..5a46b8d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -20,23 +20,20 @@
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE;
 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
 
+import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.launcher3.LauncherSettings.Animation.DEFAULT_NO_ICON;
 import static com.android.launcher3.LauncherSettings.Animation.VIEW_BACKGROUND;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.NO_OFFSET;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE;
-import static com.android.launcher3.config.FeatureFlags.RECEIVE_UNFOLD_EVENTS_FROM_SYSUI;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
 import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
 import static com.android.launcher3.popup.QuickstepSystemShortcut.getSplitSelectShortcutByPosition;
@@ -60,8 +57,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
-import android.animation.ValueAnimator;
-import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
@@ -71,20 +66,20 @@
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.hardware.SensorManager;
-import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.display.DisplayManager;
 import android.media.permission.SafeCloseable;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.CancellationSignal;
 import android.os.IBinder;
 import android.os.SystemProperties;
 import android.os.Trace;
+import android.util.AttributeSet;
+import android.util.Log;
 import android.view.Display;
 import android.view.HapticFeedbackConstants;
-import android.view.RemoteAnimationTarget;
 import android.view.View;
+import android.widget.AnalogClock;
+import android.widget.TextClock;
 import android.window.BackEvent;
 import android.window.OnBackAnimationCallback;
 import android.window.OnBackInvokedDispatcher;
@@ -111,7 +106,6 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.appprediction.PredictionRowView;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.hybridhotseat.HotseatPredictionController;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager;
@@ -119,6 +113,7 @@
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.WellbeingModel;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.proxy.ProxyActivityStarter;
 import com.android.launcher3.statehandlers.DepthController;
@@ -160,12 +155,10 @@
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.TouchInteractionService.TISBinder;
+import com.android.quickstep.util.AsyncClockEventDelegate;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.LauncherUnfoldAnimationController;
-import com.android.quickstep.util.ProxyScreenStatusProvider;
 import com.android.quickstep.util.QuickstepOnboardingPrefs;
-import com.android.quickstep.util.RemoteAnimationProvider;
-import com.android.quickstep.util.RemoteFadeOutAnimationListener;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.util.SplitToWorkspaceController;
 import com.android.quickstep.util.SplitWithKeyboardShortcutController;
@@ -175,16 +168,14 @@
 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;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.unfold.RemoteUnfoldSharedComponent;
-import com.android.systemui.unfold.UnfoldSharedComponent;
 import com.android.systemui.unfold.UnfoldTransitionFactory;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
 import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig;
 import com.android.systemui.unfold.config.UnfoldTransitionConfig;
 import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver;
-import com.android.systemui.unfold.system.ActivityManagerActivityTypeProvider;
-import com.android.systemui.unfold.system.DeviceStateManagerFoldProvider;
 import com.android.systemui.unfold.updates.RotationChangeProvider;
 
 import java.io.FileDescriptor;
@@ -194,13 +185,17 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 public class QuickstepLauncher extends Launcher {
+    private static final boolean TRACE_LAYOUTS =
+            SystemProperties.getBoolean("persist.debug.trace_layouts", false);
+    private static final String TRACE_RELAYOUT_CLASS =
+            SystemProperties.get("persist.debug.trace_request_layout_class", null);
 
-    public static final boolean ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
-            SystemProperties.getBoolean("persist.wm.debug.enable_pip_keep_clear_algorithm", true);
+    private static final String TAG = "QuickstepLauncher";
 
     public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false;
 
@@ -213,11 +208,8 @@
     private QuickstepTransitionManager mAppTransitionManager;
     private OverviewActionsView mActionsView;
     private TISBindHelper mTISBindHelper;
-    private @Nullable TaskbarManager mTaskbarManager;
-    private @Nullable OverviewCommandHelper mOverviewCommandHelper;
     private @Nullable LauncherTaskbarUIController mTaskbarUIController;
     // Will be updated when dragging from taskbar.
-    private @Nullable DragOptions mNextWorkspaceDragOptions = null;
     private @Nullable UnfoldTransitionProgressProvider mUnfoldTransitionProgressProvider;
     private @Nullable LauncherUnfoldAnimationController mLauncherUnfoldAnimationController;
 
@@ -225,6 +217,8 @@
     private SplitWithKeyboardShortcutController mSplitWithKeyboardShortcutController;
     private SplitToWorkspaceController mSplitToWorkspaceController;
 
+    private AsyncClockEventDelegate mAsyncClockEventDelegate;
+
     /**
      * If Launcher restarted while in the middle of an Overview split select, it needs this data to
      * recover. In all other cases this will remain null.
@@ -260,6 +254,10 @@
         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
         mDepthController = new DepthController(this);
         mDesktopVisibilityController = new DesktopVisibilityController(this);
+        if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) {
+            mDesktopVisibilityController.registerSystemUiListener();
+            mSplitSelectStateController.initSplitFromDesktopController(this);
+        }
         mHotseatPredictionController = new HotseatPredictionController(this);
 
         mEnableWidgetDepth = SystemProperties.getBoolean("ro.launcher.depth.widget", true);
@@ -281,7 +279,6 @@
 
         if (mAllAppsPredictions != null
                 && (info.itemType == ITEM_TYPE_APPLICATION
-                || info.itemType == ITEM_TYPE_SHORTCUT
                 || info.itemType == ITEM_TYPE_DEEP_SHORTCUT)) {
             int count = mAllAppsPredictions.items.size();
             for (int i = 0; i < count; i++) {
@@ -351,9 +348,7 @@
         mHotseatPredictionController.setPauseUIUpdate(getTaskbarUIController() == null);
         RunnableList result = super.startActivitySafely(v, intent, item);
         if (result == null) {
-            if (getTaskbarUIController() == null) {
-                mHotseatPredictionController.setPauseUIUpdate(false);
-            }
+            mHotseatPredictionController.setPauseUIUpdate(false);
         } else {
             result.add(() -> mHotseatPredictionController.setPauseUIUpdate(false));
         }
@@ -408,8 +403,7 @@
     }
 
     private List<SystemShortcut.Factory<QuickstepLauncher>> getSplitShortcuts() {
-
-        if (!ENABLE_SPLIT_FROM_WORKSPACE.get() || !mDeviceProfile.isTablet) {
+        if (!mDeviceProfile.isTablet || mSplitSelectStateController.isSplitSelectActive()) {
             return Collections.emptyList();
         }
         RecentsView recentsView = getOverviewPanel();
@@ -437,12 +431,8 @@
             boolean visible = (state == NORMAL || state == OVERVIEW)
                     && (willUserBeActive || isUserActive())
                     && !profile.isVerticalBarLayout();
-            if (ENABLE_PIP_KEEP_CLEAR_ALGORITHM)  {
-                SystemUiProxy.INSTANCE.get(this)
-                        .setLauncherKeepClearAreaHeight(visible, profile.hotseatBarSizePx);
-            } else {
-                SystemUiProxy.INSTANCE.get(this).setShelfHeight(visible, profile.hotseatBarSizePx);
-            }
+            SystemUiProxy.INSTANCE.get(this)
+                    .setLauncherKeepClearAreaHeight(visible, profile.hotseatBarSizePx);
         }
         if (state == NORMAL && !inTransition) {
             ((RecentsView) getOverviewPanel()).setSwipeDownShouldLaunchApp(false);
@@ -451,6 +441,7 @@
 
     @Override
     public void bindExtraContainerItems(FixedContainerItems item) {
+        Log.d(TAG, "Bind extra container items");
         if (item.containerId == Favorites.CONTAINER_PREDICTION) {
             mAllAppsPredictions = item;
             PredictionRowView<?> predictionRowView =
@@ -458,6 +449,7 @@
                             PredictionRowView.class);
             predictionRowView.setPredictedApps(item.items);
         } else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+            Log.d(TAG, "Bind extra container item is hotseat prediction");
             mHotseatPredictionController.setPredictedItems(item);
         } else if (item.containerId == Favorites.CONTAINER_WIDGETS_PREDICTION) {
             getPopupDataProvider().setRecommendedWidgets(item.items);
@@ -474,22 +466,28 @@
     public void onDestroy() {
         mAppTransitionManager.onActivityDestroyed();
         if (mUnfoldTransitionProgressProvider != null) {
-            if (FeatureFlags.RECEIVE_UNFOLD_EVENTS_FROM_SYSUI.get()) {
-                SystemUiProxy.INSTANCE.get(this).setUnfoldAnimationListener(null);
-            }
-
+            SystemUiProxy.INSTANCE.get(this).setUnfoldAnimationListener(null);
             mUnfoldTransitionProgressProvider.destroy();
         }
 
         mTISBindHelper.onDestroy();
-        if (mTaskbarManager != null) {
-            mTaskbarManager.clearActivity(this);
-        }
 
         if (mLauncherUnfoldAnimationController != null) {
             mLauncherUnfoldAnimationController.onDestroy();
         }
 
+        if (mDesktopVisibilityController != null) {
+            mDesktopVisibilityController.unregisterSystemUiListener();
+        }
+
+        if (mSplitSelectStateController != null) {
+            mSplitSelectStateController.onDestroy();
+        }
+
+        if (mAsyncClockEventDelegate != null) {
+            mAsyncClockEventDelegate.onDestroy();
+        }
+
         super.onDestroy();
         mHotseatPredictionController.destroy();
         mSplitWithKeyboardShortcutController.onDestroy();
@@ -523,7 +521,7 @@
             }
             case QUICK_SWITCH_STATE_ORDINAL: {
                 RecentsView rv = getOverviewPanel();
-                TaskView tasktolaunch = rv.getTaskViewAt(0);
+                TaskView tasktolaunch = rv.getCurrentPageTaskView();
                 if (tasktolaunch != null) {
                     tasktolaunch.launchTask(success -> {
                         if (!success) {
@@ -547,11 +545,14 @@
 
         ArrayList<TouchController> list = new ArrayList<>();
         list.add(getDragController());
+        Consumer<AnimatorSet> splitAnimator = animatorSet ->
+                animatorSet.play(mSplitSelectStateController.getSplitAnimationController()
+                        .createPlaceholderDismissAnim(this));
         switch (mode) {
             case NO_BUTTON:
                 list.add(new NoButtonQuickSwitchTouchController(this));
-                list.add(new NavBarToHomeTouchController(this));
-                list.add(new NoButtonNavbarToOverviewTouchController(this));
+                list.add(new NavBarToHomeTouchController(this, splitAnimator));
+                list.add(new NoButtonNavbarToOverviewTouchController(this, splitAnimator));
                 break;
             case TWO_BUTTONS:
                 list.add(new TwoButtonNavbarTouchController(this));
@@ -561,8 +562,14 @@
                 list.add(new PortraitStatesTouchController(this));
                 break;
             case THREE_BUTTONS:
+                list.add(new NoButtonQuickSwitchTouchController(this));
+                list.add(new NavBarToHomeTouchController(this, splitAnimator));
+                list.add(new NoButtonNavbarToOverviewTouchController(this, splitAnimator));
+                list.add(new PortraitStatesTouchController(this));
+                break;
             default:
                 list.add(new PortraitStatesTouchController(this));
+                break;
         }
 
         if (!getDeviceProfile().isMultiWindowMode) {
@@ -603,6 +610,8 @@
             mViewCapture = SettingsAwareViewCapture.getInstance(this).startCapture(getWindow());
         }
         getWindow().addPrivateFlags(PRIVATE_FLAG_OPTIMIZE_MEASURE);
+        View.setTraceLayoutSteps(TRACE_LAYOUTS);
+        View.setTracedRequestLayoutClassClass(TRACE_RELAYOUT_CLASS);
     }
 
     @Override
@@ -610,12 +619,14 @@
         RecentsView recentsView = getOverviewPanel();
         // Check if there is already an instance of this app running, if so, initiate the split
         // using that.
-        mSplitSelectStateController.findLastActiveTaskAndRunCallback(
-                splitSelectSource.itemInfo.getComponentKey(),
-                foundTask -> {
-                    splitSelectSource.alreadyRunningTaskId = foundTask == null
-                            ? INVALID_TASK_ID
-                            : foundTask.key.id;
+        mSplitSelectStateController.findLastActiveTasksAndRunCallback(
+                Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
+                foundTasks -> {
+                    @Nullable Task foundTask = foundTasks.get(0);
+                    boolean taskWasFound = foundTask != null;
+                    splitSelectSource.alreadyRunningTaskId = taskWasFound
+                            ? foundTask.key.id
+                            : INVALID_TASK_ID;
                     if (ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
                         startSplitToHome(splitSelectSource);
                     } else {
@@ -655,9 +666,13 @@
             @Override
             public void onAnimationCancel(Animator animation) {
                 getDragLayer().removeView(floatingTaskView);
+                mSplitSelectStateController.getSplitAnimationController()
+                        .removeSplitInstructionsView(QuickstepLauncher.this);
                 mSplitSelectStateController.resetState();
             }
         });
+        anim.add(mSplitSelectStateController.getSplitAnimationController()
+                .getShowSplitInstructionsAnim(this).buildAnim());
         anim.buildAnim().start();
     }
 
@@ -683,9 +698,9 @@
     @Override
     protected void onNewIntent(Intent intent) {
         super.onNewIntent(intent);
-
-        if (mOverviewCommandHelper != null) {
-            mOverviewCommandHelper.clearPendingCommands();
+        OverviewCommandHelper overviewCommandHelper = mTISBindHelper.getOverviewCommandHelper();
+        if (overviewCommandHelper != null) {
+            overviewCommandHelper.clearPendingCommands();
         }
     }
 
@@ -808,8 +823,9 @@
     }
 
     private void onTaskbarInAppDisplayProgressUpdate(float progress, int flag) {
-        if (mTaskbarManager == null
-                || mTaskbarManager.getCurrentActivityContext() == null
+        TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager();
+        if (taskbarManager == null
+                || taskbarManager.getCurrentActivityContext() == null
                 || mTaskbarUIController == null) {
             return;
         }
@@ -853,7 +869,7 @@
         if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) {
             DesktopVisibilityController controller = mDesktopVisibilityController;
             if (controller != null && controller.areFreeformTasksVisible()
-                    && !controller.isGestureInProgress()) {
+                    && !controller.isRecentsGestureInProgress()) {
                 // Return early to skip setting activity to appear as resumed
                 // TODO(b/255649902): shouldn't be needed when we have a separate launcher state
                 //  for desktop that we can use to control other parts of launcher
@@ -881,11 +897,10 @@
     }
 
     private void onTISConnected(TISBinder binder) {
-        mTaskbarManager = binder.getTaskbarManager();
-        if (mTaskbarManager != null) {
-            mTaskbarManager.setActivity(this);
+        TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager();
+        if (taskbarManager != null) {
+            taskbarManager.setActivity(this);
         }
-        mOverviewCommandHelper = binder.getOverviewCommandHelper();
     }
 
     @Override
@@ -896,43 +911,10 @@
     private void initUnfoldTransitionProgressProvider() {
         final UnfoldTransitionConfig config = new ResourceUnfoldTransitionConfig();
         if (config.isEnabled()) {
-            if (RECEIVE_UNFOLD_EVENTS_FROM_SYSUI.get()) {
-                initRemotelyCalculatedUnfoldAnimation(config);
-            } else {
-                initLocallyCalculatedUnfoldAnimation(config);
-            }
-
+            initRemotelyCalculatedUnfoldAnimation(config);
         }
     }
 
-    /** Registers hinge angle listener and calculates the animation progress in this process. */
-    private void initLocallyCalculatedUnfoldAnimation(UnfoldTransitionConfig config) {
-        UnfoldSharedComponent unfoldComponent =
-                UnfoldTransitionFactory.createUnfoldSharedComponent(
-                        /* context= */ this,
-                        config,
-                        ProxyScreenStatusProvider.INSTANCE,
-                        new DeviceStateManagerFoldProvider(
-                                getSystemService(DeviceStateManager.class), /* context= */ this),
-                        new ActivityManagerActivityTypeProvider(
-                                getSystemService(ActivityManager.class)),
-                        getSystemService(SensorManager.class),
-                        getMainThreadHandler(),
-                        getMainExecutor(),
-                        /* backgroundExecutor= */ UI_HELPER_EXECUTOR,
-                        /* tracingTagPrefix= */ "launcher",
-                        getSystemService(DisplayManager.class)
-                );
-
-        mUnfoldTransitionProgressProvider = unfoldComponent.getUnfoldTransitionProvider()
-                .orElseThrow(() -> new IllegalStateException(
-                        "Trying to create UnfoldTransitionProgressProvider when the "
-                                + "transition is disabled"));
-
-        initUnfoldAnimationController(mUnfoldTransitionProgressProvider,
-                unfoldComponent.getRotationChangeProvider());
-    }
-
     /** Receives animation progress from sysui process. */
     private void initRemotelyCalculatedUnfoldAnimation(UnfoldTransitionConfig config) {
         RemoteUnfoldSharedComponent unfoldComponent =
@@ -982,6 +964,13 @@
         return mSplitToWorkspaceController;
     }
 
+    @Override
+    protected void handleSplitAnimationGoingToHome() {
+        super.handleSplitAnimationGoingToHome();
+        mSplitSelectStateController.getSplitAnimationController()
+                .playPlaceholderDismissAnim(this);
+    }
+
     public <T extends OverviewActionsView> T getActionsView() {
         return (T) mActionsView;
     }
@@ -1018,41 +1007,6 @@
     }
 
     @Override
-    public DragOptions getDefaultWorkspaceDragOptions() {
-        if (mNextWorkspaceDragOptions != null) {
-            DragOptions options = mNextWorkspaceDragOptions;
-            mNextWorkspaceDragOptions = null;
-            return options;
-        }
-        return super.getDefaultWorkspaceDragOptions();
-    }
-
-    public void setNextWorkspaceDragOptions(DragOptions dragOptions) {
-        mNextWorkspaceDragOptions = dragOptions;
-    }
-
-    @Override
-    public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) {
-        QuickstepTransitionManager appTransitionManager = getAppTransitionManager();
-        appTransitionManager.setRemoteAnimationProvider(new RemoteAnimationProvider() {
-            @Override
-            public AnimatorSet createWindowAnimation(RemoteAnimationTarget[] appTargets,
-                    RemoteAnimationTarget[] wallpaperTargets) {
-
-                // On the first call clear the reference.
-                signal.cancel();
-
-                ValueAnimator fadeAnimation = ValueAnimator.ofFloat(1, 0);
-                fadeAnimation.addUpdateListener(new RemoteFadeOutAnimationListener(appTargets,
-                        wallpaperTargets));
-                AnimatorSet anim = new AnimatorSet();
-                anim.play(fadeAnimation);
-                return anim;
-            }
-        }, signal);
-    }
-
-    @Override
     public float[] getNormalOverviewScaleAndOffset() {
         return DisplayController.getNavigationMode(this).hasGestures
                 ? new float[] {1, 1} : new float[] {1.1f, NO_OFFSET};
@@ -1102,6 +1056,8 @@
         activityOptions.options.setLaunchDisplayId(
                 (v != null && v.getDisplay() != null) ? v.getDisplay().getDisplayId()
                         : Display.DEFAULT_DISPLAY);
+        activityOptions.options.setPendingIntentBackgroundActivityStartMode(
+                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
         addLaunchCookie(item, activityOptions.options);
         return activityOptions;
     }
@@ -1114,6 +1070,8 @@
                 Executors.MAIN_EXECUTOR.getHandler(), null,
                 elapsedRealTime -> callbacks.executeAllAndDestroy());
         options.setSplashScreenStyle(splashScreenStyle);
+        options.setPendingIntentBackgroundActivityStartMode(
+                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
         return new ActivityOptionsWrapper(options, callbacks);
     }
 
@@ -1161,7 +1119,6 @@
         }
         switch (info.itemType) {
             case Favorites.ITEM_TYPE_APPLICATION:
-            case Favorites.ITEM_TYPE_SHORTCUT:
             case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
             case Favorites.ITEM_TYPE_APPWIDGET:
                 // Fall through and continue if it's an app, shortcut, or widget
@@ -1270,8 +1227,9 @@
         Trace.instantForTrack(TRACE_TAG_APP, "QuickstepLauncher#DeviceProfileChanged",
                 getDeviceProfile().toSmallString());
         SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx);
-        if (mTaskbarManager != null) {
-            mTaskbarManager.debugWhyTaskbarNotDestroyed("QuickstepLauncher#onDeviceProfileChanged");
+        TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager();
+        if (taskbarManager != null) {
+            taskbarManager.debugWhyTaskbarNotDestroyed("QuickstepLauncher#onDeviceProfileChanged");
         }
     }
 
@@ -1293,7 +1251,7 @@
                 groupTask.task1.key.id,
                 groupTask.task2.key.id,
                 SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT,
-                /* callback= */ success -> {},
+                /* callback= */ success -> mSplitSelectStateController.resetState(),
                 /* freezeTaskList= */ true,
                 groupTask.mSplitBounds == null
                         ? DEFAULT_SPLIT_RATIO
@@ -1302,6 +1260,18 @@
                                 : groupTask.mSplitBounds.leftTaskPercent);
     }
 
+    /**
+     * Launches two apps as an app pair.
+     */
+    public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
+        mSplitSelectStateController.getAppPairsController().launchAppPair(app1, app2);
+    }
+
+    public boolean canStartHomeSafely() {
+        OverviewCommandHelper overviewCommandHelper = mTISBindHelper.getOverviewCommandHelper();
+        return overviewCommandHelper == null || overviewCommandHelper.canStartHomeSafely();
+    }
+
     private static final class LauncherTaskViewController extends
             TaskViewTouchController<Launcher> {
 
@@ -1341,5 +1311,31 @@
         if (mAppTransitionManager != null) {
             mAppTransitionManager.dump(prefix + "\t" + RING_APPEAR_ANIMATION_PREFIX, writer);
         }
+        if (mHotseatPredictionController != null) {
+            mHotseatPredictionController.dump(prefix, writer);
+        }
+    }
+
+    @Override
+    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
+        switch (name) {
+            case "TextClock", "android.widget.TextClock" -> {
+                TextClock tc = new TextClock(context, attrs);
+                if (mAsyncClockEventDelegate == null) {
+                    mAsyncClockEventDelegate = new AsyncClockEventDelegate(this);
+                }
+                tc.setClockEventDelegate(mAsyncClockEventDelegate);
+                return tc;
+            }
+            case "AnalogClock", "android.widget.AnalogClock" -> {
+                AnalogClock ac = new AnalogClock(context, attrs);
+                if (mAsyncClockEventDelegate == null) {
+                    mAsyncClockEventDelegate = new AsyncClockEventDelegate(this);
+                }
+                ac.setClockEventDelegate(mAsyncClockEventDelegate);
+                return ac;
+            }
+        }
+        return super.onCreateView(parent, name, context, attrs);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
index 39543b0..f7bef03 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
@@ -243,6 +243,7 @@
         } else {
             widgetView = new LauncherAppWidgetHostView(context);
         }
+        widgetView.setIsWidgetCachingDisabled(true);
         widgetView.setInteractionHandler(mInteractionHandler);
         widgetView.setAppWidget(appWidgetId, appWidget);
         mViews.put(appWidgetId, widgetView);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index f16b43d..23e922c 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -15,10 +15,10 @@
  */
 package com.android.launcher3.uioverrides;
 
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON;
 import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java
index b901a87..a76eb43 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java
@@ -23,6 +23,7 @@
 import static android.view.View.GONE;
 import static android.view.View.VISIBLE;
 
+import static com.android.launcher3.LauncherPrefs.ALL_APPS_OVERVIEW_THRESHOLD;
 import static com.android.launcher3.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY;
 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.PLUGIN_CHANGED;
 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.pluginEnabledKey;
@@ -59,6 +60,7 @@
 import androidx.preference.PreferenceGroup;
 import androidx.preference.PreferenceScreen;
 import androidx.preference.PreferenceViewHolder;
+import androidx.preference.SeekBarPreference;
 import androidx.preference.SwitchPreference;
 
 import com.android.launcher3.LauncherPrefs;
@@ -106,6 +108,9 @@
         loadPluginPrefs();
         maybeAddSandboxCategory();
         addOnboardingPrefsCatergory();
+        if (FeatureFlags.ENABLE_ALL_APPS_FROM_OVERVIEW.get()) {
+            addAllAppsFromOverviewCatergory();
+        }
 
         if (getActivity() != null) {
             getActivity().setTitle("Developer Options");
@@ -393,6 +398,33 @@
         }
     }
 
+    private void addAllAppsFromOverviewCatergory() {
+        PreferenceCategory category = newCategory("All Apps from Overview Config");
+
+        SeekBarPreference thresholdPref = new SeekBarPreference(getContext());
+        thresholdPref.setTitle("Threshold to open All Apps from Overview");
+        thresholdPref.setSingleLineTitle(false);
+
+        // These values are 100x swipe up shift value (100 = where overview sits).
+        thresholdPref.setMax(500);
+        thresholdPref.setMin(105);
+        thresholdPref.setUpdatesContinuously(true);
+        thresholdPref.setIconSpaceReserved(false);
+        // Don't directly save to shared prefs, use LauncherPrefs instead.
+        thresholdPref.setPersistent(false);
+        thresholdPref.setOnPreferenceChangeListener((preference, newValue) -> {
+            LauncherPrefs.get(getContext()).put(ALL_APPS_OVERVIEW_THRESHOLD, newValue);
+            preference.setSummary(String.valueOf((int) newValue / 100f));
+            return true;
+        });
+        int value = LauncherPrefs.get(getContext()).get(ALL_APPS_OVERVIEW_THRESHOLD);
+        thresholdPref.setValue(value);
+        // For some reason the initial value is not triggering the summary update, so call manually.
+        thresholdPref.getOnPreferenceChangeListener().onPreferenceChange(thresholdPref, value);
+
+        category.addPreference(thresholdPref);
+    }
+
     private String toName(String action) {
         String str = action.replace("com.android.systemui.action.PLUGIN_", "")
                 .replace("com.android.launcher3.action.PLUGIN_", "");
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index 2a42175..ed0a0d5 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -15,14 +15,16 @@
  */
 package com.android.launcher3.uioverrides.states;
 
-import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.app.animation.Interpolators.DECELERATE_2;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS;
 
 import android.content.Context;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
 
@@ -91,7 +93,7 @@
     @Override
     public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
         PageAlphaProvider superPageAlphaProvider = super.getWorkspacePageAlphaProvider(launcher);
-        return new PageAlphaProvider(DEACCEL_2) {
+        return new PageAlphaProvider(DECELERATE_2) {
             @Override
             public float getPageAlpha(int pageIndex) {
                 return launcher.getDeviceProfile().isTablet
@@ -103,14 +105,52 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        // Don't add HOTSEAT_ICONS for non-tablets in ALL_APPS state.
-        return launcher.getDeviceProfile().isTablet ? ALL_APPS_CONTENT | HOTSEAT_ICONS
-                : ALL_APPS_CONTENT;
+        int elements = ALL_APPS_CONTENT | FLOATING_SEARCH_BAR;
+        // Only add HOTSEAT_ICONS for tablets in ALL_APPS state.
+        if (launcher.getDeviceProfile().isTablet) {
+            elements |= HOTSEAT_ICONS;
+        }
+        return elements;
+    }
+
+    @Override
+    public int getFloatingSearchBarRestingMarginBottom(Launcher launcher) {
+        return 0;
+    }
+
+    @Override
+    public int getFloatingSearchBarRestingMarginStart(Launcher launcher) {
+        DeviceProfile dp = launcher.getDeviceProfile();
+        return dp.allAppsLeftRightMargin + dp.getAllAppsIconStartMargin();
+    }
+
+    @Override
+    public int getFloatingSearchBarRestingMarginEnd(Launcher launcher) {
+        DeviceProfile dp = launcher.getDeviceProfile();
+        return dp.allAppsLeftRightMargin + dp.getAllAppsIconStartMargin();
+    }
+
+    @Override
+    public boolean shouldFloatingSearchBarUsePillWhenUnfocused(Launcher launcher) {
+        DeviceProfile dp = launcher.getDeviceProfile();
+        return dp.isPhone && !dp.isLandscape;
     }
 
     @Override
     public LauncherState getHistoryForState(LauncherState previousState) {
-        return previousState == OVERVIEW ? OVERVIEW : NORMAL;
+        return previousState == BACKGROUND_APP ? QUICK_SWITCH_FROM_HOME
+                : previousState == OVERVIEW ? OVERVIEW : NORMAL;
+    }
+
+    @Override
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        if (!FeatureFlags.ENABLE_ALL_APPS_FROM_OVERVIEW.get()) {
+            return super.getOverviewScaleAndOffset(launcher);
+        }
+        // This handles the case of returning to the previous app from Overview -> All Apps gesture.
+        // This is the start scale/offset of overview that will be used for that transition.
+        // TODO (b/283336332): Translate in Y direction (ideally with overview resistance).
+        return new float[] {0.5f /* scale */, NO_OFFSET};
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 214679a..396d0ab 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -15,7 +15,7 @@
  */
 package com.android.launcher3.uioverrides.states;
 
-import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.app.animation.Interpolators.DECELERATE_2;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
 
 import android.content.Context;
@@ -97,7 +97,7 @@
 
     @Override
     public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
-        return new PageAlphaProvider(DEACCEL_2) {
+        return new PageAlphaProvider(DECELERATE_2) {
             @Override
             public float getPageAlpha(int pageIndex) {
                 return 0;
@@ -107,7 +107,32 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        return CLEAR_ALL_BUTTON | OVERVIEW_ACTIONS;
+        int elements = CLEAR_ALL_BUTTON | OVERVIEW_ACTIONS;
+        DeviceProfile dp = launcher.getDeviceProfile();
+        boolean showFloatingSearch;
+        if (dp.isPhone) {
+            // Only show search in phone overview in portrait mode.
+            showFloatingSearch = !dp.isLandscape;
+        } else {
+            // Only show search in tablet overview if taskbar is not visible.
+            showFloatingSearch = !dp.isTaskbarPresent || isTaskbarStashed(launcher);
+        }
+        if (showFloatingSearch) {
+            elements |= FLOATING_SEARCH_BAR;
+        }
+        return elements;
+    }
+
+    @Override
+    public int getFloatingSearchBarRestingMarginBottom(Launcher launcher) {
+        return areElementsVisible(launcher, FLOATING_SEARCH_BAR) ? 0
+                : super.getFloatingSearchBarRestingMarginBottom(launcher);
+    }
+
+    @Override
+    public boolean shouldFloatingSearchBarUsePillWhenUnfocused(Launcher launcher) {
+        DeviceProfile dp = launcher.getDeviceProfile();
+        return dp.isPhone && !dp.isLandscape;
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index a8d7538..6651c73 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -17,6 +17,20 @@
 
 import static android.view.View.VISIBLE;
 
+import static com.android.app.animation.Interpolators.ACCELERATE;
+import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
+import static com.android.app.animation.Interpolators.DECELERATE;
+import static com.android.app.animation.Interpolators.DECELERATE_1_7;
+import static com.android.app.animation.Interpolators.DECELERATE_3;
+import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE;
+import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
+import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.app.animation.Interpolators.FINAL_FRAME;
+import static com.android.app.animation.Interpolators.INSTANT;
+import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.OVERSHOOT_0_75;
+import static com.android.app.animation.Interpolators.OVERSHOOT_1_2;
+import static com.android.app.animation.Interpolators.clampToProgress;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.HINT_STATE;
 import static com.android.launcher3.LauncherState.HINT_STATE_TWO_BUTTON;
@@ -25,20 +39,6 @@
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
 import static com.android.launcher3.QuickstepTransitionManager.TASKBAR_TO_HOME_DURATION;
 import static com.android.launcher3.WorkspaceStateTransitionAnimation.getWorkspaceSpringScaleAnimator;
-import static com.android.launcher3.anim.Interpolators.ACCEL;
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED_ACCELERATE;
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED_DECELERATE;
-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_0_75;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
-import static com.android.launcher3.anim.Interpolators.clampToProgress;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
@@ -55,12 +55,14 @@
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 
 import android.animation.ValueAnimator;
+import android.util.Log;
 
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.touch.AllAppsSwipeController;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.DisplayController;
@@ -94,7 +96,8 @@
     @Override
     public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState,
             StateAnimationConfig config) {
-
+        Log.d(TestProtocol.OVERVIEW_OVER_HOME, "creating animation fromState: "
+                + fromState + " toState: " + toState);
         RecentsView overview = mActivity.getOverviewPanel();
         if ((fromState == OVERVIEW || fromState == OVERVIEW_SPLIT_SELECT) && toState == NORMAL) {
             overview.switchToScreenshot(() ->
@@ -112,8 +115,8 @@
                     fromState == OVERVIEW_SPLIT_SELECT
                             ? clampToProgress(LINEAR, 0.33f, 1)
                             : LINEAR);
-            config.setInterpolator(ANIM_WORKSPACE_SCALE, DEACCEL);
-            config.setInterpolator(ANIM_WORKSPACE_FADE, ACCEL);
+            config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATE);
+            config.setInterpolator(ANIM_WORKSPACE_FADE, ACCELERATE);
 
             if (DisplayController.getNavigationMode(mActivity).hasGestures
                     && overview.getTaskViewCount() > 0) {
@@ -139,9 +142,9 @@
                 }
                 overview.snapToPage(DEFAULT_PAGE, Math.toIntExact(config.duration));
             } else {
-                config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, ACCEL_DEACCEL);
-                config.setInterpolator(ANIM_OVERVIEW_SCALE, clampToProgress(ACCEL, 0, 0.9f));
-                config.setInterpolator(ANIM_OVERVIEW_FADE, DEACCEL_1_7);
+                config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, ACCELERATE_DECELERATE);
+                config.setInterpolator(ANIM_OVERVIEW_SCALE, clampToProgress(ACCELERATE, 0, 0.9f));
+                config.setInterpolator(ANIM_OVERVIEW_FADE, DECELERATE_1_7);
             }
 
             Workspace<?> workspace = mActivity.getWorkspace();
@@ -167,8 +170,8 @@
                 || fromState == HINT_STATE_TWO_BUTTON) && toState == OVERVIEW) {
             if (DisplayController.getNavigationMode(mActivity).hasGestures) {
                 config.setInterpolator(ANIM_WORKSPACE_SCALE,
-                        fromState == NORMAL ? ACCEL : OVERSHOOT_1_2);
-                config.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL);
+                        fromState == NORMAL ? ACCELERATE : OVERSHOOT_1_2);
+                config.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCELERATE);
 
                 // Scrolling in tasks, so show straight away
                 if (overview.getTaskViewCount() > 0) {
@@ -196,7 +199,7 @@
             config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_2);
             config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, OVERSHOOT_1_2);
         } else if (fromState == HINT_STATE && toState == NORMAL) {
-            config.setInterpolator(ANIM_DEPTH, DEACCEL_3);
+            config.setInterpolator(ANIM_DEPTH, DECELERATE_3);
             if (mHintToNormalDuration == -1) {
                 ValueAnimator va = getWorkspaceSpringScaleAnimator(mActivity,
                         mActivity.getWorkspace(),
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index 8cbd6e8..b266bcd 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -15,19 +15,22 @@
  */
 package com.android.launcher3.uioverrides.touchcontrollers;
 
+import static com.android.app.animation.Interpolators.DECELERATE_3;
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PULL_BACK_ALPHA;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PULL_BACK_TRANSLATION;
 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
+import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
+import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.view.MotionEvent;
 import android.view.animation.Interpolator;
@@ -41,24 +44,29 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.TouchController;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.OverviewToHomeAnim;
 import com.android.quickstep.views.RecentsView;
 
+import java.util.function.Consumer;
+
 /**
  * Handles swiping up on the nav bar to go home from launcher, e.g. overview or all apps.
  */
 public class NavBarToHomeTouchController implements TouchController,
         SingleAxisSwipeDetector.Listener {
 
-    private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL_3;
+    private static final Interpolator PULLBACK_INTERPOLATOR = DECELERATE_3;
     // The min amount of overview scrim we keep during the transition.
     private static final float OVERVIEW_TO_HOME_SCRIM_MULTIPLIER = 0.5f;
 
     private final Launcher mLauncher;
+    private final Consumer<AnimatorSet> mCancelSplitRunnable;
     private final SingleAxisSwipeDetector mSwipeDetector;
     private final float mPullbackDistance;
 
@@ -67,8 +75,14 @@
     private LauncherState mEndState = NORMAL;
     private AnimatorPlaybackController mCurrentAnimation;
 
-    public NavBarToHomeTouchController(Launcher launcher) {
+    /**
+     * @param cancelSplitRunnable Called when split placeholder view needs to be cancelled.
+     *                            Animation should be added to the provided AnimatorSet
+     */
+    public NavBarToHomeTouchController(Launcher launcher,
+            Consumer<AnimatorSet> cancelSplitRunnable) {
         mLauncher = launcher;
+        mCancelSplitRunnable = cancelSplitRunnable;
         mSwipeDetector = new SingleAxisSwipeDetector(mLauncher, this,
                 SingleAxisSwipeDetector.VERTICAL);
         mPullbackDistance = mLauncher.getResources().getDimension(R.dimen.home_pullback_distance);
@@ -95,6 +109,10 @@
     }
 
     private boolean canInterceptTouch(MotionEvent ev) {
+        if (!isTrackpadMotionEvent(ev) && DisplayController.getNavigationMode(mLauncher)
+                == THREE_BUTTONS) {
+            return false;
+        }
         boolean cameFromNavBar = (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0;
         if (!cameFromNavBar) {
             return false;
@@ -176,7 +194,10 @@
             recentsView.switchToScreenshot(null,
                     () -> recentsView.finishRecentsAnimation(true /* toRecents */, null));
             if (mStartState.overviewUi) {
-                new OverviewToHomeAnim(mLauncher, () -> onSwipeInteractionCompleted(mEndState))
+                new OverviewToHomeAnim(mLauncher, () -> onSwipeInteractionCompleted(mEndState),
+                        FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()
+                                ? mCancelSplitRunnable
+                                : null)
                         .animateWithVelocity(velocity);
             } else {
                 mLauncher.getStateManager().goToState(mEndState, true,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
index b7bafd8..ca598c8 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -16,19 +16,22 @@
 
 package com.android.launcher3.uioverrides.touchcontrollers;
 
+import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR;
 import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
 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.MotionEventsUtils.isTrackpadMotionEvent;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 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 android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.graphics.PointF;
@@ -43,6 +46,7 @@
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.taskbar.LauncherTaskbarUIController;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
@@ -50,6 +54,8 @@
 import com.android.quickstep.util.OverviewToHomeAnim;
 import com.android.quickstep.views.RecentsView;
 
+import java.util.function.Consumer;
+
 /**
  * Touch controller which handles swipe and hold from the nav bar to go to Overview. Swiping above
  * the nav bar falls back to go to All Apps. Swiping from the nav bar without holding goes to the
@@ -64,6 +70,7 @@
     private static final float TRANSLATION_ANIM_VELOCITY_DP_PER_MS = 0.8f;
 
     private final VibratorWrapper mVibratorWrapper;
+    private final Consumer<AnimatorSet> mCancelSplitRunnable;
     private final RecentsView mRecentsView;
     private final MotionPauseDetector mMotionPauseDetector;
     private final float mMotionPauseMinDisplacement;
@@ -79,16 +86,26 @@
     // Normal to Hint animation has flag SKIP_OVERVIEW, so we update this scrim with this animator.
     private ObjectAnimator mNormalToHintOverviewScrimAnimator;
 
-    public NoButtonNavbarToOverviewTouchController(Launcher l) {
+    /**
+     * @param cancelSplitRunnable Called when split placeholder view needs to be cancelled.
+     *                            Animation should be added to the provided AnimatorSet
+     */
+    public NoButtonNavbarToOverviewTouchController(Launcher l,
+            Consumer<AnimatorSet> cancelSplitRunnable) {
         super(l);
         mRecentsView = l.getOverviewPanel();
         mMotionPauseDetector = new MotionPauseDetector(l);
         mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
         mVibratorWrapper = VibratorWrapper.INSTANCE.get(l.getApplicationContext());
+        mCancelSplitRunnable = cancelSplitRunnable;
     }
 
     @Override
     protected boolean canInterceptTouch(MotionEvent ev) {
+        if (!isTrackpadMotionEvent(ev) && DisplayController.getNavigationMode(mLauncher)
+                == THREE_BUTTONS) {
+            return false;
+        }
         mDidTouchStartInNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
         boolean isOneHandedModeActive = (SystemUiProxy.INSTANCE.get(mLauncher)
                 .getLastSystemUiStateFlags() & SYSUI_STATE_ONE_HANDED_ACTIVE) != 0;
@@ -102,9 +119,6 @@
     protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
         if (fromState == NORMAL && mDidTouchStartInNavBar) {
             return HINT_STATE;
-        } else if (fromState == OVERVIEW && isDragTowardPositive) {
-            // Don't allow swiping up to all apps.
-            return OVERVIEW;
         }
         return super.getTargetState(fromState, isDragTowardPositive);
     }
@@ -190,6 +204,9 @@
             // state, but since the hint state tracks the entire screen without a clear endpoint, we
             // need to manually set the duration to a reasonable value.
             animator.setDuration(HINT_STATE.getTransitionDuration(mLauncher, true /* isToState */));
+            AnimatorSet animatorSet = new AnimatorSet();
+            mCancelSplitRunnable.accept(animatorSet);
+            animatorSet.start();
         }
         if (FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() &&
                 ((mFromState == NORMAL && mToState == ALL_APPS)
@@ -261,7 +278,7 @@
     private void goToOverviewOrHomeOnDragEnd(float velocity) {
         boolean goToHomeInsteadOfOverview = !mMotionPauseDetector.isPaused();
         if (goToHomeInsteadOfOverview) {
-            new OverviewToHomeAnim(mLauncher, () -> onSwipeInteractionCompleted(NORMAL))
+            new OverviewToHomeAnim(mLauncher, () -> onSwipeInteractionCompleted(NORMAL), null)
                     .animateWithVelocity(velocity);
         }
         if (mReachedOverview) {
@@ -273,7 +290,7 @@
             mRecentsView.animate()
                     .translationX(0)
                     .translationY(0)
-                    .setInterpolator(ACCEL_DEACCEL)
+                    .setInterpolator(ACCELERATE_DECELERATE)
                     .setDuration(duration)
                     .withEndAction(goToHomeInsteadOfOverview
                             ? null
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 80f5558..6f421eb 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -16,21 +16,21 @@
 package com.android.launcher3.uioverrides.touchcontrollers;
 
 import static android.view.MotionEvent.ACTION_DOWN;
-import static android.view.MotionEvent.ACTION_MOVE;
 
+import static com.android.app.animation.Interpolators.ACCELERATE_0_75;
+import static com.android.app.animation.Interpolators.DECELERATE_3;
+import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
 import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME;
 import static com.android.launcher3.MotionEventsUtils.isTrackpadFourFingerSwipe;
+import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
 import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
-import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
@@ -46,6 +46,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM;
 import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_RIGHT;
 import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_UP;
+import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
@@ -74,6 +75,7 @@
 import com.android.launcher3.touch.BaseSwipeDetector;
 import com.android.launcher3.touch.BothAxesSwipeDetector;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.quickstep.SystemUiProxy;
@@ -84,6 +86,7 @@
 import com.android.quickstep.views.DesktopTaskView;
 import com.android.quickstep.views.LauncherRecentsView;
 import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 
 /**
  * Handles quick switching to a recent task from the home screen. To give as much flexibility to
@@ -93,8 +96,8 @@
         BothAxesSwipeDetector.Listener {
 
     private static final float Y_ANIM_MIN_PROGRESS = 0.25f;
-    private static final Interpolator FADE_OUT_INTERPOLATOR = DEACCEL_3;
-    private static final Interpolator TRANSLATE_OUT_INTERPOLATOR = ACCEL_0_75;
+    private static final Interpolator FADE_OUT_INTERPOLATOR = DECELERATE_3;
+    private static final Interpolator TRANSLATE_OUT_INTERPOLATOR = ACCELERATE_0_75;
     private static final Interpolator SCALE_DOWN_INTERPOLATOR = LINEAR;
     private static final long ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW = 300;
 
@@ -110,7 +113,6 @@
             newCancelListener(this::clearState);
 
     private boolean mNoIntercept;
-    private Boolean mIsTrackpadFourFingerSwipe;
     private LauncherState mStartState;
 
     private boolean mIsHomeScreenVisible = true;
@@ -136,9 +138,7 @@
 
     @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        int action = ev.getActionMasked();
-        if (action == ACTION_DOWN) {
-            mIsTrackpadFourFingerSwipe = null;
+        if (ev.getActionMasked() == ACTION_DOWN) {
             mNoIntercept = !canInterceptTouch(ev);
             if (mNoIntercept) {
                 return false;
@@ -147,13 +147,6 @@
             // Only detect horizontal swipe for intercept, then we will allow swipe up as well.
             mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT,
                     false /* ignoreSlopWhenSettling */);
-        } else if (isTrackpadMultiFingerSwipe(ev) && mIsTrackpadFourFingerSwipe == null
-                && action == ACTION_MOVE) {
-            mIsTrackpadFourFingerSwipe = isTrackpadFourFingerSwipe(ev);
-            mNoIntercept = !mIsTrackpadFourFingerSwipe;
-            if (mNoIntercept) {
-                return false;
-            }
         }
 
         if (mNoIntercept) {
@@ -170,6 +163,10 @@
     }
 
     private boolean canInterceptTouch(MotionEvent ev) {
+        if (!isTrackpadMotionEvent(ev) && DisplayController.getNavigationMode(mLauncher)
+                == THREE_BUTTONS) {
+            return false;
+        }
         if (!mLauncher.isInState(LauncherState.NORMAL)) {
             return false;
         }
@@ -184,6 +181,9 @@
             // TODO(b/268075592): add support for quickswitch to/from desktop
             return false;
         }
+        if (isTrackpadMultiFingerSwipe(ev)) {
+            return isTrackpadFourFingerSwipe(ev);
+        }
         return true;
     }
 
@@ -191,6 +191,9 @@
     public void onDragStart(boolean start) {
         mMotionPauseDetector.clear();
         if (start) {
+            InteractionJankMonitorWrapper.begin(mRecentsView,
+                    InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+
             mStartState = mLauncher.getStateManager().getState();
 
             mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseDetected);
@@ -325,6 +328,7 @@
         if (mMotionPauseDetector.isPaused() && noFling) {
             // Going to Overview.
             cancelAnimations();
+            InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
 
             StateAnimationConfig config = new StateAnimationConfig();
             config.duration = ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
@@ -441,6 +445,8 @@
                     RecentsView.SCROLL_VIBRATION_PRIMITIVE,
                     RecentsView.SCROLL_VIBRATION_PRIMITIVE_SCALE,
                     RecentsView.SCROLL_VIBRATION_FALLBACK);
+        } else {
+            InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
         }
 
         nonOverviewAnim.setDuration(Math.max(xDuration, yDuration));
@@ -462,6 +468,11 @@
                                 : targetState.ordinal > mStartState.ordinal
                                         ? LAUNCHER_UNKNOWN_SWIPEUP
                                         : LAUNCHER_UNKNOWN_SWIPEDOWN));
+
+        if (targetState == QUICK_SWITCH_FROM_HOME) {
+            InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+        }
+
         mLauncher.getStateManager().goToState(targetState, false, forEndCallback(this::clearState));
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 8368f9c..e30fe66 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -24,11 +24,12 @@
 
 import android.view.MotionEvent;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.allapps.AllAppsTransitionController;
-import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
 import com.android.launcher3.touch.AllAppsSwipeController;
@@ -92,9 +93,9 @@
     @Override
     protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
         if (fromState == ALL_APPS && !isDragTowardPositive) {
-            return NORMAL;
-        } else if (fromState == OVERVIEW) {
-            return isDragTowardPositive ? OVERVIEW : NORMAL;
+            return FeatureFlags.ENABLE_ALL_APPS_FROM_OVERVIEW.get()
+                    ? mLauncher.getStateManager().getLastState()
+                    : NORMAL;
         } else if (fromState == NORMAL && isDragTowardPositive) {
             return ALL_APPS;
         }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index f941b02..9a35bb2 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -15,12 +15,12 @@
  */
 package com.android.launcher3.uioverrides.touchcontrollers;
 
+import static com.android.app.animation.Interpolators.ACCELERATE_2;
+import static com.android.app.animation.Interpolators.DECELERATE_2;
+import static com.android.app.animation.Interpolators.INSTANT;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME;
-import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
-import static com.android.launcher3.anim.Interpolators.INSTANT;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
@@ -128,14 +128,14 @@
     }
 
     private void setupInterpolators(StateAnimationConfig stateAnimationConfig) {
-        stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_FADE, DEACCEL_2);
-        stateAnimationConfig.setInterpolator(ANIM_ALL_APPS_FADE, DEACCEL_2);
+        stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_FADE, DECELERATE_2);
+        stateAnimationConfig.setInterpolator(ANIM_ALL_APPS_FADE, DECELERATE_2);
         if (DisplayController.getNavigationMode(mLauncher) == NavigationMode.NO_BUTTON) {
             // Overview lives to the left of workspace, so translate down later than over
-            stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL_2);
-            stateAnimationConfig.setInterpolator(ANIM_VERTICAL_PROGRESS, ACCEL_2);
-            stateAnimationConfig.setInterpolator(ANIM_OVERVIEW_SCALE, ACCEL_2);
-            stateAnimationConfig.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, ACCEL_2);
+            stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCELERATE_2);
+            stateAnimationConfig.setInterpolator(ANIM_VERTICAL_PROGRESS, ACCELERATE_2);
+            stateAnimationConfig.setInterpolator(ANIM_OVERVIEW_SCALE, ACCELERATE_2);
+            stateAnimationConfig.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, ACCELERATE_2);
             stateAnimationConfig.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
         } else {
             stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_TRANSLATE, LINEAR);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
index 395833f..26ab3d6 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
@@ -76,7 +76,7 @@
     private void dispatchTouchEvent(MotionEvent ev) {
         if (mSystemUiProxy.isActive()) {
             mLastAction = ev.getActionMasked();
-            mSystemUiProxy.onStatusBarMotionEvent(ev);
+            mSystemUiProxy.onStatusBarTouchEvent(ev);
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index eddc50c..3d94857 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -27,13 +27,13 @@
 import android.view.View;
 import android.view.animation.Interpolator;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.LauncherAnimUtils;
 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.touch.BaseSwipeDetector;
 import com.android.launcher3.touch.PagedOrientationHandler;
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 43db7c3..1ef4039 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -21,19 +21,19 @@
 import static android.view.Surface.ROTATION_90;
 import static android.widget.Toast.LENGTH_SHORT;
 
+import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
+import static com.android.app.animation.Interpolators.DECELERATE;
+import static com.android.app.animation.Interpolators.OVERSHOOT_1_2;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
+import static com.android.launcher3.LauncherPrefs.ALL_APPS_OVERVIEW_THRESHOLD;
 import static com.android.launcher3.PagedView.INVALID_PAGE;
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
 import static com.android.launcher3.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.uioverrides.QuickstepLauncher.ENABLE_PIP_KEEP_CLEAR_ALGORITHM;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
@@ -94,6 +94,7 @@
 import com.android.internal.util.LatencyTracker;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
@@ -101,14 +102,15 @@
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.taskbar.TaskbarThresholdUtils;
 import com.android.launcher3.taskbar.TaskbarUIController;
-import com.android.launcher3.tracing.InputConsumerProto;
-import com.android.launcher3.tracing.SwipeHandlerProto;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
 import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.util.WindowBounds;
@@ -122,7 +124,6 @@
 import com.android.quickstep.util.InputConsumerProxy;
 import com.android.quickstep.util.InputProxyHandlerFactory;
 import com.android.quickstep.util.MotionPauseDetector;
-import com.android.quickstep.util.ProtoTracer;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
@@ -133,6 +134,7 @@
 import com.android.quickstep.views.DesktopTaskView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
+import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -149,6 +151,7 @@
 import java.util.HashMap;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.OptionalInt;
 import java.util.function.Consumer;
 
 /**
@@ -163,9 +166,6 @@
 
     private static final ArrayList<String> STATE_NAMES = new ArrayList<>();
 
-    /** Shift distance to transition to All Apps if ENABLE_ALL_APPS_FROM_OVERVIEW. */
-    public static final float ALL_APPS_SHIFT_THRESHOLD = 2f;
-
     protected final BaseActivityInterface<S, T> mActivityInterface;
     protected final InputConsumerProxy mInputConsumerProxy;
     protected final ActivityInitListener mActivityInitListener;
@@ -193,6 +193,7 @@
                     ActiveGestureLog.INSTANCE.addLog("Launcher destroyed", LAUNCHER_DESTROYED);
                     mRecentsView = null;
                     mActivity = null;
+                    mStateCallback.clearState(STATE_LAUNCHER_PRESENT);
                 }
             };
 
@@ -293,7 +294,8 @@
 
     private boolean mContinuingLastGesture;
 
-    private ThumbnailData mTaskSnapshot;
+    // Cache of recently-updated task snapshots, mapping task id to ThumbnailData
+    private HashMap<Integer, ThumbnailData> mTaskSnapshotCache = new HashMap<>();
 
     // Used to control launcher components throughout the swipe gesture.
     private AnimatorControllerWithResistance mLauncherTransitionController;
@@ -378,12 +380,12 @@
         mTaskbarAlreadyOpen = controller != null && !controller.isTaskbarStashed();
         mIsTaskbarAllAppsOpen = controller != null && controller.isTaskbarAllAppsOpen();
         mTaskbarAppWindowThreshold =
-                res.getDimensionPixelSize(R.dimen.taskbar_app_window_threshold);
+                TaskbarThresholdUtils.getAppWindowThreshold(res, mDp);
         boolean swipeWillNotShowTaskbar = mTaskbarAlreadyOpen || mGestureState.isTrackpadGesture();
         mTaskbarHomeOverviewThreshold = swipeWillNotShowTaskbar
                 ? 0
-                : res.getDimensionPixelSize(R.dimen.taskbar_home_overview_threshold);
-        mTaskbarCatchUpThreshold = res.getDimensionPixelSize(R.dimen.taskbar_catch_up_threshold);
+                : TaskbarThresholdUtils.getHomeOverviewThreshold(res, mDp);
+        mTaskbarCatchUpThreshold = TaskbarThresholdUtils.getCatchUpThreshold(res, mDp);
     }
 
     @Nullable
@@ -516,20 +518,22 @@
         // Set up a entire animation lifecycle callback to notify the current recents view when
         // the animation is canceled
         mGestureState.runOnceAtState(STATE_RECENTS_ANIMATION_CANCELED, () -> {
-                HashMap<Integer, ThumbnailData> snapshots =
-                        mGestureState.consumeRecentsAnimationCanceledSnapshot();
-                if (snapshots != null) {
-                    mRecentsView.switchToScreenshot(snapshots, () -> {
-                        if (mRecentsAnimationController != null) {
-                            mRecentsAnimationController.cleanupScreenshot();
-                        } else if (mDeferredCleanupRecentsAnimationController != null) {
-                            mDeferredCleanupRecentsAnimationController.cleanupScreenshot();
-                            mDeferredCleanupRecentsAnimationController = null;
-                        }
-                    });
-                    mRecentsView.onRecentsAnimationComplete();
-                }
-            });
+            if (mRecentsView == null) return;
+
+            HashMap<Integer, ThumbnailData> snapshots =
+                    mGestureState.consumeRecentsAnimationCanceledSnapshot();
+            if (snapshots != null) {
+                mRecentsView.switchToScreenshot(snapshots, () -> {
+                    if (mRecentsAnimationController != null) {
+                        mRecentsAnimationController.cleanupScreenshot();
+                    } else if (mDeferredCleanupRecentsAnimationController != null) {
+                        mDeferredCleanupRecentsAnimationController.cleanupScreenshot();
+                        mDeferredCleanupRecentsAnimationController = null;
+                    }
+                });
+                mRecentsView.onRecentsAnimationComplete();
+            }
+        });
 
         setupRecentsViewUi();
         mRecentsView.runOnPageScrollsInitialized(this::linkRecentsViewScroll);
@@ -587,7 +591,7 @@
         if (mWasLauncherAlreadyVisible) {
             mStateCallback.setState(STATE_LAUNCHER_DRAWN);
         } else {
-            Object traceToken = TraceHelper.INSTANCE.beginSection("WTS-init");
+            SafeCloseable traceToken = TraceHelper.INSTANCE.beginAsyncSection("WTS-init");
             View dragLayer = activity.getDragLayer();
             dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {
                 boolean mHandled = false;
@@ -599,7 +603,7 @@
                     }
                     mHandled = true;
 
-                    TraceHelper.INSTANCE.endSection(traceToken);
+                    traceToken.close();
                     dragLayer.post(() ->
                             dragLayer.getViewTreeObserver().removeOnDrawListener(this));
                     if (activity != mActivity) {
@@ -657,11 +661,12 @@
 
     protected void notifyGestureAnimationStartToRecents() {
         Task[] runningTasks;
+        TopTaskTracker.CachedTaskInfo cachedTaskInfo = mGestureState.getRunningTask();
         if (mIsSwipeForSplit) {
             int[] splitTaskIds = TopTaskTracker.INSTANCE.get(mContext).getRunningSplitTaskIds();
-            runningTasks = mGestureState.getRunningTask().getPlaceholderTasks(splitTaskIds);
+            runningTasks = cachedTaskInfo.getPlaceholderTasks(splitTaskIds);
         } else {
-            runningTasks = mGestureState.getRunningTask().getPlaceholderTasks();
+            runningTasks = cachedTaskInfo.getPlaceholderTasks();
         }
 
         // Safeguard against any null tasks being sent to recents view, happens when quickswitching
@@ -681,11 +686,10 @@
     private void initializeLauncherAnimationController() {
         buildAnimationController();
 
-        Object traceToken = TraceHelper.INSTANCE.beginSection("logToggleRecents",
-                TraceHelper.FLAG_IGNORE_BINDERS);
-        LatencyTracker.getInstance(mContext).logAction(LatencyTracker.ACTION_TOGGLE_RECENTS,
-                (int) (mLauncherFrameDrawnTime - mTouchTimeMs));
-        TraceHelper.INSTANCE.endSection(traceToken);
+        try (SafeCloseable c = TraceHelper.INSTANCE.allowIpcs("logToggleRecents")) {
+            LatencyTracker.getInstance(mContext).logAction(LatencyTracker.ACTION_TOGGLE_RECENTS,
+                    (int) (mLauncherFrameDrawnTime - mTouchTimeMs));
+        }
 
         // This method is only called when STATE_GESTURE_STARTED is set, so we can enable the
         // high-res thumbnail loader here once we are sure that we will end up in an overview state
@@ -731,11 +735,15 @@
      * @param moveRunningTask whether to move running task to front when attaching
      */
     private void maybeUpdateRecentsAttachedState(boolean animate, boolean moveRunningTask) {
-        if (!mDeviceState.isFullyGesturalNavMode() || mRecentsView == null) {
+        if ((!mDeviceState.isFullyGesturalNavMode() && !mGestureState.isTrackpadGesture())
+                || mRecentsView == null) {
             return;
         }
+        // looking at single target is fine here since either app of a split pair would
+        // have their "isInRecents" field set? (that's what this is used for below)
         RemoteAnimationTarget runningTaskTarget = mRecentsAnimationTargets != null
-                ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId())
+                ? mRecentsAnimationTargets
+                .findTask(mGestureState.getTopRunningTaskId())
                 : null;
         final boolean recentsAttachedToAppWindow;
         if (mIsInAllAppsRegion) {
@@ -810,6 +818,10 @@
         VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
         maybeUpdateRecentsAttachedState(true);
 
+        if (mActivity != null) {
+            mActivity.getAppsView().getSearchUiManager().prepareToFocusEditText(mIsInAllAppsRegion);
+        }
+
         // Draw active task below Launcher so that All Apps can appear over it.
         runActionOnRemoteHandles(remoteTargetHandle ->
                 remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(isInAllAppsRegion));
@@ -873,7 +885,8 @@
     @UiThread
     @Override
     public void onCurrentShiftUpdated() {
-        setIsInAllAppsRegion(mCurrentShift.value >= ALL_APPS_SHIFT_THRESHOLD);
+        float threshold = LauncherPrefs.get(mContext).get(ALL_APPS_OVERVIEW_THRESHOLD) / 100f;
+        setIsInAllAppsRegion(mCurrentShift.value >= threshold);
         updateSysUiFlags(mCurrentShift.value);
         applyScrollAndTransform();
 
@@ -1148,6 +1161,14 @@
                 mStateCallback.setState(STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT);
                 // Notify the SysUI to use fade-in animation when entering PiP
                 SystemUiProxy.INSTANCE.get(mContext).setPipAnimationTypeToAlpha();
+                if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) {
+                    // Notify the SysUI to stash desktop apps if they are visible
+                    DesktopVisibilityController desktopVisibilityController =
+                            mActivityInterface.getDesktopVisibilityController();
+                    if (desktopVisibilityController != null) {
+                        desktopVisibilityController.onHomeActionTriggered();
+                    }
+                }
                 break;
             case RECENTS:
                 mStateCallback.setState(STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
@@ -1178,7 +1199,7 @@
             return false;
         }
         boolean hasStartedTaskBefore = Arrays.stream(appearedTaskTarget).anyMatch(
-                targetCompat -> targetCompat.taskId == mGestureState.getLastStartedTaskId());
+                mGestureState.mLastStartedTaskIdPredicate);
         if (mStateCallback.hasStates(STATE_START_NEW_TASK) && hasStartedTaskBefore) {
             reset();
             return true;
@@ -1324,11 +1345,11 @@
         Interpolator interpolator;
         S state = mActivityInterface.stateFromGestureEndTarget(endTarget);
         if (state.displayOverviewTasksAsGrid(mDp)) {
-            interpolator = ACCEL_DEACCEL;
+            interpolator = ACCELERATE_DECELERATE;
         } else if (endTarget == RECENTS) {
             interpolator = OVERSHOOT_1_2;
         } else {
-            interpolator = DEACCEL;
+            interpolator = DECELERATE;
         }
 
         if (endTarget.isLauncher) {
@@ -1444,9 +1465,12 @@
         @Override
         public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
                 boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
-            if (task.taskId == mGestureState.getRunningTaskId()
-                    && task.configuration.windowConfiguration.getActivityType()
-                    != ACTIVITY_TYPE_HOME) {
+            boolean taskRunningAndNotHome = Arrays.stream(mGestureState
+                            .getRunningTaskIds(true /*getMultipleTasks*/))
+                    .anyMatch(taskId -> task.taskId == taskId
+                            && task.configuration.windowConfiguration.getActivityType()
+                            != ACTIVITY_TYPE_HOME);
+            if (taskRunningAndNotHome) {
                 // 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 */);
@@ -1488,8 +1512,12 @@
 
         if (mGestureState.getEndTarget() == HOME) {
             getOrientationHandler().adjustFloatingIconStartVelocity(velocityPxPerMs);
+            // Take first task ID, if there are multiple we don't have any special home
+            // animation so doesn't matter for splitscreen.. though the "allowEnterPip" might change
+            // depending on which task it is..
             final RemoteAnimationTarget runningTaskTarget = mRecentsAnimationTargets != null
-                    ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId())
+                    ? mRecentsAnimationTargets
+                    .findTask(mGestureState.getTopRunningTaskId())
                     : null;
             final ArrayList<IBinder> cookies = runningTaskTarget != null
                     ? runningTaskTarget.taskInfo.launchCookies
@@ -1518,8 +1546,10 @@
 
                 // grab a screenshot before the PipContentOverlay gets parented on top of the task
                 UI_HELPER_EXECUTOR.execute(() -> {
-                    mTaskSnapshot = mRecentsAnimationController.screenshotTask(
-                            mGestureState.getRunningTaskId());
+                    // Directly use top task, split to pip handled on shell side
+                    final int taskId = mGestureState.getTopRunningTaskId();
+                    mTaskSnapshotCache.put(taskId,
+                            mRecentsAnimationController.screenshotTask(taskId));
                 });
 
                 // let SystemUi reparent the overlay leash as soon as possible
@@ -1725,11 +1755,6 @@
 
     private Rect getKeepClearAreaForHotseat() {
         Rect keepClearArea;
-        if (!ENABLE_PIP_KEEP_CLEAR_ALGORITHM) {
-            // make the height equal to hotseatBarSizePx only
-            keepClearArea = new Rect(0, 0, 0, mDp.hotseatBarSizePx);
-            return keepClearArea;
-        }
         // the keep clear area in global screen coordinates, in pixels
         if (mDp.isPhone) {
             if (mDp.isSeascape()) {
@@ -1908,7 +1933,7 @@
         mActivityInitListener.unregister();
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
                 mActivityRestartListener);
-        mTaskSnapshot = null;
+        mTaskSnapshotCache.clear();
     }
 
     private void invalidateHandler() {
@@ -1926,7 +1951,7 @@
         mActivityInitListener.unregister();
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
                 mActivityRestartListener);
-        mTaskSnapshot = null;
+        mTaskSnapshotCache.clear();
     }
 
     private void invalidateHandlerWithLauncher() {
@@ -1981,37 +2006,39 @@
             // If there are no targets, then we don't need to capture anything
             mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
         } else {
-            final int runningTaskId = mGestureState.getRunningTaskId();
             boolean finishTransitionPosted = false;
+            // If we already have cached screenshot(s) from running tasks, skip update
+            boolean shouldUpdate = false;
+            int[] runningTaskIds = mGestureState.getRunningTaskIds(mIsSwipeForSplit);
+            for (int id : runningTaskIds) {
+                if (!mTaskSnapshotCache.containsKey(id)) {
+                    shouldUpdate = true;
+                    break;
+                }
+            }
+
             if (mRecentsAnimationController != null) {
                 // Update the screenshot of the task
-                if (mTaskSnapshot == null) {
+                if (shouldUpdate) {
                     UI_HELPER_EXECUTOR.execute(() -> {
                         if (mRecentsAnimationController == null) return;
-                        final ThumbnailData taskSnapshot =
-                                mRecentsAnimationController.screenshotTask(runningTaskId);
-                        // If split case, we should update all split tasks snapshot
-                        if (mIsSwipeForSplit) {
-                            int[] splitTaskIds = TopTaskTracker.INSTANCE.get(
-                                    mContext).getRunningSplitTaskIds();
-                            for (int i = 0; i < splitTaskIds.length; i++) {
-                                // Skip running one because done above.
-                                if (splitTaskIds[i] == runningTaskId) continue;
-
-                                mRecentsAnimationController.screenshotTask(splitTaskIds[i]);
-                            }
+                        for (int id : runningTaskIds) {
+                            mTaskSnapshotCache.put(
+                                    id, mRecentsAnimationController.screenshotTask(id));
                         }
+
                         MAIN_EXECUTOR.execute(() -> {
-                            mTaskSnapshot = taskSnapshot;
-                            if (!updateThumbnail(runningTaskId, false /* refreshView */)) {
+                            if (!updateThumbnail(false /* refreshView */)) {
                                 setScreenshotCapturedState();
                             }
                         });
                     });
                     return;
                 }
-                finishTransitionPosted = updateThumbnail(runningTaskId, false /* refreshView */);
+
+                finishTransitionPosted = updateThumbnail(false /* refreshView */);
             }
+
             if (!finishTransitionPosted) {
                 setScreenshotCapturedState();
             }
@@ -2019,35 +2046,34 @@
     }
 
     // Returns whether finish transition was posted.
-    private boolean updateThumbnail(int runningTaskId, boolean refreshView) {
-        boolean finishTransitionPosted = false;
-        final TaskView taskView;
+    private boolean updateThumbnail(boolean refreshView) {
         if (mGestureState.getEndTarget() == HOME
                 || mGestureState.getEndTarget() == NEW_TASK
                 || mGestureState.getEndTarget() == ALL_APPS
                 || mRecentsView == null) {
             // Capture the screenshot before finishing the transition to home or quickswitching to
             // ensure it's taken in the correct orientation, but no need to update the thumbnail.
-            taskView = null;
-        } else {
-            taskView = mRecentsView.updateThumbnail(runningTaskId, mTaskSnapshot, refreshView);
+            return false;
         }
-        if (taskView != null && refreshView && !mCanceled) {
+
+        boolean finishTransitionPosted = false;
+        TaskView updatedTaskView = mRecentsView.updateThumbnail(mTaskSnapshotCache, refreshView);
+        if (updatedTaskView != null && refreshView && !mCanceled) {
             // Defer finishing the animation until the next launcher frame with the
             // new thumbnail
-            finishTransitionPosted = ViewUtils.postFrameDrawn(taskView,
+            finishTransitionPosted = ViewUtils.postFrameDrawn(updatedTaskView,
                     () -> mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED),
                     this::isCanceled);
         }
+
         return finishTransitionPosted;
     }
 
     private void setScreenshotCapturedState() {
         // If we haven't posted a draw callback, set the state immediately.
-        Object traceToken = TraceHelper.INSTANCE.beginSection(SCREENSHOT_CAPTURED_EVT,
-                TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS);
+        TraceHelper.INSTANCE.beginSection(SCREENSHOT_CAPTURED_EVT);
         mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
-        TraceHelper.INSTANCE.endSection(traceToken);
+        TraceHelper.INSTANCE.endSection();
     }
 
     private void finishCurrentTransitionToRecents() {
@@ -2188,16 +2214,27 @@
         if (!mCanceled) {
             TaskView nextTask = mRecentsView == null ? null : mRecentsView.getNextPageTaskView();
             if (nextTask != null) {
-                Task.TaskKey nextTaskKey = nextTask.getTask().key;
-                int taskId = nextTaskKey.id;
-                mGestureState.updateLastStartedTaskId(taskId);
-                boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
-                        .contains(taskId);
+                int[] taskIds = nextTask.getTaskIds();
+                StringBuilder nextTaskLog = new StringBuilder();
+                for (TaskIdAttributeContainer c : nextTask.getTaskIdAttributeContainers()) {
+                    if (c == null) {
+                        continue;
+                    }
+                    nextTaskLog
+                            .append("[id: ")
+                            .append(c.getTask().key.id)
+                            .append(", pkg: ")
+                            .append(c.getTask().key.getPackageName())
+                            .append("] | ");
+                }
+                mGestureState.updateLastStartedTaskIds(taskIds);
+                boolean hasTaskPreviouslyAppeared = Arrays.stream(taskIds).anyMatch(
+                                taskId -> mGestureState.getPreviouslyAppearedTaskIds()
+                                        .contains(taskId));
                 if (!hasTaskPreviouslyAppeared) {
                     ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED);
                 }
-                ActiveGestureLog.INSTANCE.addLog("Launching task: id=" + taskId
-                        + " pkg=" + nextTaskKey.getPackageName());
+                ActiveGestureLog.INSTANCE.addLog("Launching task: " + nextTaskLog);
                 nextTask.launchTask(success -> {
                     resultCallback.accept(success);
                     if (success) {
@@ -2267,7 +2304,7 @@
     public void onTasksAppeared(RemoteAnimationTarget[] appearedTaskTargets) {
         if (mRecentsAnimationController != null) {
             boolean hasStartedTaskBefore = Arrays.stream(appearedTaskTargets).anyMatch(
-                    targetCompat -> targetCompat.taskId == mGestureState.getLastStartedTaskId());
+                    mGestureState.mLastStartedTaskIdPredicate);
             if (!mStateCallback.hasStates(STATE_GESTURE_COMPLETED) && !hasStartedTaskBefore) {
                 // This is a special case, if a task is started mid-gesture that wasn't a part of a
                 // previous quickswitch task launch, then cancel the animation back to the app
@@ -2280,8 +2317,7 @@
             } else if (handleTaskAppeared(appearedTaskTargets)) {
                 Optional<RemoteAnimationTarget> taskTargetOptional =
                         Arrays.stream(appearedTaskTargets)
-                                .filter(targetCompat ->
-                                        targetCompat.taskId == mGestureState.getLastStartedTaskId())
+                                .filter(mGestureState.mLastStartedTaskIdPredicate)
                                 .findFirst();
                 if (!taskTargetOptional.isPresent()) {
                     ActiveGestureLog.INSTANCE.addLog("No appeared task matching started task id");
@@ -2350,11 +2386,16 @@
      * resume if we finish the controller.
      */
     protected int getLastAppearedTaskIndex() {
-        return mRecentsView == null
-                ? -1
-                : mGestureState.getLastAppearedTaskId() != -1
-                        ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId())
-                        : mRecentsView.getRunningTaskIndex();
+        if (mRecentsView == null) {
+            return -1;
+        }
+
+        OptionalInt firstValidTaskId = Arrays.stream(mGestureState.getLastAppearedTaskIds())
+                .filter(i -> i != -1)
+                .findFirst();
+        return firstValidTaskId.isPresent()
+                ? mRecentsView.getTaskIndexForId(firstValidTaskId.getAsInt())
+                : mRecentsView.getRunningTaskIndex();
     }
 
     /**
@@ -2362,7 +2403,7 @@
      * but before that task appeared.
      */
     protected boolean hasStartedNewTask() {
-        return mGestureState.getLastStartedTaskId() != -1;
+        return mGestureState.getLastStartedTaskIds()[0] != -1;
     }
 
     /**
@@ -2406,7 +2447,6 @@
                 taskViewSimulator.apply(remoteHandle.getTransformParams());
             }
         }
-        ProtoTracer.INSTANCE.get(mContext).scheduleFrameUpdate();
     }
 
     // Scaling of RecentsView during quick switch based on amount of recents scroll
@@ -2428,11 +2468,11 @@
 
         if (scrollOffset < mQuickSwitchScaleScrollThreshold) {
             scaleProgress = Utilities.mapToRange(scrollOffset, 0, mQuickSwitchScaleScrollThreshold,
-                    0, maxScaleProgress, ACCEL_DEACCEL);
+                    0, maxScaleProgress, ACCELERATE_DECELERATE);
         } else if (scrollOffset > (maxScrollOffset - mQuickSwitchScaleScrollThreshold)) {
             scaleProgress = Utilities.mapToRange(scrollOffset,
                     (maxScrollOffset - mQuickSwitchScaleScrollThreshold), maxScrollOffset,
-                    maxScaleProgress, 0, ACCEL_DEACCEL);
+                    maxScaleProgress, 0, ACCELERATE_DECELERATE);
         }
 
         return scaleProgress;
@@ -2461,7 +2501,7 @@
         // "Catch up" with the displacement at mTaskbarCatchUpThreshold.
         if (displacement < mTaskbarCatchUpThreshold) {
             return Utilities.mapToRange(displacement, mTaskbarAppWindowThreshold,
-                    mTaskbarCatchUpThreshold, 0, mTaskbarCatchUpThreshold, ACCEL_DEACCEL);
+                    mTaskbarCatchUpThreshold, 0, mTaskbarCatchUpThreshold, ACCELERATE_DECELERATE);
         }
 
         return displacement;
@@ -2476,26 +2516,6 @@
                 mRecentsAnimationTargets.nonApps, shown, null /* animatorHandler */);
     }
 
-    /**
-     * Used for winscope tracing, see launcher_trace.proto
-     * @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto
-     * @param inputConsumerProto The parent of this proto message.
-     */
-    public void writeToProto(InputConsumerProto.Builder inputConsumerProto) {
-        SwipeHandlerProto.Builder swipeHandlerProto = SwipeHandlerProto.newBuilder();
-
-        mGestureState.writeToProto(swipeHandlerProto);
-
-        swipeHandlerProto.setIsRecentsAttachedToAppWindow(
-                mAnimationFactory.isRecentsAttachedToAppWindow());
-        swipeHandlerProto.setScrollOffset(mRecentsView == null
-                ? 0
-                : mRecentsView.getScrollOffset());
-        swipeHandlerProto.setAppToOverviewProgress(mCurrentShift.value);
-
-        inputConsumerProto.setSwipeHandler(swipeHandlerProto);
-    }
-
     public interface Factory {
         AbsSwipeUpHandler newHandler(GestureState gestureState, long touchTimeMs);
     }
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 60083c6..5a9d80d 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -15,11 +15,11 @@
  */
 package com.android.quickstep;
 
+import static com.android.app.animation.Interpolators.ACCELERATE_2;
+import static com.android.app.animation.Interpolators.INSTANT;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR;
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
-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.quickstep.AbsSwipeUpHandler.RECENTS_ATTACH_DURATION;
 import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
 import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
@@ -553,7 +553,7 @@
             long animationDuration = animate ? RECENTS_ATTACH_DURATION : 0;
             Animator fadeAnim = mActivity.getStateManager()
                     .createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
-            fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
+            fadeAnim.setInterpolator(attached ? INSTANT : ACCELERATE_2);
             fadeAnim.setDuration(animationDuration);
             animatorSet.play(fadeAnim);
 
diff --git a/quickstep/src/com/android/quickstep/BinderTracker.java b/quickstep/src/com/android/quickstep/BinderTracker.java
new file mode 100644
index 0000000..a876cd8
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/BinderTracker.java
@@ -0,0 +1,187 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.os.IBinder.FLAG_ONEWAY;
+
+import android.os.Binder;
+import android.os.Binder.ProxyTransactListener;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.Trace;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.util.TraceHelper;
+
+import java.util.LinkedList;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import kotlin.random.Random;
+
+/**
+ * A binder proxy transaction listener for tracking binder calls on main thread.
+ */
+public class BinderTracker {
+
+    private static final String TAG = "BinderTracker";
+
+    // Common IPCs that are ok to block the main thread.
+    private static final Set<String> sAllowedFrameworkClasses = Set.of(
+            "android.view.IWindowSession",
+            "android.os.IPowerManager",
+            "android.os.IServiceManager");
+
+    /**
+     * Starts tracking binder class and returns a {@link SafeCloseable} to end tracking
+     */
+    public static SafeCloseable startTracking(Consumer<BinderCallSite> callback) {
+        TraceHelper current = TraceHelper.INSTANCE;
+
+        TraceHelperExtension helper = new TraceHelperExtension(callback);
+        TraceHelper.INSTANCE = helper;
+        Binder.setProxyTransactListener(helper);
+
+        return () -> {
+            Binder.setProxyTransactListener(null);
+            TraceHelper.INSTANCE = current;
+        };
+    }
+
+    private static final LinkedList<String> mMainThreadTraceStack = new LinkedList<>();
+    private static final LinkedList<String> mMainThreadIgnoreIpcStack = new LinkedList<>();
+
+    private static class TraceHelperExtension extends TraceHelper implements ProxyTransactListener {
+
+        private final Consumer<BinderCallSite> mUnexpectedTransactionCallback;
+
+        TraceHelperExtension(Consumer<BinderCallSite> unexpectedTransactionCallback) {
+            mUnexpectedTransactionCallback = unexpectedTransactionCallback;
+        }
+
+        @Override
+        public void beginSection(String sectionName) {
+            if (isMainThread()) {
+                mMainThreadTraceStack.add(sectionName);
+            }
+            super.beginSection(sectionName);
+        }
+
+        @Override
+        public SafeCloseable beginAsyncSection(String sectionName) {
+            if (!isMainThread()) {
+                return super.beginAsyncSection(sectionName);
+            }
+
+            mMainThreadTraceStack.add(sectionName);
+            int cookie = Random.Default.nextInt();
+            Trace.beginAsyncSection(sectionName, cookie);
+            return () -> {
+                Trace.endAsyncSection(sectionName, cookie);
+                mMainThreadTraceStack.remove(sectionName);
+            };
+        }
+
+        @Override
+        public void endSection() {
+            super.endSection();
+            if (isMainThread()) {
+                mMainThreadTraceStack.pollLast();
+            }
+        }
+
+        @Override
+        public SafeCloseable allowIpcs(String rpcName) {
+            if (!isMainThread()) {
+                return super.allowIpcs(rpcName);
+            }
+
+            mMainThreadTraceStack.add(rpcName);
+            mMainThreadIgnoreIpcStack.add(rpcName);
+            int cookie = Random.Default.nextInt();
+            Trace.beginAsyncSection(rpcName, cookie);
+            return () -> {
+                Trace.endAsyncSection(rpcName, cookie);
+                mMainThreadTraceStack.remove(rpcName);
+                mMainThreadIgnoreIpcStack.remove(rpcName);
+            };
+        }
+
+        @Override
+        public Object onTransactStarted(IBinder binder, int transactionCode, int flags) {
+            if (!isMainThread() || (flags & FLAG_ONEWAY) == FLAG_ONEWAY) {
+                return null;
+            }
+
+            String ipcBypass = mMainThreadIgnoreIpcStack.peekLast();
+            String descriptor;
+            try {
+                descriptor = binder.getInterfaceDescriptor();
+                if (sAllowedFrameworkClasses.contains(descriptor)) {
+                    return null;
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error getting IPC descriptor", e);
+                descriptor = binder.getClass().getSimpleName();
+            }
+
+            if (ipcBypass == null) {
+                mUnexpectedTransactionCallback.accept(new BinderCallSite(
+                        mMainThreadTraceStack.peekLast(), descriptor, transactionCode));
+            } else {
+                Log.d(TAG, "MainThread-IPC " + descriptor + " ignored due to " + ipcBypass);
+            }
+            return null;
+        }
+
+        @Override
+        public Object onTransactStarted(IBinder binder, int transactionCode) {
+            // Do nothing
+            return null;
+        }
+
+        @Override
+        public void onTransactEnded(Object session) {
+            // Do nothing
+        }
+    }
+
+    private static boolean isMainThread() {
+        return Thread.currentThread() == Looper.getMainLooper().getThread();
+    }
+
+    /**
+     * Information about a binder call
+     */
+    public static class BinderCallSite {
+
+        @Nullable
+        public final String activeTrace;
+        public final String descriptor;
+        public final int transactionCode;
+
+        BinderCallSite(String activeTrace, String descriptor, int transactionCode) {
+            this.activeTrace = activeTrace;
+            this.descriptor = descriptor;
+            this.transactionCode = transactionCode;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
index eff53f3..c5a88bc 100644
--- a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -19,13 +19,13 @@
 import static android.content.Intent.EXTRA_COMPONENT_NAME;
 import static android.content.Intent.EXTRA_USER;
 
+import static com.android.app.animation.Interpolators.ACCELERATE;
 import static com.android.launcher3.GestureNavContract.EXTRA_GESTURE_CONTRACT;
 import static com.android.launcher3.GestureNavContract.EXTRA_ICON_POSITION;
 import static com.android.launcher3.GestureNavContract.EXTRA_ICON_SURFACE;
 import static com.android.launcher3.GestureNavContract.EXTRA_ON_FINISH_CALLBACK;
 import static com.android.launcher3.GestureNavContract.EXTRA_REMOTE_CALLBACK;
 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
-import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.quickstep.OverviewComponentObserver.startHomeIntentSafely;
 
 import android.animation.ObjectAnimator;
@@ -296,7 +296,7 @@
         @Override
         public AnimatorPlaybackController createActivityAnimationToHome() {
             PendingAnimation pa = new PendingAnimation(mDuration);
-            pa.setFloat(mRecentsAlpha, AnimatedFloat.VALUE, 0, ACCEL);
+            pa.setFloat(mRecentsAlpha, AnimatedFloat.VALUE, 0, ACCELERATE);
             return pa.createPlaybackController();
         }
 
@@ -324,7 +324,7 @@
         @Override
         public void playAtomicAnimation(float velocity) {
             ObjectAnimator alphaAnim = mHomeAlpha.animateToValue(mHomeAlpha.value, 1);
-            alphaAnim.setDuration(mDuration).setInterpolator(ACCEL);
+            alphaAnim.setDuration(mDuration).setInterpolator(ACCELERATE);
             alphaAnim.start();
 
             if (mRunningOverHome) {
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 9d7ccb4..c2d8c62 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -15,8 +15,9 @@
  */
 package com.android.quickstep;
 
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+
 import static com.android.launcher3.MotionEventsUtils.isTrackpadFourFingerSwipe;
-import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
 import static com.android.launcher3.MotionEventsUtils.isTrackpadThreeFingerSwipe;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
@@ -37,8 +38,6 @@
 
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.tracing.GestureStateProto;
-import com.android.launcher3.tracing.SwipeHandlerProto;
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
 import com.android.quickstep.util.ActiveGestureErrorDetector;
 import com.android.quickstep.util.ActiveGestureLog;
@@ -46,10 +45,12 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Predicate;
 
 /**
  * Manages the state for an active system gesture, listens for events from the system and Launcher,
@@ -58,28 +59,37 @@
 @TargetApi(Build.VERSION_CODES.R)
 public class GestureState implements RecentsAnimationCallbacks.RecentsAnimationListener {
 
+    final Predicate<RemoteAnimationTarget> mLastStartedTaskIdPredicate = new Predicate<>() {
+        @Override
+        public boolean test(RemoteAnimationTarget targetCompat) {
+            for (int taskId : mLastStartedTaskId) {
+                if (targetCompat.taskId == taskId) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    };
+
     /**
      * Defines the end targets of a gesture and the associated state.
      */
     public enum GestureEndTarget {
-        HOME(true, LAUNCHER_STATE_HOME, false, GestureStateProto.GestureEndTarget.HOME),
+        HOME(true, LAUNCHER_STATE_HOME, false),
 
-        RECENTS(true, LAUNCHER_STATE_OVERVIEW, true, GestureStateProto.GestureEndTarget.RECENTS),
+        RECENTS(true, LAUNCHER_STATE_OVERVIEW, true),
 
-        NEW_TASK(false, LAUNCHER_STATE_BACKGROUND, true,
-                GestureStateProto.GestureEndTarget.NEW_TASK),
+        NEW_TASK(false, LAUNCHER_STATE_BACKGROUND, true),
 
-        LAST_TASK(false, LAUNCHER_STATE_BACKGROUND, true,
-                GestureStateProto.GestureEndTarget.LAST_TASK),
+        LAST_TASK(false, LAUNCHER_STATE_BACKGROUND, true),
 
-        ALL_APPS(true, LAUNCHER_STATE_ALLAPPS, false, GestureStateProto.GestureEndTarget.ALL_APPS);
+        ALL_APPS(true, LAUNCHER_STATE_ALLAPPS, false);
 
-        GestureEndTarget(boolean isLauncher, int containerType, boolean recentsAttachedToAppWindow,
-                GestureStateProto.GestureEndTarget protoEndTarget) {
+        GestureEndTarget(boolean isLauncher, int containerType,
+                boolean recentsAttachedToAppWindow) {
             this.isLauncher = isLauncher;
             this.containerType = containerType;
             this.recentsAttachedToAppWindow = recentsAttachedToAppWindow;
-            this.protoEndTarget = protoEndTarget;
         }
 
         /** Whether the target is in the launcher activity. Implicitly, if the end target is going
@@ -89,8 +99,6 @@
         public final int containerType;
         /** Whether RecentsView should be attached to the window as we animate to this target */
         public final boolean recentsAttachedToAppWindow;
-        /** The GestureStateProto enum value, used for winscope tracing. See launcher_trace.proto */
-        public final GestureStateProto.GestureEndTarget protoEndTarget;
     }
 
     private static final String TAG = "GestureState";
@@ -150,15 +158,10 @@
 
     public enum TrackpadGestureType {
         NONE,
-        // Assigned before we know whether it's a 3-finger or 4-finger gesture.
-        MULTI_FINGER,
         THREE_FINGER,
         FOUR_FINGER;
 
         public static TrackpadGestureType getTrackpadGestureType(MotionEvent event) {
-            if (!isTrackpadMultiFingerSwipe(event)) {
-                return TrackpadGestureType.NONE;
-            }
             if (isTrackpadThreeFingerSwipe(event)) {
                 return TrackpadGestureType.THREE_FINGER;
             }
@@ -166,16 +169,16 @@
                 return TrackpadGestureType.FOUR_FINGER;
             }
 
-            return TrackpadGestureType.MULTI_FINGER;
+            return TrackpadGestureType.NONE;
         }
     }
 
     private TrackpadGestureType mTrackpadGestureType = TrackpadGestureType.NONE;
     private CachedTaskInfo mRunningTask;
     private GestureEndTarget mEndTarget;
-    private RemoteAnimationTarget mLastAppearedTaskTarget;
+    private RemoteAnimationTarget[] mLastAppearedTaskTargets;
     private Set<Integer> mPreviouslyAppearedTaskIds = new HashSet<>();
-    private int mLastStartedTaskId = -1;
+    private int[] mLastStartedTaskId = new int[]{INVALID_TASK_ID, INVALID_TASK_ID};
     private RecentsAnimationController mRecentsAnimationController;
     private HashMap<Integer, ThumbnailData> mRecentsAnimationCanceledSnapshots;
 
@@ -201,7 +204,7 @@
         mGestureId = other.mGestureId;
         mRunningTask = other.mRunningTask;
         mEndTarget = other.mEndTarget;
-        mLastAppearedTaskTarget = other.mLastAppearedTaskTarget;
+        mLastAppearedTaskTargets = other.mLastAppearedTaskTargets;
         mPreviouslyAppearedTaskIds = other.mPreviouslyAppearedTaskIds;
         mLastStartedTaskId = other.mLastStartedTaskId;
     }
@@ -305,10 +308,29 @@
     }
 
     /**
-     * @return the running task id for this gesture.
+     * @param getMultipleTasks Whether multiple tasks or not are to be returned (for split)
+     * @return the running task ids for this gesture.
      */
-    public int getRunningTaskId() {
-        return mRunningTask != null ? mRunningTask.getTaskId() : -1;
+    public int[] getRunningTaskIds(boolean getMultipleTasks) {
+        if (mRunningTask == null) {
+            return new int[]{INVALID_TASK_ID, INVALID_TASK_ID};
+        } else {
+            int cachedTasksSize = mRunningTask.mAllCachedTasks.size();
+            int count = Math.min(cachedTasksSize, getMultipleTasks ? 2 : 1);
+            int[] runningTaskIds = new int[count];
+            for (int i = 0; i < count; i++) {
+                runningTaskIds[i] = mRunningTask.mAllCachedTasks.get(i).taskId;
+            }
+            return runningTaskIds;
+        }
+    }
+
+    /**
+     * @see #getRunningTaskIds(boolean)
+     * @return the single top-most running taskId for this gesture
+     */
+    public int getTopRunningTaskId() {
+        return getRunningTaskIds(false /*getMultipleTasks*/)[0];
     }
 
     /**
@@ -321,18 +343,26 @@
     /**
      * Updates the last task that appeared during this gesture.
      */
-    public void updateLastAppearedTaskTarget(RemoteAnimationTarget lastAppearedTaskTarget) {
-        mLastAppearedTaskTarget = lastAppearedTaskTarget;
-        if (lastAppearedTaskTarget != null) {
-            mPreviouslyAppearedTaskIds.add(lastAppearedTaskTarget.taskId);
+    public void updateLastAppearedTaskTargets(RemoteAnimationTarget[] lastAppearedTaskTargets) {
+        mLastAppearedTaskTargets = lastAppearedTaskTargets;
+        for (RemoteAnimationTarget target : lastAppearedTaskTargets) {
+            if (target == null) {
+                continue;
+            }
+            mPreviouslyAppearedTaskIds.add(target.taskId);
         }
     }
 
     /**
      * @return The id of the task that appeared during this gesture.
      */
-    public int getLastAppearedTaskId() {
-        return mLastAppearedTaskTarget != null ? mLastAppearedTaskTarget.taskId : -1;
+    public int[] getLastAppearedTaskIds() {
+        if (mLastAppearedTaskTargets == null) {
+            return new int[]{INVALID_TASK_ID, INVALID_TASK_ID};
+        } else {
+            return Arrays.stream(mLastAppearedTaskTargets)
+                    .mapToInt(target -> target != null ? target.taskId : INVALID_TASK_ID).toArray();
+        }
     }
 
     public void updatePreviouslyAppearedTaskIds(Set<Integer> previouslyAppearedTaskIds) {
@@ -346,7 +376,7 @@
     /**
      * Updates the last task that we started via startActivityFromRecents() during this gesture.
      */
-    public void updateLastStartedTaskId(int lastStartedTaskId) {
+    public void updateLastStartedTaskIds(int[] lastStartedTaskId) {
         mLastStartedTaskId = lastStartedTaskId;
     }
 
@@ -354,7 +384,7 @@
      * @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() {
+    public int[] getLastStartedTaskIds() {
         return mLastStartedTaskId;
     }
 
@@ -490,21 +520,8 @@
         pw.println("  gestureID=" + mGestureId);
         pw.println("  runningTask=" + mRunningTask);
         pw.println("  endTarget=" + mEndTarget);
-        pw.println("  lastAppearedTaskTargetId=" + getLastAppearedTaskId());
-        pw.println("  lastStartedTaskId=" + mLastStartedTaskId);
+        pw.println("  lastAppearedTaskTargetId=" + Arrays.toString(mLastAppearedTaskTargets));
+        pw.println("  lastStartedTaskId=" + Arrays.toString(mLastStartedTaskId));
         pw.println("  isRecentsAnimationRunning=" + isRecentsAnimationRunning());
     }
-
-    /**
-     * Used for winscope tracing, see launcher_trace.proto
-     * @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto
-     * @param swipeHandlerProto The parent of this proto message.
-     */
-    public void writeToProto(SwipeHandlerProto.Builder swipeHandlerProto) {
-        GestureStateProto.Builder gestureStateProto = GestureStateProto.newBuilder();
-        gestureStateProto.setEndTarget(mEndTarget == null
-                ? GestureStateProto.GestureEndTarget.UNSET
-                : mEndTarget.protoEndTarget);
-        swipeHandlerProto.setGestureState(gestureStateProto);
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index 6b189cf..23def14 100644
--- a/quickstep/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -21,9 +21,6 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
-import com.android.launcher3.tracing.InputConsumerProto;
-import com.android.launcher3.tracing.TouchInteractionServiceProto;
-
 @TargetApi(Build.VERSION_CODES.O)
 public interface InputConsumer {
 
@@ -42,6 +39,7 @@
     int TYPE_TASKBAR_STASH = 1 << 12;
     int TYPE_STATUS_BAR = 1 << 13;
     int TYPE_CURSOR_HOVER = 1 << 14;
+    int TYPE_NAV_HANDLE_LONG_PRESS = 1 << 15;
 
     String[] NAMES = new String[] {
            "TYPE_NO_OP",                    // 0
@@ -59,6 +57,7 @@
             "TYPE_TASKBAR_STASH",           // 12
             "TYPE_STATUS_BAR",              // 13
             "TYPE_CURSOR_HOVER",            // 14
+            "TYPE_NAV_HANDLE_LONG_PRESS",   // 15
     };
 
     InputConsumer NO_OP = () -> TYPE_NO_OP;
@@ -127,21 +126,4 @@
         }
         return name.toString();
     }
-
-    /**
-     * Used for winscope tracing, see launcher_trace.proto
-     * @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto
-     * @param serviceProto The parent of this proto message.
-     */
-    default void writeToProto(TouchInteractionServiceProto.Builder serviceProto) {
-        InputConsumerProto.Builder inputConsumerProto = InputConsumerProto.newBuilder();
-        inputConsumerProto.setName(getName());
-        writeToProtoInternal(inputConsumerProto);
-        serviceProto.setInputConsumer(inputConsumerProto);
-    }
-
-    /**
-     * @see #writeToProto - allows subclasses to write additional info to the proto.
-     */
-    default void writeToProtoInternal(InputConsumerProto.Builder inputConsumerProto) {}
 }
diff --git a/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java b/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java
index 7638541..33a2366 100644
--- a/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java
+++ b/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java
@@ -20,6 +20,9 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.Log;
 
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.util.InstantAppResolver;
@@ -49,4 +52,18 @@
         ComponentName cn = info.getTargetComponent();
         return cn != null && cn.getClassName().equals(COMPONENT_CLASS_MARKER);
     }
+
+    @Override
+    public boolean isInstantApp(String packageName, int userId) {
+        if (!Process.myUserHandle().equals(UserHandle.of(userId))) {
+            // Instant app can only exist on current user
+            return false;
+        }
+        try {
+            return mPM.isInstantApp(packageName);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to determine whether package is instant app " + packageName, e);
+            return false;
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 0e0b022..5784c37 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -15,12 +15,13 @@
  */
 package com.android.quickstep;
 
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.FLOATING_SEARCH_BAR;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 
@@ -267,7 +268,9 @@
 
     @Override
     public boolean allowAllAppsFromOverview() {
-        return FeatureFlags.ENABLE_ALL_APPS_FROM_OVERVIEW.get();
+        return FeatureFlags.ENABLE_ALL_APPS_FROM_OVERVIEW.get()
+                // If floating search bar would not show in overview, don't allow all apps gesture.
+                && OVERVIEW.areElementsVisible(getCreatedActivity(), FLOATING_SEARCH_BAR);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index c18ad5a..f1660ee 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -129,7 +129,7 @@
         mWindowMaxDeltaY = mLauncher.getResources().getDimensionPixelSize(
                 R.dimen.swipe_back_window_max_delta_y);
         mCancelInterpolator =
-                AnimationUtils.loadInterpolator(mLauncher, R.interpolator.back_cancel);
+                AnimationUtils.loadInterpolator(mLauncher, R.interpolator.standard_interpolator);
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index fd5c1a7..a9d8afc 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -15,10 +15,10 @@
  */
 package com.android.quickstep;
 
+import static com.android.app.animation.Interpolators.EXAGGERATED_EASE;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.Utilities.mapBoundToRange;
-import static com.android.launcher3.anim.Interpolators.EXAGGERATED_EASE;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 import static com.android.launcher3.views.FloatingIconView.getFloatingIconView;
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 07db194..42bf1ac 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -37,7 +37,6 @@
 import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.util.RunnableList;
 import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
-import com.android.quickstep.views.DesktopTaskView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -141,6 +140,11 @@
         mPendingCommands.clear();
     }
 
+    @UiThread
+    public boolean canStartHomeSafely() {
+        return mPendingCommands.isEmpty() || mPendingCommands.get(0).type == TYPE_HOME;
+    }
+
     @Nullable
     private TaskView getNextTask(RecentsView view) {
         final TaskView runningTaskView = view.getRunningTaskView();
@@ -186,11 +190,6 @@
                     && dp != null
                     && (dp.isTablet || dp.isTwoPanels);
 
-            if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) {
-                // TODO(b/268075592): add support for quickswitch to/from desktop
-                allowQuickSwitch = false;
-            }
-
             if (cmd.type == TYPE_HIDE) {
                 if (!allowQuickSwitch) {
                     return true;
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index a8f3c3a..60713cf 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -38,8 +38,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.R;
-import com.android.launcher3.tracing.OverviewComponentObserverProto;
-import com.android.launcher3.tracing.TouchInteractionServiceProto;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.systemui.shared.system.PackageManagerWrapper;
 
@@ -276,19 +274,6 @@
     }
 
     /**
-     * Used for winscope tracing, see launcher_trace.proto
-     * @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto
-     * @param serviceProto The parent of this proto message.
-     */
-    public void writeToProto(TouchInteractionServiceProto.Builder serviceProto) {
-        OverviewComponentObserverProto.Builder overviewComponentObserver =
-                OverviewComponentObserverProto.newBuilder();
-        overviewComponentObserver.setOverviewActivityStarted(mActivityInterface.isStarted());
-        overviewComponentObserver.setOverviewActivityResumed(mActivityInterface.isResumed());
-        serviceProto.setOverviewComponentObvserver(overviewComponentObserver);
-    }
-
-    /**
      * Starts the intent for the current home activity.
      */
     public static void startHomeIntentSafely(@NonNull Context context, @Nullable Bundle options) {
diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
index 5f589bf..d1939ef 100644
--- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
+++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -19,6 +19,8 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Build;
+import android.os.Looper;
+import android.os.Trace;
 import android.os.UserManager;
 import android.util.Log;
 import android.view.ThreadedRenderer;
@@ -60,5 +62,14 @@
         // Elevate GPU priority for Quickstep and Remote animations.
         ThreadedRenderer.setContextPriority(
                 ThreadedRenderer.EGL_CONTEXT_PRIORITY_HIGH_IMG);
+
+        // Enable Looper trace points.
+        // This allows us to see Handler callbacks on traces.
+        Looper.getMainLooper().setTraceTag(Trace.TRACE_TAG_APP);
+
+        if (BuildConfig.IS_STUDIO_BUILD) {
+            BinderTracker.startTracking(call ->  Log.e("BinderCall",
+                    call.descriptor + " called on mainthread under " + call.activeTrace));
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 4c9cf8b..031d409 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -80,8 +80,7 @@
             }
 
             case TestProtocol.REQUEST_HAS_TIS: {
-                response.putBoolean(
-                        TestProtocol.REQUEST_HAS_TIS, true);
+                response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, true);
                 return response;
             }
 
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 38ac5bb..34817c0 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -308,7 +308,6 @@
             task.setLastSnapshotData(taskInfo);
             task.positionInParent = taskInfo.positionInParent;
             task.appBounds = taskInfo.configuration.windowConfiguration.getAppBounds();
-            // TODO(b/244348395): tasks should be sorted from oldest to most recently used
             tasks.add(task);
         }
         return new DesktopTask(tasks);
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 39af7fd..72439de 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -22,7 +22,6 @@
 import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
 import static com.android.launcher3.QuickstepTransitionManager.STATUS_BAR_TRANSITION_DURATION;
 import static com.android.launcher3.QuickstepTransitionManager.STATUS_BAR_TRANSITION_PRE_DELAY;
-import static com.android.launcher3.graphics.SysUiScrim.SYSUI_PROGRESS;
 import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL;
 import static com.android.quickstep.OverviewComponentObserver.startHomeIntentSafely;
 import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
@@ -38,6 +37,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Trace;
+import android.util.Log;
 import android.view.Display;
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationTarget;
@@ -46,8 +46,10 @@
 import android.window.RemoteTransition;
 import android.window.SplashScreen;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
@@ -56,7 +58,6 @@
 import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.model.data.ItemInfo;
@@ -66,6 +67,7 @@
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.taskbar.FallbackTaskbarUIController;
 import com.android.launcher3.taskbar.TaskbarManager;
+import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.ActivityOptionsWrapper;
 import com.android.launcher3.util.ActivityTracker;
 import com.android.launcher3.util.RunnableList;
@@ -107,7 +109,6 @@
     private FallbackRecentsView mFallbackRecentsView;
     private OverviewActionsView mActionsView;
     private TISBindHelper mTISBindHelper;
-    private @Nullable TaskbarManager mTaskbarManager;
     private @Nullable FallbackTaskbarUIController mTaskbarUIController;
 
     private StateManager<RecentsState> mStateManager;
@@ -119,6 +120,7 @@
     // animation callback
     private final Handler mHandler = new Handler();
     private final Runnable mAnimationStartTimeoutRunnable = this::onAnimationStartTimeout;
+    private SplitSelectStateController mSplitSelectStateController;
 
     /**
      * Init drag layer and overview panel views.
@@ -130,22 +132,21 @@
         mScrimView = findViewById(R.id.scrim_view);
         mFallbackRecentsView = findViewById(R.id.overview_panel);
         mActionsView = findViewById(R.id.overview_actions_view);
-        SYSUI_PROGRESS.set(getRootView().getSysUiScrim(), 0f);
-
-        SplitSelectStateController controller =
+        getRootView().getSysUiScrim().getSysUIProgress().updateValue(0);
+        mSplitSelectStateController =
                 new SplitSelectStateController(this, mHandler, getStateManager(),
-                         null /* depthController */, getStatsLogManager(),
+                        null /* depthController */, getStatsLogManager(),
                         SystemUiProxy.INSTANCE.get(this), RecentsModel.INSTANCE.get(this));
         mDragLayer.recreateControllers();
-        mFallbackRecentsView.init(mActionsView, controller);
+        mFallbackRecentsView.init(mActionsView, mSplitSelectStateController);
 
         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
     }
 
     private void onTISConnected(TouchInteractionService.TISBinder binder) {
-        mTaskbarManager = binder.getTaskbarManager();
-        if (mTaskbarManager != null) {
-            mTaskbarManager.setActivity(this);
+        TaskbarManager taskbarManager = binder.getTaskbarManager();
+        if (taskbarManager != null) {
+            taskbarManager.setActivity(this);
         }
     }
 
@@ -241,6 +242,11 @@
         }
 
         final TaskView taskView = (TaskView) v;
+        final RecentsView recentsView = taskView.getRecentsView();
+        if (recentsView == null) {
+            return super.getActivityLaunchOptions(v, item);
+        }
+
         RunnableList onEndCallback = new RunnableList();
 
         mActivityLaunchAnimationRunner = new RemoteAnimationFactory() {
@@ -249,7 +255,7 @@
                     RemoteAnimationTarget[] wallpaperTargets,
                     RemoteAnimationTarget[] nonAppTargets, AnimationResult result) {
                 mHandler.removeCallbacks(mAnimationStartTimeoutRunnable);
-                AnimatorSet anim = composeRecentsLaunchAnimator(taskView, appTargets,
+                AnimatorSet anim = composeRecentsLaunchAnimator(recentsView, taskView, appTargets,
                         wallpaperTargets, nonAppTargets);
                 anim.addListener(resetStateListener());
                 result.setAnimation(anim, RecentsActivity.this, onEndCallback::executeAllAndDestroy,
@@ -277,6 +283,8 @@
         activityOptions.options.setLaunchDisplayId(
                 (v != null && v.getDisplay() != null) ? v.getDisplay().getDisplayId()
                         : Display.DEFAULT_DISPLAY);
+        activityOptions.options.setPendingIntentBackgroundActivityStartMode(
+                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
         mHandler.postDelayed(mAnimationStartTimeoutRunnable, RECENTS_ANIMATION_TIMEOUT);
         return activityOptions;
     }
@@ -284,14 +292,16 @@
     /**
      * Composes the animations for a launch from the recents list if possible.
      */
-    private AnimatorSet  composeRecentsLaunchAnimator(TaskView taskView,
+    private AnimatorSet  composeRecentsLaunchAnimator(
+            @NonNull RecentsView recentsView,
+            @NonNull TaskView taskView,
             RemoteAnimationTarget[] appTargets,
             RemoteAnimationTarget[] wallpaperTargets,
             RemoteAnimationTarget[] nonAppTargets) {
         AnimatorSet target = new AnimatorSet();
         boolean activityClosing = taskIsATargetWithMode(appTargets, getTaskId(), MODE_CLOSING);
         PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
-        createRecentsWindowAnimator(taskView, !activityClosing, appTargets,
+        createRecentsWindowAnimator(recentsView, taskView, !activityClosing, appTargets,
                 wallpaperTargets, nonAppTargets, null /* depthController */, pa);
         target.play(pa.buildAnim());
 
@@ -299,7 +309,7 @@
         if (activityClosing) {
             Animator adjacentAnimation = mFallbackRecentsView
                     .createAdjacentPageAnimForTaskLaunch(taskView);
-            adjacentAnimation.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
+            adjacentAnimation.setInterpolator(Interpolators.TOUCH_RESPONSE);
             adjacentAnimation.setDuration(RECENTS_LAUNCH_DURATION);
             adjacentAnimation.addListener(resetStateListener());
             target.play(adjacentAnimation);
@@ -384,11 +394,8 @@
         super.onDestroy();
         ACTIVITY_TRACKER.onActivityDestroyed(this);
         mActivityLaunchAnimationRunner = null;
-
+        mSplitSelectStateController.onDestroy();
         mTISBindHelper.onDestroy();
-        if (mTaskbarManager != null) {
-            mTaskbarManager.clearActivity(this);
-        }
     }
 
     @Override
@@ -398,6 +405,7 @@
     }
 
     public void startHome() {
+        Log.d(TestProtocol.INCORRECT_HOME_STATE, "start home from recents activity");
         RecentsView recentsView = getOverviewPanel();
         recentsView.switchToScreenshot(() -> recentsView.finishRecentsAnimation(true,
                 this::startHomeInternal));
@@ -471,4 +479,9 @@
             }
         };
     }
+
+    public boolean canStartHomeSafely() {
+        OverviewCommandHelper overviewCommandHelper = mTISBindHelper.getOverviewCommandHelper();
+        return overviewCommandHelper == null || overviewCommandHelper.canStartHomeSafely();
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index f8e09e1..8972dc8 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -115,8 +115,8 @@
      * {@link RecentsAnimationCallbacks#onTasksAppeared}}.
      */
     @UiThread
-    public void removeTaskTarget(@NonNull RemoteAnimationTarget target) {
-        UI_HELPER_EXECUTOR.execute(() -> mController.removeTask(target.taskId));
+    public void removeTaskTarget(int targetTaskId) {
+        UI_HELPER_EXECUTOR.execute(() -> mController.removeTask(targetTaskId));
     }
 
     @UiThread
@@ -167,7 +167,6 @@
                 /* event= */ "finishRecentsAnimation",
                 /* extras= */ toRecents,
                 /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
-
         // Finish not yet requested
         mFinishRequested = true;
         mFinishTargetIsLauncher = toRecents;
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 64ec1d8..1448a52 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -17,7 +17,6 @@
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.content.Intent.ACTION_USER_UNLOCKED;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.launcher3.util.DisplayController.CHANGE_ALL;
@@ -52,10 +51,8 @@
 import android.graphics.Region;
 import android.inputmethodservice.InputMethodService;
 import android.net.Uri;
-import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemProperties;
-import android.os.UserManager;
 import android.provider.Settings;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
@@ -68,7 +65,6 @@
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.SettingsCache;
-import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
 import com.android.quickstep.util.NavBarPosition;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -116,15 +112,6 @@
     private final boolean mIsOneHandedModeSupported;
     private boolean mPipIsActive;
 
-    private boolean mIsUserUnlocked;
-    private final ArrayList<Runnable> mUserUnlockedActions = new ArrayList<>();
-    private final SimpleBroadcastReceiver mUserUnlockedReceiver = new SimpleBroadcastReceiver(i -> {
-        if (ACTION_USER_UNLOCKED.equals(i.getAction())) {
-            mIsUserUnlocked = true;
-            notifyUserUnlocked();
-        }
-    });
-
     private int mGestureBlockingTaskId = -1;
     private @NonNull Region mExclusionRegion = new Region();
     private SystemGestureExclusionListenerCompat mExclusionListener;
@@ -150,14 +137,6 @@
             runOnDestroy(mRotationTouchHelper::destroy);
         }
 
-        // Register for user unlocked if necessary
-        mIsUserUnlocked = context.getSystemService(UserManager.class)
-                .isUserUnlocked(Process.myUserHandle());
-        if (!mIsUserUnlocked) {
-            mUserUnlockedReceiver.register(mContext, ACTION_USER_UNLOCKED);
-        }
-        runOnDestroy(() -> mUserUnlockedReceiver.unregisterReceiverSafely(mContext));
-
         // Register for exclusion updates
         mExclusionListener = new SystemGestureExclusionListenerCompat(mDisplayId) {
             @Override
@@ -317,39 +296,12 @@
     }
 
     /**
-     * Adds a callback for when a user is unlocked. If the user is already unlocked, this listener
-     * will be called back immediately.
-     */
-    public void runOnUserUnlocked(Runnable action) {
-        if (mIsUserUnlocked) {
-            action.run();
-        } else {
-            mUserUnlockedActions.add(action);
-        }
-    }
-
-    /**
-     * @return whether the user is unlocked.
-     */
-    public boolean isUserUnlocked() {
-        return mIsUserUnlocked;
-    }
-
-    /**
      * @return whether the user has completed setup wizard
      */
     public boolean isUserSetupComplete() {
         return mIsUserSetupComplete;
     }
 
-    private void notifyUserUnlocked() {
-        for (Runnable action : mUserUnlockedActions) {
-            action.run();
-        }
-        mUserUnlockedActions.clear();
-        mUserUnlockedReceiver.unregisterReceiverSafely(mContext);
-    }
-
     /**
      * Sets the task id where gestures should be blocked
      */
@@ -386,8 +338,16 @@
         boolean canStartWithNavHidden = (mSystemUiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0
                 || (mSystemUiStateFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0
                 || mRotationTouchHelper.isTaskListFrozen();
-        return canStartWithNavHidden
-                && (mSystemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0
+        return canStartWithNavHidden && canStartTrackpadGesture();
+    }
+
+    /**
+     * @return whether SystemUI is in a state where we can start a system gesture from the trackpad.
+     * Trackpad gestures can start even when the nav bar / task bar is hidden in sticky immersive
+     * mode.
+     */
+    public boolean canStartTrackpadGesture() {
+        return (mSystemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0
                 && (mSystemUiStateFlags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING) == 0
                 && (mSystemUiStateFlags & SYSUI_STATE_QUICK_SETTINGS_EXPANDED) == 0
                 && (mSystemUiStateFlags & SYSUI_STATE_MAGNIFICATION_OVERLAP) == 0
@@ -607,7 +567,6 @@
         pw.println("  assistantAvailable=" + mAssistantAvailable);
         pw.println("  assistantDisabled="
                 + QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags));
-        pw.println("  isUserUnlocked=" + mIsUserUnlocked);
         pw.println("  isOneHandedModeEnabled=" + mIsOneHandedModeEnabled);
         pw.println("  isSwipeToNotificationEnabled=" + mIsSwipeToNotificationEnabled);
         pw.println("  deferredGestureRegion=" + mDeferredGestureRegion.getBounds());
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index 8626c40..2d47097 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -19,6 +19,7 @@
 import static android.view.Surface.ROTATION_0;
 
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
+import static com.android.launcher3.MotionEventsUtils.isTrackpadScroll;
 import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
 import static com.android.launcher3.util.DisplayController.CHANGE_ALL;
 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
@@ -232,16 +233,18 @@
     /**
      * @return whether the coordinates of the {@param event} is in the swipe up gesture region.
      */
-    public boolean isInSwipeUpTouchRegion(MotionEvent event, BaseActivityInterface activity) {
-        return isInSwipeUpTouchRegion(event, 0, activity);
+    public boolean isInSwipeUpTouchRegion(MotionEvent event) {
+        return isInSwipeUpTouchRegion(event, 0);
     }
 
     /**
      * @return whether the coordinates of the {@param event} with the given {@param pointerIndex}
      *         is in the swipe up gesture region.
      */
-    public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex,
-            BaseActivityInterface activity) {
+    public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex) {
+        if (isTrackpadScroll(event)) {
+            return false;
+        }
         if (isTrackpadMultiFingerSwipe(event)) {
             return true;
         }
diff --git a/quickstep/src/com/android/quickstep/SplitSelectionListener.kt b/quickstep/src/com/android/quickstep/SplitSelectionListener.kt
new file mode 100644
index 0000000..5025c1c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/SplitSelectionListener.kt
@@ -0,0 +1,17 @@
+package com.android.quickstep
+
+interface SplitSelectionListener {
+    /** Called when the first app has been selected with the intention to launch split screen */
+    fun onSplitSelectionActive()
+
+    /** Called when the second app has been selected with the intention to launch split screen */
+    fun onSplitSelectionConfirmed()
+
+    /**
+     * Called when the user no longer is in the process of selecting apps for split screen.
+     * [launchedSplit] will be true if selected apps have launched successfully (either in
+     * split screen or fullscreen), false if the user canceled/exited the selection process
+     */
+    fun onSplitSelectionExit(launchedSplit: Boolean) {
+    }
+}
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index 25ac47a..e481165 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -15,8 +15,8 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.ACCELERATE_1_5;
+import static com.android.app.animation.Interpolators.LINEAR;
 
 import android.animation.Animator;
 import android.content.Context;
@@ -218,7 +218,7 @@
             if (progress >= end) {
                 return 0f;
             }
-            return Utilities.mapToRange(progress, start, end, 1, 0, ACCEL_1_5);
+            return Utilities.mapToRange(progress, start, end, 1, 0, ACCELERATE_1_5);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 9350c72..fae929a 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -19,6 +19,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.util.LogUtils.splitFailureMessage;
 
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
@@ -61,6 +62,7 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.SplitConfigurationOptions;
+import com.android.quickstep.util.AssistUtils;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -74,6 +76,7 @@
 import com.android.wm.shell.bubbles.IBubbles;
 import com.android.wm.shell.bubbles.IBubblesListener;
 import com.android.wm.shell.desktopmode.IDesktopMode;
+import com.android.wm.shell.desktopmode.IDesktopTaskListener;
 import com.android.wm.shell.draganddrop.IDragAndDrop;
 import com.android.wm.shell.onehanded.IOneHanded;
 import com.android.wm.shell.pip.IPip;
@@ -82,6 +85,7 @@
 import com.android.wm.shell.recents.IRecentTasksListener;
 import com.android.wm.shell.splitscreen.ISplitScreen;
 import com.android.wm.shell.splitscreen.ISplitScreenListener;
+import com.android.wm.shell.splitscreen.ISplitSelectListener;
 import com.android.wm.shell.startingsurface.IStartingWindow;
 import com.android.wm.shell.startingsurface.IStartingWindowListener;
 import com.android.wm.shell.transition.IShellTransitions;
@@ -126,10 +130,12 @@
     private IPipAnimationListener mPipAnimationListener;
     private IBubblesListener mBubblesListener;
     private ISplitScreenListener mSplitScreenListener;
+    private ISplitSelectListener mSplitSelectListener;
     private IStartingWindowListener mStartingWindowListener;
     private ILauncherUnlockAnimationController mLauncherUnlockAnimationController;
     private IRecentTasksListener mRecentTasksListener;
     private IUnfoldTransitionListener mUnfoldAnimationListener;
+    private IDesktopTaskListener mDesktopTaskListener;
     private final LinkedHashMap<RemoteTransition, TransitionFilter> mRemoteTransitions =
             new LinkedHashMap<>();
     private IBinder mOriginalTransactionToken = null;
@@ -236,6 +242,7 @@
         setPipAnimationListener(mPipAnimationListener);
         setBubblesListener(mBubblesListener);
         registerSplitScreenListener(mSplitScreenListener);
+        registerSplitSelectListener(mSplitSelectListener);
         setStartingWindowListener(mStartingWindowListener);
         setLauncherUnlockAnimationController(mLauncherUnlockAnimationController);
         new LinkedHashMap<>(mRemoteTransitions).forEach(this::registerRemoteTransition);
@@ -243,6 +250,9 @@
         registerRecentTasksListener(mRecentTasksListener);
         setBackToLauncherCallback(mBackToLauncherCallback, mBackToLauncherRunner);
         setUnfoldAnimationListener(mUnfoldAnimationListener);
+        setDesktopTaskListener(mDesktopTaskListener);
+        setAssistantOverridesRequested(
+                AssistUtils.newInstance(mContext).getSysUiAssistOverrideInvocationTypes());
     }
 
     /**
@@ -311,13 +321,24 @@
 
     @MainThread
     @Override
-    public void onStatusBarMotionEvent(MotionEvent event) {
+    public void onStatusBarTouchEvent(MotionEvent event) {
         Preconditions.assertUIThread();
         if (mSystemUiProxy != null) {
             try {
-                mSystemUiProxy.onStatusBarMotionEvent(event);
+                mSystemUiProxy.onStatusBarTouchEvent(event);
             } catch (RemoteException e) {
-                Log.w(TAG, "Failed call onStatusBarMotionEvent", e);
+                Log.w(TAG, "Failed call onStatusBarTouchEvent with arg: " + event, e);
+            }
+        }
+    }
+
+    @Override
+    public void onStatusBarTrackpadEvent(MotionEvent event) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.onStatusBarTrackpadEvent(event);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call onStatusBarTrackpadEvent with arg: " + event, e);
             }
         }
     }
@@ -356,6 +377,17 @@
     }
 
     @Override
+    public void setAssistantOverridesRequested(int[] invocationTypes) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.setAssistantOverridesRequested(invocationTypes);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call setAssistantOverridesRequested", e);
+            }
+        }
+    }
+
+    @Override
     public void notifyAccessibilityButtonClicked(int displayId) {
         if (mSystemUiProxy != null) {
             try {
@@ -631,13 +663,15 @@
     /**
      * Tells SysUI to show the bubble with the provided key.
      * @param key the key of the bubble to show.
-     * @param onLauncherHome whether the bubble is showing on launcher home or not (modifies where
-     *                       the expanded bubble view is placed).
+     * @param bubbleBarOffsetX the offset of the bubble bar from the edge of the screen on the X
+     *                         axis.
+     * @param bubbleBarOffsetY the offset of the bubble bar from the edge of the screen on the Y
+     *                         axis.
      */
-    public void showBubble(String key, boolean onLauncherHome) {
+    public void showBubble(String key, int bubbleBarOffsetX, int bubbleBarOffsetY) {
         if (mBubbles != null) {
             try {
-                mBubbles.showBubble(key, onLauncherHome);
+                mBubbles.showBubble(key, bubbleBarOffsetX, bubbleBarOffsetY);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call showBubble");
             }
@@ -645,6 +679,31 @@
     }
 
     /**
+     * Tells SysUI to remove the bubble with the provided key.
+     * @param key the key of the bubble to show.
+     */
+    public void removeBubble(String key) {
+        if (mBubbles == null) return;
+        try {
+            mBubbles.removeBubble(key);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call removeBubble");
+        }
+    }
+
+    /**
+     * Tells SysUI to remove all bubbles.
+     */
+    public void removeAllBubbles() {
+        if (mBubbles == null) return;
+        try {
+            mBubbles.removeAllBubbles();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call removeAllBubbles");
+        }
+    }
+
+    /**
      * Tells SysUI to collapse the bubbles.
      */
     public void collapseBubbles() {
@@ -657,6 +716,21 @@
         }
     }
 
+    /**
+     * Tells SysUI when the bubble is being dragged.
+     * Should be called only when the bubble bar is expanded.
+     * @param bubbleKey the key of the bubble to collapse/expand
+     * @param isBeingDragged whether the bubble is being dragged
+     */
+    public void onBubbleDrag(@Nullable String bubbleKey, boolean isBeingDragged) {
+        if (mBubbles == null) return;
+        try {
+            mBubbles.onBubbleDrag(bubbleKey, isBeingDragged);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call onBubbleDrag");
+        }
+    }
+
     //
     // Splitscreen
     //
@@ -683,6 +757,28 @@
         mSplitScreenListener = null;
     }
 
+    public void registerSplitSelectListener(ISplitSelectListener listener) {
+        if (mSplitScreen != null) {
+            try {
+                mSplitScreen.registerSplitSelectListener(listener);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call registerSplitSelectListener");
+            }
+        }
+        mSplitSelectListener = listener;
+    }
+
+    public void unregisterSplitSelectListener(ISplitSelectListener listener) {
+        if (mSplitScreen != null) {
+            try {
+                mSplitScreen.unregisterSplitSelectListener(listener);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call unregisterSplitSelectListener");
+            }
+        }
+        mSplitSelectListener = null;
+    }
+
     /** Start multiple tasks in split-screen simultaneously. */
     public void startTasks(int taskId1, Bundle options1, int taskId2, Bundle options2,
             @SplitConfigurationOptions.StagePosition int splitPosition, float splitRatio,
@@ -692,7 +788,7 @@
                 mSplitScreen.startTasks(taskId1, options1, taskId2, options2, splitPosition,
                         splitRatio, remoteTransition, instanceId);
             } catch (RemoteException e) {
-                Log.w(TAG, "Failed call startTasks");
+                Log.w(TAG, splitFailureMessage("startTasks", "RemoteException"), e);
             }
         }
     }
@@ -705,7 +801,7 @@
                 mSplitScreen.startIntentAndTask(pendingIntent, userId1, options1, taskId, options2,
                         splitPosition, splitRatio, remoteTransition, instanceId);
             } catch (RemoteException e) {
-                Log.w(TAG, "Failed call startIntentAndTask");
+                Log.w(TAG, splitFailureMessage("startIntentAndTask", "RemoteException"), e);
             }
         }
     }
@@ -721,7 +817,7 @@
                         pendingIntent2, userId2, shortcutInfo2, options2, splitPosition, splitRatio,
                         remoteTransition, instanceId);
             } catch (RemoteException e) {
-                Log.w(TAG, "Failed call startIntents");
+                Log.w(TAG, splitFailureMessage("startIntents", "RemoteException"), e);
             }
         }
     }
@@ -734,7 +830,7 @@
                 mSplitScreen.startShortcutAndTask(shortcutInfo, options1, taskId, options2,
                         splitPosition, splitRatio, remoteTransition, instanceId);
             } catch (RemoteException e) {
-                Log.w(TAG, "Failed call startShortcutAndTask");
+                Log.w(TAG, splitFailureMessage("startShortcutAndTask", "RemoteException"), e);
             }
         }
     }
@@ -750,7 +846,8 @@
                 mSplitScreen.startTasksWithLegacyTransition(taskId1, options1, taskId2, options2,
                         splitPosition, splitRatio, adapter, instanceId);
             } catch (RemoteException e) {
-                Log.w(TAG, "Failed call startTasksWithLegacyTransition");
+                Log.w(TAG, splitFailureMessage(
+                        "startTasksWithLegacyTransition", "RemoteException"), e);
             }
         }
     }
@@ -764,7 +861,8 @@
                 mSplitScreen.startIntentAndTaskWithLegacyTransition(pendingIntent, userId1,
                         options1, taskId, options2, splitPosition, splitRatio, adapter, instanceId);
             } catch (RemoteException e) {
-                Log.w(TAG, "Failed call startIntentAndTaskWithLegacyTransition");
+                Log.w(TAG, splitFailureMessage(
+                        "startIntentAndTaskWithLegacyTransition", "RemoteException"), e);
             }
         }
     }
@@ -777,7 +875,8 @@
                 mSplitScreen.startShortcutAndTaskWithLegacyTransition(shortcutInfo, options1,
                         taskId, options2, splitPosition, splitRatio, adapter, instanceId);
             } catch (RemoteException e) {
-                Log.w(TAG, "Failed call startShortcutAndTaskWithLegacyTransition");
+                Log.w(TAG, splitFailureMessage(
+                        "startShortcutAndTaskWithLegacyTransition", "RemoteException"), e);
             }
         }
     }
@@ -797,7 +896,8 @@
                         shortcutInfo1, options1, pendingIntent2, userId2, shortcutInfo2, options2,
                         sidePosition, splitRatio, adapter, instanceId);
             } catch (RemoteException e) {
-                Log.w(TAG, "Failed call startIntentsWithLegacyTransition");
+                Log.w(TAG, splitFailureMessage(
+                        "startIntentsWithLegacyTransition", "RemoteException"), e);
             }
         }
     }
@@ -809,7 +909,7 @@
                 mSplitScreen.startShortcut(packageName, shortcutId, position, options,
                         user, instanceId);
             } catch (RemoteException e) {
-                Log.w(TAG, "Failed call startShortcut");
+                Log.w(TAG, splitFailureMessage("startShortcut", "RemoteException"), e);
             }
         }
     }
@@ -821,7 +921,7 @@
                 mSplitScreen.startIntent(intent, userId, fillInIntent, position, options,
                         instanceId);
             } catch (RemoteException e) {
-                Log.w(TAG, "Failed call startIntent");
+                Log.w(TAG, splitFailureMessage("startIntent", "RemoteException"), e);
             }
         }
     }
@@ -1161,6 +1261,41 @@
         }
     }
 
+    /** Call shell to stash desktop apps */
+    public void stashDesktopApps(int displayId) {
+        if (mDesktopMode != null) {
+            try {
+                mDesktopMode.stashDesktopApps(displayId);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call stashDesktopApps", e);
+            }
+        }
+    }
+
+    /** Call shell to hide desktop apps that may be stashed */
+    public void hideStashedDesktopApps(int displayId) {
+        if (mDesktopMode != null) {
+            try {
+                mDesktopMode.hideStashedDesktopApps(displayId);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call hideStashedDesktopApps", e);
+            }
+        }
+    }
+
+    /**
+     * If task with the given id is on the desktop, bring it to front
+     */
+    public void showDesktopApp(int taskId) {
+        if (mDesktopMode != null) {
+            try {
+                mDesktopMode.showDesktopApp(taskId);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call showDesktopApp", e);
+            }
+        }
+    }
+
     /** Call shell to get number of visible freeform tasks */
     public int getVisibleDesktopTaskCount(int displayId) {
         if (mDesktopMode != null) {
@@ -1173,6 +1308,29 @@
         return 0;
     }
 
+    /** Set a listener on shell to get updates about desktop task state */
+    public void setDesktopTaskListener(@Nullable IDesktopTaskListener listener) {
+        mDesktopTaskListener = listener;
+        if (mDesktopMode != null) {
+            try {
+                mDesktopMode.setTaskListener(listener);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call setDesktopTaskListener", e);
+            }
+        }
+    }
+
+    /** Perform cleanup transactions after animation to split select is complete */
+    public void onDesktopSplitSelectAnimComplete(ActivityManager.RunningTaskInfo taskInfo) {
+        if (mDesktopMode != null) {
+            try {
+                mDesktopMode.onDesktopSplitSelectAnimComplete(taskInfo);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call onDesktopSplitSelectAnimComplete", e);
+            }
+        }
+    }
+
     //
     // Unfold transition
     //
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index eacca0d..2896fe0 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -38,6 +38,7 @@
 
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
 import com.android.quickstep.util.ActiveGestureLog;
@@ -61,7 +62,7 @@
     private RecentsAnimationTargets mTargets;
     // Temporary until we can hook into gesture state events
     private GestureState mLastGestureState;
-    private RemoteAnimationTarget mLastAppearedTaskTarget;
+    private RemoteAnimationTarget[] mLastAppearedTaskTargets;
     private Runnable mLiveTileCleanUpHandler;
     private Context mCtx;
 
@@ -124,13 +125,14 @@
             // If mCallbacks still != null, that means we are getting this startRecentsAnimation()
             // before the previous one got onRecentsAnimationStart(). In that case, cleanup the
             // previous animation so it doesn't mess up/listen to state changes in this animation.
-            cleanUpRecentsAnimation();
+            cleanUpRecentsAnimation(mCallbacks);
         }
 
         final BaseActivityInterface activityInterface = gestureState.getActivityInterface();
         mLastGestureState = gestureState;
-        mCallbacks = new RecentsAnimationCallbacks(SystemUiProxy.INSTANCE.get(mCtx),
-                activityInterface.allowMinimizeSplitScreen());
+        RecentsAnimationCallbacks newCallbacks = new RecentsAnimationCallbacks(
+                SystemUiProxy.INSTANCE.get(mCtx), activityInterface.allowMinimizeSplitScreen());
+        mCallbacks = newCallbacks;
         mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
             @Override
             public void onRecentsAnimationStart(RecentsAnimationController controller,
@@ -143,18 +145,25 @@
                 }
                 mController = controller;
                 mTargets = targets;
-                mLastAppearedTaskTarget = mTargets.findTask(mLastGestureState.getRunningTaskId());
-                mLastGestureState.updateLastAppearedTaskTarget(mLastAppearedTaskTarget);
+                // TODO(b/236226779): We can probably get away w/ setting mLastAppearedTaskTargets
+                //  to all appeared targets directly vs just looking at running ones
+                int[] runningTaskIds = mLastGestureState.getRunningTaskIds(targets.apps.length > 1);
+                mLastAppearedTaskTargets = new RemoteAnimationTarget[runningTaskIds.length];
+                for (int i = 0; i < runningTaskIds.length; i++) {
+                    RemoteAnimationTarget task = mTargets.findTask(runningTaskIds[i]);
+                    mLastAppearedTaskTargets[i] = task;
+                }
+                mLastGestureState.updateLastAppearedTaskTargets(mLastAppearedTaskTargets);
             }
 
             @Override
             public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
-                cleanUpRecentsAnimation();
+                cleanUpRecentsAnimation(newCallbacks);
             }
 
             @Override
             public void onRecentsAnimationFinished(RecentsAnimationController controller) {
-                cleanUpRecentsAnimation();
+                cleanUpRecentsAnimation(newCallbacks);
             }
 
             @Override
@@ -172,6 +181,9 @@
                         RecentsView recentsView =
                                 activityInterface.getCreatedActivity().getOverviewPanel();
                         if (recentsView != null) {
+                            Log.d(TestProtocol.INCORRECT_HOME_STATE,
+                                    "finish recents animation on "
+                                            + compat.taskInfo.description);
                             recentsView.finishRecentsAnimation(true, null);
                         }
                         return;
@@ -204,14 +216,18 @@
                             true /*shown*/, null /* animatorHandler */);
                 }
                 if (mController != null) {
-                    if (mLastAppearedTaskTarget == null
-                            || appearedTaskTarget.taskId != mLastAppearedTaskTarget.taskId) {
-                        if (mLastAppearedTaskTarget != null) {
-                            mController.removeTaskTarget(mLastAppearedTaskTarget);
+                    if (mLastAppearedTaskTargets != null) {
+                        for (RemoteAnimationTarget lastTarget : mLastAppearedTaskTargets) {
+                            for (RemoteAnimationTarget appearedTarget : appearedTaskTargets) {
+                                if (lastTarget != null &&
+                                        appearedTarget.taskId != lastTarget.taskId) {
+                                    mController.removeTaskTarget(lastTarget.taskId);
+                                }
+                            }
                         }
-                        mLastAppearedTaskTarget = appearedTaskTarget;
-                        mLastGestureState.updateLastAppearedTaskTarget(mLastAppearedTaskTarget);
                     }
+                    mLastAppearedTaskTargets = appearedTaskTargets;
+                    mLastGestureState.updateLastAppearedTaskTargets(mLastAppearedTaskTargets);
                 }
             }
 
@@ -276,7 +292,7 @@
         mCallbacks.addListener(gestureState);
         gestureState.setState(STATE_RECENTS_ANIMATION_INITIALIZED
                 | STATE_RECENTS_ANIMATION_STARTED);
-        gestureState.updateLastAppearedTaskTarget(mLastAppearedTaskTarget);
+        gestureState.updateLastAppearedTaskTargets(mLastAppearedTaskTargets);
         return mCallbacks;
     }
 
@@ -315,11 +331,10 @@
      * Finishes the running recents animation.
      * @param forceFinish will synchronously finish the controller
      */
-    private void finishRunningRecentsAnimation(boolean toHome, boolean forceFinish) {
+    public void finishRunningRecentsAnimation(boolean toHome, boolean forceFinish) {
         if (mController != null) {
             ActiveGestureLog.INSTANCE.addLog(
                     /* event= */ "finishRunningRecentsAnimation", toHome);
-            mCallbacks.notifyAnimationCanceled();
             if (forceFinish) {
                 mController.finishController(toHome, null, false /* sendUserLeaveHint */,
                         true /* forceFinish */);
@@ -328,7 +343,6 @@
                         ? mController::finishAnimationToHome
                         : mController::finishAnimationToApp);
             }
-            cleanUpRecentsAnimation();
         }
     }
 
@@ -355,7 +369,12 @@
     /**
      * Cleans up the recents animation entirely.
      */
-    private void cleanUpRecentsAnimation() {
+    private void cleanUpRecentsAnimation(RecentsAnimationCallbacks targetCallbacks) {
+        if (mCallbacks != targetCallbacks) {
+            ActiveGestureLog.INSTANCE.addLog(
+                    /* event= */ "cleanUpRecentsAnimation skipped due to wrong callbacks");
+            return;
+        }
         ActiveGestureLog.INSTANCE.addLog(/* event= */ "cleanUpRecentsAnimation");
         if (mLiveTileCleanUpHandler != null) {
             mLiveTileCleanUpHandler.run();
@@ -377,7 +396,7 @@
         mCallbacks = null;
         mTargets = null;
         mLastGestureState = null;
-        mLastAppearedTaskTarget = null;
+        mLastAppearedTaskTargets = null;
     }
 
     @Nullable
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index 7c05a10..164a366 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -15,7 +15,6 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.uioverrides.QuickstepLauncher.GO_LOW_RAM_RECENTS_ENABLED;
 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
 
 import android.annotation.Nullable;
@@ -182,17 +181,14 @@
             }
         }
 
-        // Loading content descriptions if accessibility or low RAM recents is enabled.
-        if (GO_LOW_RAM_RECENTS_ENABLED || mAccessibilityManager.isEnabled()) {
-            // Skip loading the content description if the activity no longer exists
-            if (activityInfo == null) {
-                activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
-                        key.getComponent(), key.userId);
-            }
-            if (activityInfo != null) {
-                entry.contentDescription = getBadgedContentDescription(
-                        activityInfo, task.key.userId, task.taskDescription);
-            }
+        // Skip loading the content description if the activity no longer exists
+        if (activityInfo == null) {
+            activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
+                    key.getComponent(), key.userId);
+        }
+        if (activityInfo != null) {
+            entry.contentDescription = getBadgedContentDescription(
+                    activityInfo, task.key.userId, task.taskDescription);
         }
 
         mIconCache.put(task.key, entry);
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index 1744b08..076f4b1 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -122,6 +122,12 @@
     public void removeListeners() {
     }
 
+    /**
+     * Clears any active state outside of the TaskOverlay lifecycle which might have built
+     * up over time
+     */
+    public void clearAllActiveState() { }
+
     /** Note that these will be shown in order from top to bottom, if available for the task. */
     private static final TaskShortcutFactory[] MENU_OPTIONS = new TaskShortcutFactory[]{
             TaskShortcutFactory.APP_INFO,
@@ -159,6 +165,10 @@
             return mActionsView;
         }
 
+        public TaskThumbnailView getThumbnailView() {
+            return mThumbnailView;
+        }
+
         /**
          * Called when the current task is interactive for the user
          */
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 208f99a..901690b 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -140,6 +140,7 @@
 
         @Override
         public void onClick(View view) {
+            dismissTaskMenuView(mTarget);
             ((RecentsView) mTarget.getOverviewPanel())
                     .getSplitSelectController().getAppPairsController().saveAppPair(mTaskView);
         }
@@ -396,11 +397,12 @@
         @Override
         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
                 TaskIdAttributeContainer taskContainer) {
-            return InstantAppResolver.newInstance(activity).isInstantApp(activity,
-                    taskContainer.getTask().getTopComponent().getPackageName()) ?
-                    Collections.singletonList(new SystemShortcut.Install(activity,
-                            taskContainer.getItemInfo(), taskContainer.getTaskView())) :
-                    null;
+            Task t = taskContainer.getTask();
+            return InstantAppResolver.newInstance(activity).isInstantApp(
+                    t.getTopComponent().getPackageName(), t.getKey().userId)
+                    ? Collections.singletonList(new SystemShortcut.Install(activity,
+                            taskContainer.getItemInfo(), taskContainer.getTaskView()))
+                    : null;
         }
     };
 
diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java
index 67360c4..80a449b 100644
--- a/quickstep/src/com/android/quickstep/TaskUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskUtils.java
@@ -33,6 +33,7 @@
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.TraceHelper;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 
@@ -51,7 +52,8 @@
      * TODO: remove this once we switch to getting the icon and label from IconCache.
      */
     public static CharSequence getTitle(Context context, Task task) {
-        return getTitle(context, task.key.userId, task.getTopComponent().getPackageName());
+        return TraceHelper.allowIpcs("TaskUtils.getTitle", () ->
+                getTitle(context, task.key.userId, task.getTopComponent().getPackageName()));
     }
 
     public static CharSequence getTitle(
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 1238819..af49774 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -21,6 +21,9 @@
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 
+import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.TOUCH_RESPONSE;
+import static com.android.app.animation.Interpolators.clampToProgress;
 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;
@@ -35,9 +38,8 @@
 import static com.android.launcher3.QuickstepTransitionManager.SPLIT_DIVIDER_ANIM_DURATION;
 import static com.android.launcher3.QuickstepTransitionManager.SPLIT_LAUNCH_DURATION;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
-import static com.android.launcher3.anim.Interpolators.clampToProgress;
+import static com.android.launcher3.testing.shared.TestProtocol.LAUNCH_SPLIT_PAIR;
+import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.quickstep.views.DesktopTaskView.DESKTOP_MODE_SUPPORTED;
 
@@ -61,12 +63,12 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statehandlers.DepthController;
@@ -160,13 +162,14 @@
     }
 
     public static void createRecentsWindowAnimator(
-            @NonNull TaskView v, boolean skipViewChanges,
+            @NonNull RecentsView recentsView,
+            @NonNull TaskView v,
+            boolean skipViewChanges,
             @NonNull RemoteAnimationTarget[] appTargets,
             @NonNull RemoteAnimationTarget[] wallpaperTargets,
             @NonNull RemoteAnimationTarget[] nonAppTargets,
             @Nullable DepthController depthController,
             PendingAnimation out) {
-        RecentsView recentsView = v.getRecentsView();
         boolean isQuickSwitch = v.isEndQuickswitchCuj();
         v.setEndQuickswitchCuj(false);
 
@@ -237,12 +240,12 @@
         for (RemoteTargetHandle targetHandle : remoteTargetHandles) {
             TaskViewSimulator tvsLocal = targetHandle.getTaskViewSimulator();
             out.setFloat(tvsLocal.fullScreenProgress,
-                    AnimatedFloat.VALUE, 1, TOUCH_RESPONSE_INTERPOLATOR);
+                    AnimatedFloat.VALUE, 1, TOUCH_RESPONSE);
             out.setFloat(tvsLocal.recentsViewScale,
                     AnimatedFloat.VALUE, tvsLocal.getFullScreenScale(),
-                    TOUCH_RESPONSE_INTERPOLATOR);
+                    TOUCH_RESPONSE);
             out.setFloat(tvsLocal.recentsViewScroll, AnimatedFloat.VALUE, 0,
-                    TOUCH_RESPONSE_INTERPOLATOR);
+                    TOUCH_RESPONSE);
             out.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationStart(Animator animation) {
@@ -355,9 +358,9 @@
                 float fullScreenScale =
                         topMostSimulators[i].getTaskViewSimulator().getFullScreenScale();
                 out.addFloat(ttv, VIEW_TRANSLATE_Y, translationY,
-                        translationY / fullScreenScale, TOUCH_RESPONSE_INTERPOLATOR);
+                        translationY / fullScreenScale, TOUCH_RESPONSE);
                 out.addFloat(ttv, VIEW_TRANSLATE_X, translationX,
-                         translationX / fullScreenScale, TOUCH_RESPONSE_INTERPOLATOR);
+                         translationX / fullScreenScale, TOUCH_RESPONSE);
             }
 
             Matrix[] k0i = new Matrix[matrixSize];
@@ -405,31 +408,26 @@
 
         if (depthController != null) {
             out.setFloat(depthController.stateDepth, MULTI_PROPERTY_VALUE,
-                    BACKGROUND_APP.getDepth(baseActivity), TOUCH_RESPONSE_INTERPOLATOR);
+                    BACKGROUND_APP.getDepth(baseActivity), TOUCH_RESPONSE);
         }
     }
 
     /**
-     * TODO: This doesn't animate at present. Feel free to blow out everyhing in this method
-     * if needed
+     * If {@param launchingTaskView} is not null, then this will play the tasks launch animation
+     * from the position of the GroupedTaskView (when user taps on the TaskView to start it).
+     * Technically this case should be taken care of by
+     * {@link #composeRecentsSplitLaunchAnimatorLegacy} below, but the way we launch tasks whether
+     * it's a single task or multiple tasks results in different entry-points.
      *
-     * We could manually try to animate the just the bounds for the leashes we get back, but we try
-     * to do it through TaskViewSimulator(TVS) since that handles a lot of the recents UI stuff for
-     * us.
-     *
-     * First you have to call TVS#setPreview() to indicate which leash it will operate one
-     * Then operations happen in TVS#apply() on each frame callback.
-     *
-     * TVS uses DeviceProfile to try to figure out things like task height and such based on if the
-     * device is in multiWindowMode or not. It's unclear given the two calls to startTask() when the
-     * device is considered in multiWindowMode and things like insets and stuff change
-     * and calculations have to be adjusted in the animations for that
+     * If it is null, then it will simply fade in the starting apps and fade out launcher (for the
+     * case where launcher handles animating starting split tasks from app icon)
      */
     public static void composeRecentsSplitLaunchAnimator(GroupedTaskView launchingTaskView,
             @NonNull StateManager stateManager, @Nullable DepthController depthController,
             int initialTaskId, int secondTaskId, @NonNull TransitionInfo transitionInfo,
             SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
         if (launchingTaskView != null) {
+            testLogD(LAUNCH_SPLIT_PAIR, "composeRecentsSplitLaunchAnimator taskView not-null");
             AnimatorSet animatorSet = new AnimatorSet();
             animatorSet.addListener(new AnimatorListenerAdapter() {
                 @Override
@@ -457,12 +455,15 @@
             return;
         }
 
-        // TODO: consider initialTaskPendingIntent
         TransitionInfo.Change splitRoot1 = null;
         TransitionInfo.Change splitRoot2 = null;
+        final ArrayList<SurfaceControl> openingTargets = new ArrayList<>();
         for (int i = 0; i < transitionInfo.getChanges().size(); ++i) {
             final TransitionInfo.Change change = transitionInfo.getChanges().get(i);
-            if (change.getTaskInfo() == null) continue;
+            if (change.getTaskInfo() == null) {
+                testLogD(LAUNCH_SPLIT_PAIR, "changeTaskInfo null; change: " + change);
+                continue;
+            }
             final int taskId = change.getTaskInfo().taskId;
             final int mode = change.getMode();
 
@@ -477,31 +478,48 @@
             if (taskId == initialTaskId) {
                 splitRoot1 = change.getParent() == null ? change :
                         transitionInfo.getChange(change.getParent());
+                openingTargets.add(splitRoot1.getLeash());
             }
             if (taskId == secondTaskId) {
                 splitRoot2 = change.getParent() == null ? change :
                         transitionInfo.getChange(change.getParent());
+                openingTargets.add(splitRoot2.getLeash());
             }
         }
 
-        // This is where we should animate the split roots. For now, though, just make them visible.
-        animateSplitRoot(t, splitRoot1);
-        animateSplitRoot(t, splitRoot2);
+        SurfaceControl.Transaction animTransaction = new SurfaceControl.Transaction();
+        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
+        animator.setDuration(SPLIT_LAUNCH_DURATION);
+        animator.addUpdateListener(valueAnimator -> {
+            float progress = valueAnimator.getAnimatedFraction();
+            for (SurfaceControl leash: openingTargets) {
+                animTransaction.setAlpha(leash, progress);
+            }
+            animTransaction.apply();
+        });
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                for (SurfaceControl leash: openingTargets) {
+                    animTransaction.show(leash)
+                            .setAlpha(leash, 0.0f);
+                }
+                animTransaction.apply();
+            }
 
-        // This contains the initial state (before animation), so apply this at the beginning of
-        // the animation.
-        t.apply();
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                finishCallback.run();
+            }
+        });
 
-        // Once there is an animation, this should be called AFTER the animation completes.
-        finishCallback.run();
-    }
-
-    private static void animateSplitRoot(SurfaceControl.Transaction t,
-            TransitionInfo.Change splitRoot) {
-        if (splitRoot != null) {
-            t.show(splitRoot.getLeash());
-            t.setAlpha(splitRoot.getLeash(), 1.f);
+        if (splitRoot1 != null && splitRoot1.getParent() != null) {
+            // Set the highest level split root alpha; we could technically use the parent of either
+            // splitRoot1 or splitRoot2
+            t.setAlpha(transitionInfo.getChange(splitRoot1.getParent()).getLeash(), 1f);
         }
+        t.apply();
+        animator.start();
     }
 
     /**
@@ -514,7 +532,9 @@
      * it's a single task or multiple tasks results in different entry-points.
      *
      * If it is null, then it will simply fade in the starting apps and fade out launcher (for the
-     * case where launcher handles animating starting split tasks from app icon) */
+     * case where launcher handles animating starting split tasks from app icon)
+     * @deprecated with shell transitions
+     */
     public static void composeRecentsSplitLaunchAnimatorLegacy(
             @Nullable GroupedTaskView launchingTaskView, int initialTaskId, int secondTaskId,
             @NonNull RemoteAnimationTarget[] appTargets,
@@ -606,8 +626,8 @@
 
         TaskView taskView = findTaskViewToLaunch(recentsView, v, appTargets);
         PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
-        createRecentsWindowAnimator(taskView, skipLauncherChanges, appTargets, wallpaperTargets,
-                nonAppTargets, depthController, pa);
+        createRecentsWindowAnimator(recentsView, taskView, skipLauncherChanges, appTargets,
+                wallpaperTargets, nonAppTargets, depthController, pa);
         if (launcherClosing) {
             // TODO(b/182592057): differentiate between "restore split" vs "launch fullscreen app"
             TaskViewUtils.createSplitAuxiliarySurfacesAnimator(nonAppTargets, true /*shown*/,
@@ -637,7 +657,7 @@
                 raController.setWillFinishToHome(false);
             }
             launcherAnim = recentsView.createAdjacentPageAnimForTaskLaunch(taskView);
-            launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
+            launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE);
             launcherAnim.setDuration(RECENTS_LAUNCH_DURATION);
 
             windowAnimEndListener = new AnimatorListenerAdapter() {
diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index d34cddf..01baed3 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -210,7 +210,7 @@
 
         @Nullable
         private final RunningTaskInfo mTopTask;
-        private final List<RunningTaskInfo> mAllCachedTasks;
+        public final List<RunningTaskInfo> mAllCachedTasks;
 
         CachedTaskInfo(List<RunningTaskInfo> allCachedTasks) {
             mAllCachedTasks = allCachedTasks;
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index d6a468d..bf6150a 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -24,8 +24,11 @@
 import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.launcher3.Launcher.INTENT_ACTION_ALL_APPS_TOGGLE;
+import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
+import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TRACKPAD_GESTURE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH;
 import static com.android.quickstep.GestureState.DEFAULT_STATE;
 import static com.android.quickstep.GestureState.TrackpadGestureType.getTrackpadGestureType;
 import static com.android.quickstep.InputConsumer.TYPE_CURSOR_HOVER;
@@ -37,10 +40,8 @@
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE;
@@ -56,6 +57,8 @@
 import android.app.PendingIntent;
 import android.app.RemoteAction;
 import android.app.Service;
+import android.content.IIntentReceiver;
+import android.content.IIntentSender;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
@@ -66,8 +69,10 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.SystemClock;
+import android.os.Trace;
 import android.util.Log;
 import android.view.Choreographer;
+import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
@@ -82,27 +87,26 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.provider.RestoreDbTask;
-import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarManager;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.tracing.LauncherTraceProto;
-import com.android.launcher3.tracing.TouchInteractionServiceProto;
 import com.android.launcher3.uioverrides.flags.FlagsFactory;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.OnboardingPrefs;
+import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
 import com.android.quickstep.inputconsumers.AssistantInputConsumer;
 import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
+import com.android.quickstep.inputconsumers.NavHandleLongPressInputConsumer;
 import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer;
 import com.android.quickstep.inputconsumers.OtherActivityInputConsumer;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
@@ -110,13 +114,13 @@
 import com.android.quickstep.inputconsumers.ProgressDelegateInputConsumer;
 import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
 import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
-import com.android.quickstep.inputconsumers.StatusBarInputConsumer;
 import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer;
 import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer;
+import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.ActiveGestureLog.CompoundString;
-import com.android.quickstep.util.ProtoTracer;
-import com.android.quickstep.util.ProxyScreenStatusProvider;
+import com.android.quickstep.util.AssistStateManager;
+import com.android.quickstep.util.AssistUtils;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -124,7 +128,6 @@
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.InputMonitorCompat;
 import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController;
-import com.android.systemui.shared.tracing.ProtoTraceable;
 import com.android.systemui.unfold.progress.IUnfoldAnimation;
 import com.android.wm.shell.back.IBackAnimation;
 import com.android.wm.shell.bubbles.IBubbles;
@@ -140,8 +143,6 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
-import java.util.Arrays;
-import java.util.LinkedList;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
@@ -149,8 +150,7 @@
  * Service connected by system-UI for handling touch interaction.
  */
 @TargetApi(Build.VERSION_CODES.R)
-public class TouchInteractionService extends Service
-        implements ProtoTraceable<LauncherTraceProto.Builder> {
+public class TouchInteractionService extends Service {
 
     private static final String SUBSTRING_PREFIX = "; ";
     private static final String NEWLINE_PREFIX = "\n\t\t\t-> ";
@@ -284,6 +284,20 @@
             }));
         }
 
+        /**
+         * Sent when the assistant has been invoked with the given type (defined in AssistManager)
+         * and should be shown. This method is used if SystemUiProxy#setAssistantOverridesRequested
+         * was previously called including this invocation type.
+         */
+        @Override
+        public void onAssistantOverrideInvoked(int invocationType) {
+            executeForTouchInteractionService(tis -> {
+                if (!AssistUtils.newInstance(tis).tryStartAssistOverride(invocationType)) {
+                    Log.w(TAG, "Failed to invoke Assist override");
+                }
+            });
+        }
+
         @Override
         public void onNavigationBarSurface(SurfaceControl surface) {
             // TODO: implement
@@ -306,24 +320,6 @@
 
         @BinderThread
         @Override
-        public void onScreenTurnedOn() {
-            MAIN_EXECUTOR.execute(ProxyScreenStatusProvider.INSTANCE::onScreenTurnedOn);
-        }
-
-        @BinderThread
-        @Override
-        public void onScreenTurningOn() {
-            MAIN_EXECUTOR.execute(ProxyScreenStatusProvider.INSTANCE::onScreenTurningOn);
-        }
-
-        @BinderThread
-        @Override
-        public void onScreenTurningOff() {
-            MAIN_EXECUTOR.execute(ProxyScreenStatusProvider.INSTANCE::onScreenTurningOff);
-        }
-
-        @BinderThread
-        @Override
         public void enterStageSplitFromRunningApp(boolean leftOrTop) {
             executeForTouchInteractionService(tis -> {
                 StatefulActivity activity =
@@ -488,11 +484,9 @@
         BootAwarePreloader.start(this);
 
         // Call runOnUserUnlocked() before any other callbacks to ensure everything is initialized.
-        mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
-        mDeviceState.runOnUserUnlocked(mTaskbarManager::onUserUnlocked);
+        LockedUserState.get(this).runOnUserUnlocked(this::onUserUnlocked);
+        LockedUserState.get(this).runOnUserUnlocked(mTaskbarManager::onUserUnlocked);
         mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
-
-        ProtoTracer.INSTANCE.get(this).add(this);
         sConnected = true;
     }
 
@@ -511,7 +505,7 @@
     private void initInputMonitor(String reason) {
         disposeEventHandlers("Initializing input monitor due to: " + reason);
 
-        if (mDeviceState.isButtonNavMode()) {
+        if (mDeviceState.isButtonNavMode() && !ENABLE_TRACKPAD_GESTURE.get()) {
             return;
         }
 
@@ -559,7 +553,7 @@
     }
 
     private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() {
-        if (!mDeviceState.isUserUnlocked() || mDeviceState.isButtonNavMode()) {
+        if (!LockedUserState.get(this).isUserUnlocked() || mDeviceState.isButtonNavMode()) {
             // Skip if not yet unlocked (can't read user shared prefs) or if the current navigation
             // mode doesn't have gestures
             return;
@@ -579,15 +573,7 @@
         AccessibilityManager am = getSystemService(AccessibilityManager.class);
 
         if (isHomeAndOverviewSame) {
-            Intent intent = new Intent(mOverviewComponentObserver.getHomeIntent())
-                    .setAction(INTENT_ACTION_ALL_APPS_TOGGLE);
-            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, GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS, intent,
-                            PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
-            am.registerSystemAction(allAppsAction, GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
+            am.registerSystemAction(createAllAppsAction(), GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
         } else {
             am.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
         }
@@ -600,26 +586,43 @@
         mTISBinder.onOverviewTargetChange();
     }
 
+    private RemoteAction createAllAppsAction() {
+        final Intent homeIntent = new Intent(mOverviewComponentObserver.getHomeIntent())
+                .setAction(INTENT_ACTION_ALL_APPS_TOGGLE);
+        final PendingIntent actionPendingIntent;
+
+        if (FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) {
+            actionPendingIntent = new PendingIntent(new IIntentSender.Stub() {
+                @Override
+                public void send(int code, Intent intent, String resolvedType,
+                        IBinder allowlistToken, IIntentReceiver finishedReceiver,
+                        String requiredPermission, Bundle options) {
+                    MAIN_EXECUTOR.execute(() -> mTaskbarManager.toggleAllApps(homeIntent));
+                }
+            });
+        } else {
+            actionPendingIntent = PendingIntent.getActivity(
+                    this,
+                    GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS,
+                    homeIntent,
+                    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+        }
+
+        return new RemoteAction(
+                Icon.createWithResource(this, R.drawable.ic_apps),
+                getString(R.string.all_apps_label),
+                getString(R.string.all_apps_label),
+                actionPendingIntent);
+    }
+
     @UiThread
     private void onSystemUiFlagsChanged(int lastSysUIFlags) {
-        if (mDeviceState.isUserUnlocked()) {
+        if (LockedUserState.get(this).isUserUnlocked()) {
             int systemUiStateFlags = mDeviceState.getSystemUiStateFlags();
             SystemUiProxy.INSTANCE.get(this).setLastSystemUiStateFlags(systemUiStateFlags);
             mOverviewComponentObserver.onSystemUiStateChanged();
             mTaskbarManager.onSystemUiFlagsChanged(systemUiStateFlags);
 
-            boolean wasFreeformActive =
-                    (lastSysUIFlags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0;
-            boolean isFreeformActive =
-                    (systemUiStateFlags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0;
-            if (wasFreeformActive != isFreeformActive) {
-                DesktopVisibilityController controller =
-                        LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
-                if (controller != null) {
-                    controller.setFreeformTasksVisible(isFreeformActive);
-                }
-            }
-
             int isShadeExpandedFlag =
                     SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED | SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
             boolean wasExpanded = (lastSysUIFlags & isShadeExpandedFlag) != 0;
@@ -629,25 +632,12 @@
                 // overview.
                 mTaskAnimationManager.endLiveTile();
             }
-
-            if ((lastSysUIFlags & SYSUI_STATE_TRACING_ENABLED) !=
-                    (systemUiStateFlags & SYSUI_STATE_TRACING_ENABLED)) {
-                // Update the tracing state
-                if ((systemUiStateFlags & SYSUI_STATE_TRACING_ENABLED) != 0) {
-                    Log.d(TAG, "Starting tracing.");
-                    ProtoTracer.INSTANCE.get(this).start();
-                } else {
-                    Log.d(TAG, "Stopping tracing. Dumping to file="
-                            + ProtoTracer.INSTANCE.get(this).getTraceFile());
-                    ProtoTracer.INSTANCE.get(this).stop();
-                }
-            }
         }
     }
 
     @UiThread
     private void onAssistantVisibilityChanged() {
-        if (mDeviceState.isUserUnlocked()) {
+        if (LockedUserState.get(this).isUserUnlocked()) {
             mOverviewComponentObserver.getActivityInterface().onAssistantVisibilityChanged(
                     mDeviceState.getAssistantVisibility());
         }
@@ -657,15 +647,13 @@
     public void onDestroy() {
         Log.d(TAG, "Touch service destroyed: user=" + getUserId());
         sIsInitialized = false;
-        if (mDeviceState.isUserUnlocked()) {
+        if (LockedUserState.get(this).isUserUnlocked()) {
             mInputConsumer.unregisterInputConsumer();
             mOverviewComponentObserver.onDestroy();
         }
         disposeEventHandlers("TouchInteractionService onDestroy()");
         mDeviceState.destroy();
         SystemUiProxy.INSTANCE.get(this).clearProxy();
-        ProtoTracer.INSTANCE.get(this).stop();
-        ProtoTracer.INSTANCE.get(this).remove(this);
 
         getSystemService(AccessibilityManager.class)
                 .unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
@@ -691,12 +679,12 @@
         TestLogging.recordMotionEvent(
                 TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event);
 
-        if (!mDeviceState.isUserUnlocked()) {
+        if (!LockedUserState.get(this).isUserUnlocked() || (mDeviceState.isButtonNavMode()
+                && !isTrackpadMotionEvent(event))) {
             return;
         }
 
-        Object traceToken = TraceHelper.INSTANCE.beginFlagsOverride(
-                TraceHelper.FLAG_ALLOW_BINDER_TRACKING);
+        SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent");
 
         final int action = event.getActionMasked();
         // Note this will create a new consumer every mouse click, as after ACTION_UP from the click
@@ -707,8 +695,7 @@
             mRotationTouchHelper.setOrientationTransformIfNeeded(event);
 
             if ((!mDeviceState.isOneHandedModeActive()
-                    && mRotationTouchHelper.isInSwipeUpTouchRegion(event,
-                    mOverviewComponentObserver.getActivityInterface()))
+                    && mRotationTouchHelper.isInSwipeUpTouchRegion(event))
                     || isHoverActionWithoutConsumer) {
                 // Clone the previous gesture state since onConsumerAboutToBeSwitched might trigger
                 // onConsumerInactive and wipe the previous gesture state
@@ -720,7 +707,8 @@
                 mGestureState = newGestureState;
                 mConsumer = newConsumer(prevGestureState, mGestureState, event);
                 mUncheckedConsumer = mConsumer;
-            } else if (mDeviceState.isUserUnlocked() && mDeviceState.isFullyGesturalNavMode()
+            } else if (LockedUserState.get(this).isUserUnlocked()
+                    && (mDeviceState.isFullyGesturalNavMode() || isTrackpadMultiFingerSwipe(event))
                     && mDeviceState.canTriggerAssistantAction(event)) {
                 mGestureState = createGestureState(mGestureState,
                         getTrackpadGestureType(event));
@@ -783,10 +771,7 @@
         if (mGestureState.isTrackpadGesture() && (action == ACTION_POINTER_DOWN
                 || action == ACTION_POINTER_UP)) {
             // Skip ACTION_POINTER_DOWN and ACTION_POINTER_UP events from trackpad.
-            if (action == ACTION_POINTER_DOWN) {
-                mGestureState.setTrackpadGestureType(getTrackpadGestureType(event));
-            }
-        } else if (event.isHoverEvent()) {
+        } else if (isCursorHoverEvent(event)) {
             mUncheckedConsumer.onHoverEvent(event);
         } else {
             mUncheckedConsumer.onMotionEvent(event);
@@ -795,8 +780,12 @@
         if (cleanUpConsumer) {
             reset();
         }
-        TraceHelper.INSTANCE.endFlagsOverride(traceToken);
-        ProtoTracer.INSTANCE.get(this).scheduleFrameUpdate();
+        traceToken.close();
+    }
+
+    // Talkback generates hover events on touch, which we do not want to consume.
+    private boolean isCursorHoverEvent(MotionEvent event) {
+        return event.isHoverEvent() && event.getSource() == InputDevice.SOURCE_MOUSE;
     }
 
     private InputConsumer tryCreateAssistantInputConsumer(
@@ -830,7 +819,7 @@
                     ActiveGestureLog.INSTANCE.getLogId());
             taskInfo = previousGestureState.getRunningTask();
             gestureState.updateRunningTask(taskInfo);
-            gestureState.updateLastStartedTaskId(previousGestureState.getLastStartedTaskId());
+            gestureState.updateLastStartedTaskIds(previousGestureState.getLastStartedTaskIds());
             gestureState.updatePreviouslyAppearedTaskIds(
                     previousGestureState.getPreviouslyAppearedTaskIds());
         } else {
@@ -862,9 +851,11 @@
             return consumer;
         }
 
-        boolean canStartSystemGesture = mDeviceState.canStartSystemGesture();
+        boolean canStartSystemGesture =
+                mGestureState.isTrackpadGesture() ? mDeviceState.canStartTrackpadGesture()
+                        : mDeviceState.canStartSystemGesture();
 
-        if (!mDeviceState.isUserUnlocked()) {
+        if (!LockedUserState.get(this).isUserUnlocked()) {
             CompoundString reasonString = newCompoundString("device locked");
             InputConsumer consumer;
             if (canStartSystemGesture) {
@@ -897,11 +888,12 @@
                     .append(", trying to use default input consumer");
             base = getDefaultInputConsumer(reasonString);
         }
-        if (mDeviceState.isGesturalNavMode()) {
+        if (mDeviceState.isGesturalNavMode() || newGestureState.isTrackpadGesture()) {
             handleOrientationSetup(base);
         }
-        if (mDeviceState.isFullyGesturalNavMode()) {
-            String reasonPrefix = "device is in gesture navigation mode";
+        if (mDeviceState.isFullyGesturalNavMode() || newGestureState.isTrackpadGesture()) {
+            String reasonPrefix = "device is in gesture navigation mode or 3-button mode with a"
+                    + " trackpad gesture";
             if (mDeviceState.canTriggerAssistantAction(event)) {
                 reasonString.append(NEWLINE_PREFIX)
                         .append(reasonPrefix)
@@ -924,8 +916,17 @@
                             .append(SUBSTRING_PREFIX)
                             .append("TaskbarActivityContext != null, "
                                     + "using TaskbarUnstashInputConsumer");
-                    base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac);
+                    base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac,
+                            mOverviewCommandHelper);
                 }
+            } else if (canStartSystemGesture && FeatureFlags.ENABLE_LONG_PRESS_NAV_HANDLE.get()
+                    && !previousGestureState.isRecentsAnimationRunning()) {
+                reasonString.append(NEWLINE_PREFIX)
+                        .append(reasonPrefix)
+                        .append(SUBSTRING_PREFIX)
+                        .append("Long press nav handle enabled, "
+                                + "using NavHandleLongPressInputConsumer");
+                base = new NavHandleLongPressInputConsumer(this, base, mInputMonitorCompat);
             }
 
             if (mDeviceState.isBubblesExpanded()) {
@@ -945,11 +946,12 @@
             }
 
             if (ENABLE_TRACKPAD_GESTURE.get() && mGestureState.isTrackpadGesture()
-                    && !previousGestureState.isRecentsAnimationRunning()) {
+                    && canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()) {
                 reasonString = newCompoundString(reasonPrefix)
                         .append(SUBSTRING_PREFIX)
-                        .append("Trackpad 3-finger gesture, using StatusBarInputConsumer");
-                base = new StatusBarInputConsumer(getBaseContext(), base, mInputMonitorCompat);
+                        .append("Trackpad 3-finger gesture, using TrackpadStatusBarInputConsumer");
+                base = new TrackpadStatusBarInputConsumer(getBaseContext(), base,
+                        mInputMonitorCompat);
             }
 
             if (mDeviceState.isScreenPinningActive()) {
@@ -1037,6 +1039,7 @@
         }
 
         reasonString.append(SUBSTRING_PREFIX).append("keyguard is not showing occluded");
+
         // Use overview input consumer for sharesheets on top of home.
         boolean forceOverviewInputConsumer = gestureState.getActivityInterface().isStarted()
                 && gestureState.getRunningTask() != null
@@ -1112,17 +1115,21 @@
 
     private InputConsumer createDeviceLockedInputConsumer(
             GestureState gestureState, CompoundString reasonString) {
-        if (mDeviceState.isFullyGesturalNavMode() && gestureState.getRunningTask() != null) {
+        if ((mDeviceState.isFullyGesturalNavMode() || gestureState.isTrackpadGesture())
+                && gestureState.getRunningTask() != null) {
             reasonString.append(SUBSTRING_PREFIX)
-                    .append("device is in gesture nav mode and running task != null")
+                    .append("device is in gesture nav mode or 3-button mode with a trackpad gesture"
+                            + "and running task != null")
                     .append(", using DeviceLockedInputConsumer");
             return new DeviceLockedInputConsumer(
                     this, mDeviceState, mTaskAnimationManager, gestureState, mInputMonitorCompat);
         } else {
             return getDefaultInputConsumer(reasonString
                     .append(SUBSTRING_PREFIX)
-                    .append(mDeviceState.isFullyGesturalNavMode()
-                        ? "running task == null" : "device is not in gesture nav mode")
+                    .append((mDeviceState.isFullyGesturalNavMode()
+                                    || gestureState.isTrackpadGesture())
+                            ? "running task == null"
+                            : "device is not in gesture nav mode and it's not a trackpad gesture")
                     .append(", trying to use default input consumer"));
         }
     }
@@ -1211,11 +1218,13 @@
     }
 
     private void preloadOverview(boolean fromInit) {
+        Trace.beginSection("preloadOverview(fromInit=" + fromInit + ")");
         preloadOverview(fromInit, false);
+        Trace.endSection();
     }
 
     private void preloadOverview(boolean fromInit, boolean forSUWAllSet) {
-        if (!mDeviceState.isUserUnlocked()) {
+        if (!LockedUserState.get(this).isUserUnlocked()) {
             return;
         }
 
@@ -1251,7 +1260,7 @@
 
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
-        if (!mDeviceState.isUserUnlocked()) {
+        if (!LockedUserState.get(this).isUserUnlocked()) {
             return;
         }
         final BaseActivityInterface activityInterface =
@@ -1261,7 +1270,10 @@
             // We only care about the existing background activity.
             return;
         }
-        if (mOverviewComponentObserver.canHandleConfigChanges(activity.getComponentName(),
+        Configuration oldConfig = activity.getResources().getConfiguration();
+        boolean isFoldUnfold = isTablet(oldConfig) != isTablet(newConfig);
+        if (!isFoldUnfold && 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
@@ -1276,82 +1288,47 @@
         preloadOverview(false /* fromInit */);
     }
 
+    private static boolean isTablet(Configuration config) {
+        return config.smallestScreenWidthDp >= MIN_TABLET_WIDTH;
+    }
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] rawArgs) {
-        if (rawArgs.length > 0 && Utilities.IS_DEBUG_DEVICE) {
-            LinkedList<String> args = new LinkedList(Arrays.asList(rawArgs));
-            switch (args.pollFirst()) {
-                case "cmd":
-                    if (args.peekFirst() == null) {
-                        printAvailableCommands(pw);
-                    } else {
-                        onCommand(pw, args);
-                    }
-                    break;
-            }
-        } else {
-            // Dump everything
-            FlagsFactory.dump(pw);
-            if (mDeviceState.isUserUnlocked()) {
-                PluginManagerWrapper.INSTANCE.get(getBaseContext()).dump(pw);
-            }
-            mDeviceState.dump(pw);
-            if (mOverviewComponentObserver != null) {
-                mOverviewComponentObserver.dump(pw);
-            }
-            if (mOverviewCommandHelper != null) {
-                mOverviewCommandHelper.dump(pw);
-            }
-            if (mGestureState != null) {
-                mGestureState.dump(pw);
-            }
-            pw.println("Input state:");
-            pw.println("  mInputMonitorCompat=" + mInputMonitorCompat);
-            pw.println("  mInputEventReceiver=" + mInputEventReceiver);
-            DisplayController.INSTANCE.get(this).dump(pw);
-            pw.println("TouchState:");
-            BaseDraggingActivity createdOverviewActivity = mOverviewComponentObserver == null ? null
-                    : mOverviewComponentObserver.getActivityInterface().getCreatedActivity();
-            boolean resumed = mOverviewComponentObserver != null
-                    && mOverviewComponentObserver.getActivityInterface().isResumed();
-            pw.println("  createdOverviewActivity=" + createdOverviewActivity);
-            pw.println("  resumed=" + resumed);
-            pw.println("  mConsumer=" + mConsumer.getName());
-            ActiveGestureLog.INSTANCE.dump("", pw);
-            RecentsModel.INSTANCE.get(this).dump("", pw);
-            pw.println("ProtoTrace:");
-            pw.println("  file=" + ProtoTracer.INSTANCE.get(this).getTraceFile());
-            if (createdOverviewActivity != null) {
-                createdOverviewActivity.getDeviceProfile().dump(this, "", pw);
-            }
-            mTaskbarManager.dumpLogs("", pw);
+        // Dump everything
+        FlagsFactory.dump(pw);
+        if (LockedUserState.get(this).isUserUnlocked()) {
+            PluginManagerWrapper.INSTANCE.get(getBaseContext()).dump(pw);
         }
-    }
-
-    private void printAvailableCommands(PrintWriter pw) {
-        pw.println("Available commands:");
-        pw.println("  clear-touch-log: Clears the touch interaction log");
-        pw.println("  print-gesture-log: only prints the ActiveGestureLog dump");
-    }
-
-    private void onCommand(PrintWriter pw, LinkedList<String> args) {
-        String cmd = args.pollFirst();
-        if (cmd == null) {
-            pw.println("Command missing");
-            printAvailableCommands(pw);
-            return;
+        mDeviceState.dump(pw);
+        if (mOverviewComponentObserver != null) {
+            mOverviewComponentObserver.dump(pw);
         }
-        switch (cmd) {
-            case "clear-touch-log":
-                ActiveGestureLog.INSTANCE.clear();
-                break;
-            case "print-gesture-log":
-                ActiveGestureLog.INSTANCE.dump("", pw);
-                break;
-            default:
-                pw.println("Command does not exist: " + cmd);
-                printAvailableCommands(pw);
+        if (mOverviewCommandHelper != null) {
+            mOverviewCommandHelper.dump(pw);
         }
+        if (mGestureState != null) {
+            mGestureState.dump(pw);
+        }
+        pw.println("Input state:");
+        pw.println("  mInputMonitorCompat=" + mInputMonitorCompat);
+        pw.println("  mInputEventReceiver=" + mInputEventReceiver);
+        DisplayController.INSTANCE.get(this).dump(pw);
+        pw.println("TouchState:");
+        BaseDraggingActivity createdOverviewActivity = mOverviewComponentObserver == null ? null
+                : mOverviewComponentObserver.getActivityInterface().getCreatedActivity();
+        boolean resumed = mOverviewComponentObserver != null
+                && mOverviewComponentObserver.getActivityInterface().isResumed();
+        pw.println("  createdOverviewActivity=" + createdOverviewActivity);
+        pw.println("  resumed=" + resumed);
+        pw.println("  mConsumer=" + mConsumer.getName());
+        ActiveGestureLog.INSTANCE.dump("", pw);
+        RecentsModel.INSTANCE.get(this).dump("", pw);
+        if (createdOverviewActivity != null) {
+            createdOverviewActivity.getDeviceProfile().dump(this, "", pw);
+        }
+        mTaskbarManager.dumpLogs("", pw);
+        pw.println("AssistStateManager:");
+        AssistStateManager.INSTANCE.get(this).dump("  ", pw);
     }
 
     private AbsSwipeUpHandler createLauncherSwipeHandler(
@@ -1367,18 +1344,4 @@
                 gestureState, touchTimeMs, mTaskAnimationManager.isRecentsAnimationRunning(),
                 mInputConsumer);
     }
-
-    @Override
-    public void writeToProto(LauncherTraceProto.Builder proto) {
-        TouchInteractionServiceProto.Builder serviceProto =
-            TouchInteractionServiceProto.newBuilder();
-        serviceProto.setServiceConnected(true);
-
-        if (mOverviewComponentObserver != null) {
-            mOverviewComponentObserver.writeToProto(serviceProto);
-        }
-        mConsumer.writeToProto(serviceProto);
-
-        proto.setTouchInteractionService(serviceProto);
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index 11b1ab8..8a9e04e 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -15,9 +15,9 @@
  */
 package com.android.quickstep.fallback;
 
-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.app.animation.Interpolators.FINAL_FRAME;
+import static com.android.app.animation.Interpolators.INSTANT;
+import static com.android.app.animation.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;
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 074aedd..95d88cd 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -35,6 +35,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.util.SplitConfigurationOptions;
@@ -79,11 +80,16 @@
     }
 
     @Override
-    public void startHome(boolean animated) {
+    protected void handleStartHome(boolean animated) {
         mActivity.startHome();
         AbstractFloatingView.closeAllOpenViews(mActivity, mActivity.isStarted());
     }
 
+    @Override
+    protected boolean canStartHomeSafely() {
+        return mActivity.canStartHomeSafely();
+    }
+
     /**
      * 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
@@ -246,7 +252,11 @@
             setOverviewSelectEnabled(false);
         }
         if (finalState != OVERVIEW_SPLIT_SELECT) {
-            resetFromSplitSelectionState();
+            if (FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+                mSplitSelectStateController.resetState();
+            } else {
+                resetFromSplitSelectionState();
+            }
         }
 
         if (isOverlayEnabled) {
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
index 6a36d9f..ec6efcb 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
@@ -103,8 +103,7 @@
                 if (mState == STATE_INACTIVE) {
                     int pointerIndex = ev.getActionIndex();
                     if (mDeviceState.getRotationTouchHelper().isInSwipeUpTouchRegion(ev,
-                            pointerIndex, mGestureState.getActivityInterface())
-                            && mDelegate.allowInterceptByParent()) {
+                            pointerIndex) && mDelegate.allowInterceptByParent()) {
                         setActive(ev);
 
                         mActivePointerId = ev.getPointerId(pointerIndex);
@@ -153,4 +152,9 @@
             mDelegate.onMotionEvent(ev);
         }
     }
+
+    @Override
+    protected String getDelegatorName() {
+        return "AccessibilityInputConsumer";
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
index 162ace4..ba012c9 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
@@ -42,9 +42,9 @@
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
-import com.android.launcher3.anim.Interpolators;
 import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
@@ -209,7 +209,7 @@
                             SystemUiProxy.INSTANCE.get(mContext).onAssistantProgress(0f);
                         }
                     });
-                    animator.setInterpolator(Interpolators.DEACCEL_2);
+                    animator.setInterpolator(Interpolators.DECELERATE_2);
                     animator.start();
                 }
                 mPassedSlop = false;
@@ -278,4 +278,9 @@
             return true;
         }
     }
+
+    @Override
+    protected String getDelegatorName() {
+        return "AssistantInputConsumer";
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
index 03f8eef..63771f0 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
@@ -4,8 +4,8 @@
 
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.tracing.InputConsumerProto;
 import com.android.quickstep.InputConsumer;
+import com.android.quickstep.util.ActiveGestureLog;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 public abstract class DelegateInputConsumer implements InputConsumer {
@@ -43,7 +43,15 @@
         mDelegate.onConsumerAboutToBeSwitched();
     }
 
+    /**
+     * Returns the name of this DelegateInputConsumer.
+     */
+    protected abstract String getDelegatorName();
+
     protected void setActive(MotionEvent ev) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(getDelegatorName())
+                .append(" became active"));
+
         mState = STATE_ACTIVE;
         TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
         mInputMonitor.pilferPointers();
@@ -54,9 +62,4 @@
         mDelegate.onMotionEvent(event);
         event.recycle();
     }
-
-    @Override
-    public void writeToProtoInternal(InputConsumerProto.Builder inputConsumerProto) {
-        mDelegate.writeToProtoInternal(inputConsumerProto);
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index 59a9582..2a35584 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -39,9 +39,9 @@
 import android.view.RemoteAnimationTarget;
 import android.view.VelocityTracker;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.DisplayController;
@@ -153,8 +153,7 @@
                 if (!mThresholdCrossed) {
                     // Cancel interaction in case of multi-touch interaction
                     int ptrIdx = ev.getActionIndex();
-                    if (!mDeviceState.getRotationTouchHelper().isInSwipeUpTouchRegion(ev, ptrIdx,
-                            mGestureState.getActivityInterface())) {
+                    if (!mDeviceState.getRotationTouchHelper().isInSwipeUpTouchRegion(ev, ptrIdx)) {
                         int action = ev.getAction();
                         ev.setAction(ACTION_CANCEL);
                         finishTouchTracking(ev);
@@ -204,7 +203,7 @@
             // Animate back to fullscreen before finishing
             ObjectAnimator animator = mProgress.animateToValue(mProgress.value, 0);
             animator.setDuration(100);
-            animator.setInterpolator(Interpolators.ACCEL);
+            animator.setInterpolator(Interpolators.ACCELERATE);
             animator.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java
new file mode 100644
index 0000000..7a2b343
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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 androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.ResourceBasedOverride;
+
+/**
+ * Class for extending nav handle long press behavior
+ */
+public class NavHandleLongPressHandler implements ResourceBasedOverride {
+
+    /** Creates NavHandleLongPressHandler as specified by overrides */
+    public static NavHandleLongPressHandler newInstance(Context context) {
+        return Overrides.getObject(NavHandleLongPressHandler.class, context,
+                R.string.nav_handle_long_press_handler_class);
+    }
+
+    /**
+     * Called when nav handle is long pressed to get the Runnable that should be executed by the
+     * caller to invoke long press behavior. If null is returned that means long press couldn't be
+     * handled.
+     * <p>
+     * A Runnable is returned here to ensure the InputConsumer can call
+     * {@link android.view.InputMonitor#pilferPointers()} before invoking the long press behavior
+     * since pilfering can break the long press behavior.
+     */
+    public @Nullable Runnable getLongPressRunnable() {
+        return null;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
new file mode 100644
index 0000000..addcfb8
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.inputconsumers;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.DisplayController;
+import com.android.quickstep.InputConsumer;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+/**
+ * Listens for a long press
+ */
+public class NavHandleLongPressInputConsumer extends DelegateInputConsumer {
+
+    private final GestureDetector mLongPressDetector;
+    private final NavHandleLongPressHandler mNavHandleLongPressHandler;
+    private final float mNavHandleWidth;
+    private final float mScreenWidth;
+
+    public NavHandleLongPressInputConsumer(Context context, InputConsumer delegate,
+            InputMonitorCompat inputMonitor) {
+        super(delegate, inputMonitor);
+        mNavHandleWidth = context.getResources().getDimensionPixelSize(
+                R.dimen.navigation_home_handle_width);
+        mScreenWidth = DisplayController.INSTANCE.get(context).getInfo().currentSize.x;
+
+        mNavHandleLongPressHandler = NavHandleLongPressHandler.newInstance(context);
+
+        mLongPressDetector = new GestureDetector(context, new SimpleOnGestureListener() {
+            @Override
+            public void onLongPress(MotionEvent motionEvent) {
+                if (isInArea(motionEvent.getRawX())) {
+                    Runnable longPressRunnable = mNavHandleLongPressHandler.getLongPressRunnable();
+                    if (longPressRunnable != null) {
+                        setActive(motionEvent);
+
+                        MAIN_EXECUTOR.post(longPressRunnable);
+                    }
+                }
+            }
+        });
+    }
+
+    @Override
+    public int getType() {
+        return TYPE_NAV_HANDLE_LONG_PRESS | mDelegate.getType();
+    }
+
+    @Override
+    public void onMotionEvent(MotionEvent ev) {
+        mLongPressDetector.onTouchEvent(ev);
+        if (mState != STATE_ACTIVE) {
+            mDelegate.onMotionEvent(ev);
+        }
+    }
+
+    protected boolean isInArea(float x) {
+        float areaFromMiddle = mNavHandleWidth / 2.0f;
+        float distFromMiddle = Math.abs(mScreenWidth / 2.0f - x);
+
+        return distFromMiddle < areaFromMiddle;
+    }
+
+    @Override
+    protected String getDelegatorName() {
+        return "NavHandleLongPressInputConsumer";
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
index 5387c8a..83b556d 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
@@ -175,4 +175,9 @@
         final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
         return angle > ANGLE_MIN && angle < ANGLE_MAX;
     }
+
+    @Override
+    protected String getDelegatorName() {
+        return "OneHandedModeInputConsumer";
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index f9cd4ee..7e61167 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -28,7 +28,6 @@
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS;
 import static com.android.launcher3.util.VelocityUtils.PX_PER_MS;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 
@@ -49,7 +48,6 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.tracing.InputConsumerProto;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.AbsSwipeUpHandler;
@@ -229,8 +227,7 @@
                 // Until we detect the gesture, handle events as we receive them
                 mInputEventReceiver.setBatchingEnabled(false);
 
-                Object traceToken = TraceHelper.INSTANCE.beginSection(DOWN_EVT,
-                        FLAG_CHECK_FOR_RACE_CONDITIONS);
+                TraceHelper.INSTANCE.beginSection(DOWN_EVT);
                 mActivePointerId = ev.getPointerId(0);
                 mDownPos.set(ev.getX(), ev.getY());
                 mLastPos.set(mDownPos);
@@ -241,15 +238,14 @@
                     startTouchTrackingForWindowAnimation(ev.getEventTime());
                 }
 
-                TraceHelper.INSTANCE.endSection(traceToken);
+                TraceHelper.INSTANCE.endSection();
                 break;
             }
             case ACTION_POINTER_DOWN: {
                 if (!mPassedPilferInputSlop) {
                     // Cancel interaction in case of multi-touch interaction
                     int ptrIdx = ev.getActionIndex();
-                    if (!mRotationTouchHelper.isInSwipeUpTouchRegion(ev, ptrIdx,
-                            mActivityInterface)) {
+                    if (!mRotationTouchHelper.isInSwipeUpTouchRegion(ev, ptrIdx)) {
                         forceCancelGesture(ev);
                     }
                 }
@@ -282,7 +278,8 @@
                     if (!mIsDeferredDownTarget) {
                         // Normal gesture, ensure we pass the drag slop before we start tracking
                         // the gesture
-                        if (Math.abs(displacement) > mTouchSlop) {
+                        if (mGestureState.isTrackpadGesture() || Math.abs(displacement)
+                                > mTouchSlop) {
                             mPassedWindowMoveSlop = true;
                             mStartDisplacement = Math.min(displacement, -mTouchSlop);
                         }
@@ -291,8 +288,8 @@
 
                 float horizontalDist = Math.abs(displacementX);
                 float upDist = -displacement;
-                boolean passedSlop = squaredHypot(displacementX, displacementY)
-                        >= mSquaredTouchSlop;
+                boolean passedSlop = mGestureState.isTrackpadGesture() || squaredHypot(
+                        displacementX, displacementY) >= mSquaredTouchSlop;
 
                 if (!mPassedSlopOnThisGesture && passedSlop) {
                     mPassedSlopOnThisGesture = true;
@@ -352,7 +349,8 @@
                         mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
                     }
 
-                    if (mDeviceState.isFullyGesturalNavMode()) {
+                    if (mDeviceState.isFullyGesturalNavMode()
+                            || mGestureState.isTrackpadGesture()) {
                         boolean minSwipeMet = upDist >= Math.max(mMotionPauseMinDisplacement,
                                 mInteractionHandler.getThresholdToAllowMotionPause());
                         mInteractionHandler.setCanSlowSwipeGoHome(minSwipeMet);
@@ -417,11 +415,11 @@
      * the animation can still be running.
      */
     private void finishTouchTracking(MotionEvent ev) {
-        Object traceToken = TraceHelper.INSTANCE.beginSection(UP_EVT,
-                FLAG_CHECK_FOR_RACE_CONDITIONS);
+        TraceHelper.INSTANCE.beginSection(UP_EVT);
 
+        boolean isCanceled = ev.getActionMasked() == ACTION_CANCEL;
         if (mPassedWindowMoveSlop && mInteractionHandler != null) {
-            if (ev.getActionMasked() == ACTION_CANCEL) {
+            if (isCanceled) {
                 mInteractionHandler.onGestureCancelled();
             } else {
                 mVelocityTracker.computeCurrentVelocity(PX_PER_MS);
@@ -443,8 +441,10 @@
             if (mActiveCallbacks != null && mInteractionHandler != null) {
                 if (mTaskAnimationManager.isRecentsAnimationRunning()) {
                     // The animation started, but with no movement, in this case, there will be no
-                    // animateToProgress so we have to manually finish here.
-                    mTaskAnimationManager.finishRunningRecentsAnimation(false /* toHome */);
+                    // animateToProgress so we have to manually finish here. In the case of
+                    // ACTION_CANCEL, someone else may be doing something so finish synchronously.
+                    mTaskAnimationManager.finishRunningRecentsAnimation(false /* toHome */,
+                            isCanceled /* forceFinish */);
                 } else {
                     // The animation hasn't started yet, so insert a replacement handler into the
                     // callbacks which immediately finishes the animation after it starts.
@@ -455,7 +455,7 @@
             onInteractionGestureFinished();
         }
         cleanupAfterGesture();
-        TraceHelper.INSTANCE.endSection(traceToken);
+        TraceHelper.INSTANCE.endSection();
     }
 
     private void cleanupAfterGesture() {
@@ -512,13 +512,6 @@
         return !mPassedPilferInputSlop;
     }
 
-    @Override
-    public void writeToProtoInternal(InputConsumerProto.Builder inputConsumerProto) {
-        if (mInteractionHandler != null) {
-            mInteractionHandler.writeToProto(inputConsumerProto);
-        }
-    }
-
     /**
      * A listener which just finishes the animation immediately after starting. Replaces
      * AbsSwipeUpHandler if the gesture itself finishes before the animation even starts.
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
index eac09ad..c9c64b6 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
@@ -15,7 +15,7 @@
  */
 package com.android.quickstep.inputconsumers;
 
-import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.app.animation.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.touch.BaseSwipeDetector.calculateDuration;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index fbe7fde..28ac65b 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -28,6 +28,7 @@
 import android.graphics.Rect;
 import android.view.GestureDetector;
 import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.InputDevice;
 import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
@@ -36,10 +37,13 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarThresholdUtils;
 import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback;
+import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.touch.OverScroll;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.InputConsumer;
+import com.android.quickstep.OverviewCommandHelper;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 /**
@@ -50,6 +54,7 @@
 public class TaskbarUnstashInputConsumer extends DelegateInputConsumer {
 
     private final TaskbarActivityContext mTaskbarActivityContext;
+    private final OverviewCommandHelper mOverviewCommandHelper;
     private final GestureDetector mLongPressDetector;
     private final float mSquaredTouchSlop;
 
@@ -62,6 +67,7 @@
     private final int mTaskbarNavThresholdY;
     private final boolean mIsTaskbarAllAppsOpen;
     private boolean mHasPassedTaskbarNavThreshold;
+    private boolean mIsInBubbleBarArea;
 
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
@@ -78,16 +84,19 @@
     private final @Nullable TransitionCallback mTransitionCallback;
 
     public TaskbarUnstashInputConsumer(Context context, InputConsumer delegate,
-            InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext) {
+            InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext,
+            OverviewCommandHelper overviewCommandHelper) {
         super(delegate, inputMonitor);
         mTaskbarActivityContext = taskbarActivityContext;
+        mOverviewCommandHelper = overviewCommandHelper;
         // TODO(b/270395798): remove this when cleaning up old Persistent Taskbar code.
         mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
         mScreenWidth = taskbarActivityContext.getDeviceProfile().widthPx;
 
         Resources res = context.getResources();
         mUnstashArea = res.getDimensionPixelSize(R.dimen.taskbar_unstash_input_area);
-        mTaskbarNavThreshold = res.getDimensionPixelSize(R.dimen.taskbar_from_nav_threshold);
+        mTaskbarNavThreshold = TaskbarThresholdUtils.getFromNavThreshold(res,
+                taskbarActivityContext.getDeviceProfile());
         mTaskbarNavThresholdY = taskbarActivityContext.getDeviceProfile().heightPx
                 - mTaskbarNavThreshold;
         mIsTaskbarAllAppsOpen =
@@ -121,7 +130,11 @@
     public void onMotionEvent(MotionEvent ev) {
         mLongPressDetector.onTouchEvent(ev);
         if (mState != STATE_ACTIVE) {
-            mDelegate.onMotionEvent(ev);
+            boolean isStashedTaskbarHovered = isMouseEvent(ev)
+                    && isStashedTaskbarHovered((int) ev.getX(), (int) ev.getY());
+            if (!isStashedTaskbarHovered) {
+                mDelegate.onMotionEvent(ev);
+            }
 
             // Only show the transient task bar if the touch events are on the screen.
             if (mTaskbarActivityContext != null && !isTrackpadMotionEvent(ev)) {
@@ -136,7 +149,7 @@
                         mHasPassedTaskbarNavThreshold = false;
                         mTaskbarActivityContext.setAutohideSuspendFlag(
                                 FLAG_AUTOHIDE_SUSPEND_TOUCHING, true);
-                        if (isInArea(x)) {
+                        if (isInTaskbarArea(x)) {
                             if (!mIsTransientTaskbar) {
                                 mLongPressDownX = x;
                                 mLongPressDownY = y;
@@ -145,10 +158,12 @@
                                 mCanceledUnstashHint = false;
                             }
                         }
-
                         if (mTransitionCallback != null && !mIsTaskbarAllAppsOpen) {
                             mTransitionCallback.onActionDown();
                         }
+                        if (mIsTransientTaskbar && isInBubbleBarArea(x)) {
+                            mIsInBubbleBarArea = true;
+                        }
                         break;
                     case MotionEvent.ACTION_POINTER_UP:
                         int ptrIdx = ev.getActionIndex();
@@ -185,7 +200,11 @@
 
                             if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold) {
                                 mHasPassedTaskbarNavThreshold = true;
-                                mTaskbarActivityContext.onSwipeToUnstashTaskbar();
+                                if (mIsInBubbleBarArea) {
+                                    mTaskbarActivityContext.onSwipeToOpenBubblebar();
+                                } else {
+                                    mTaskbarActivityContext.onSwipeToUnstashTaskbar();
+                                }
                             }
 
                             if (dY < 0) {
@@ -208,21 +227,37 @@
                             mTransitionCallback.onActionEnd();
                         }
                         mHasPassedTaskbarNavThreshold = false;
+                        mIsInBubbleBarArea = false;
+                        break;
+                    case MotionEvent.ACTION_BUTTON_RELEASE:
+                        if (isStashedTaskbarHovered) {
+                            mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HOME);
+                        }
                         break;
                 }
             }
         }
     }
 
-    private boolean isInArea(float x) {
+    private boolean isInTaskbarArea(float x) {
         float areaFromMiddle = mUnstashArea / 2.0f;
         float distFromMiddle = Math.abs(mScreenWidth / 2.0f - x);
         return distFromMiddle < areaFromMiddle;
     }
 
+    private boolean isInBubbleBarArea(float x) {
+        if (mTaskbarActivityContext != null && mIsTransientTaskbar) {
+            BubbleControllers controllers = mTaskbarActivityContext.getBubbleControllers();
+            if (controllers == null) return false;
+            Rect bubbleBarBounds = controllers.bubbleBarViewController.getBubbleBarBounds();
+            return x >= bubbleBarBounds.left && x <= bubbleBarBounds.right;
+        }
+        return false;
+    }
+
     private void onLongPressDetected(MotionEvent motionEvent) {
         if (mTaskbarActivityContext != null
-                && isInArea(motionEvent.getRawX())
+                && isInTaskbarArea(motionEvent.getRawX())
                 && !mIsTransientTaskbar) {
             boolean taskBarPressed = mTaskbarActivityContext.onLongPressToUnstashTaskbar();
             if (taskBarPressed) {
@@ -255,19 +290,17 @@
 
     private void updateHoveredTaskbarState(int x, int y) {
         DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
-        mStashedTaskbarHandleBounds.set(
+        mBottomEdgeBounds.set(
                 (dp.widthPx - (int) mUnstashArea) / 2,
-                dp.heightPx - dp.stashedTaskbarHeight,
+                dp.heightPx - mStashedTaskbarBottomEdge,
                 (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
                 dp.heightPx);
-        mBottomEdgeBounds.set(mStashedTaskbarHandleBounds);
-        mBottomEdgeBounds.top = dp.heightPx - mStashedTaskbarBottomEdge;
 
         if (mBottomEdgeBounds.contains(x, y)) {
             // If hovering stashed taskbar and then hover screen bottom edge, unstash it.
             mTaskbarActivityContext.onSwipeToUnstashTaskbar();
             mIsStashedTaskbarHovered = false;
-        } else if (!mStashedTaskbarHandleBounds.contains(x, y)) {
+        } else if (!isStashedTaskbarHovered(x, y)) {
             // If exit hovering stashed taskbar, remove hint.
             startStashedTaskbarHover(/* isHovered = */ false);
         }
@@ -275,18 +308,13 @@
 
     private void updateUnhoveredTaskbarState(int x, int y) {
         DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
-        mStashedTaskbarHandleBounds.set(
-                (dp.widthPx - (int) mUnstashArea) / 2,
-                dp.heightPx - dp.stashedTaskbarHeight,
-                (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
-                dp.heightPx);
         mBottomEdgeBounds.set(
                 0,
                 dp.heightPx - mBottomScreenEdge,
                 dp.widthPx,
                 dp.heightPx);
 
-        if (mStashedTaskbarHandleBounds.contains(x, y)) {
+        if (isStashedTaskbarHovered(x, y)) {
             // If enter hovering stashed taskbar, start hint.
             startStashedTaskbarHover(/* isHovered = */ true);
         } else if (mBottomEdgeBounds.contains(x, y)) {
@@ -299,4 +327,28 @@
         mTaskbarActivityContext.startTaskbarUnstashHint(isHovered, /* forceUnstash = */ true);
         mIsStashedTaskbarHovered = isHovered;
     }
+
+    private boolean isStashedTaskbarHovered(int x, int y) {
+        if (!mTaskbarActivityContext.isTaskbarStashed()
+                || mTaskbarActivityContext.isTaskbarAllAppsOpen()
+                || !ENABLE_CURSOR_HOVER_STATES.get()) {
+            return false;
+        }
+        DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
+        mStashedTaskbarHandleBounds.set(
+                (dp.widthPx - (int) mUnstashArea) / 2,
+                dp.heightPx - dp.stashedTaskbarHeight,
+                (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
+                dp.heightPx);
+        return mStashedTaskbarHandleBounds.contains(x, y);
+    }
+
+    private boolean isMouseEvent(MotionEvent event) {
+        return event.getSource() == InputDevice.SOURCE_MOUSE;
+    }
+
+    @Override
+    protected String getDelegatorName() {
+        return "TaskbarUnstashInputConsumer";
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/StatusBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TrackpadStatusBarInputConsumer.java
similarity index 88%
rename from quickstep/src/com/android/quickstep/inputconsumers/StatusBarInputConsumer.java
rename to quickstep/src/com/android/quickstep/inputconsumers/TrackpadStatusBarInputConsumer.java
index 898aa86..f3e21e1 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/StatusBarInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TrackpadStatusBarInputConsumer.java
@@ -27,15 +27,15 @@
 import com.android.quickstep.SystemUiProxy;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
-/** Allows the status bar to be pull down for notification shade */
-public class StatusBarInputConsumer extends DelegateInputConsumer {
+/** Allows the status bar to be pull down for notification shade using the trackpad. */
+public class TrackpadStatusBarInputConsumer extends DelegateInputConsumer {
 
     private final SystemUiProxy mSystemUiProxy;
     private final float mTouchSlop;
     private final PointF mDown = new PointF();
     private boolean mHasPassedTouchSlop;
 
-    public StatusBarInputConsumer(Context context, InputConsumer delegate,
+    public TrackpadStatusBarInputConsumer(Context context, InputConsumer delegate,
             InputMonitorCompat inputMonitor) {
         super(delegate, inputMonitor);
 
@@ -79,7 +79,12 @@
 
     private void dispatchTouchEvent(MotionEvent ev) {
         if (mSystemUiProxy.isActive()) {
-            mSystemUiProxy.onStatusBarMotionEvent(ev);
+            mSystemUiProxy.onStatusBarTrackpadEvent(ev);
         }
     }
+
+    @Override
+    protected String getDelegatorName() {
+        return "TrackpadStatusBarInputConsumer";
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index 6619dd8..49814df 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -15,10 +15,10 @@
  */
 package com.android.quickstep.interaction;
 
+import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.Utilities.mapBoundToRange;
 import static com.android.launcher3.Utilities.mapRange;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.quickstep.OverviewComponentObserver.startHomeIntentSafely;
 
 import android.animation.Animator;
@@ -95,11 +95,10 @@
 
     private static final float ANIMATION_PAUSE_ALPHA_THRESHOLD = 0.1f;
 
-    private TISBindHelper mTISBindHelper;
-    private TISBinder mBinder;
-    @Nullable private TaskbarManager mTaskbarManager = null;
-
     private final AnimatedFloat mSwipeProgress = new AnimatedFloat(this::onSwipeProgressUpdate);
+
+    private TISBindHelper mTISBindHelper;
+
     private BgDrawable mBackground;
     private View mRootView;
     private float mSwipeUpShift;
@@ -174,7 +173,7 @@
                         LOTTIE_TERTIARY_COLOR_TOKEN, R.color.all_set_bg_tertiary),
                 getTheme());
 
-        startBackgroundAnimation();
+        startBackgroundAnimation(dp.isTablet);
     }
 
     private void runOnUiHelperThread(Runnable runnable) {
@@ -185,7 +184,7 @@
         Executors.UI_HELPER_EXECUTOR.execute(runnable);
     }
 
-    private void startBackgroundAnimation() {
+    private void startBackgroundAnimation(boolean forTablet) {
         if (!Utilities.ATLEAST_S || mVibrator == null) {
             return;
         }
@@ -201,7 +200,7 @@
                     .addPrimitive(supportsThud
                                     ? VibrationEffect.Composition.PRIMITIVE_THUD
                                     : VibrationEffect.Composition.PRIMITIVE_TICK,
-                            /* scale= */ 1.0f,
+                            /* scale= */ forTablet ? 1.0f : 0.3f,
                             /* delay= */ 50)
                     .compose();
 
@@ -233,29 +232,30 @@
     }
 
     private void setSetupUIVisible(boolean visible) {
-        if (mBinder == null || mTaskbarManager == null) return;
-        mTaskbarManager.setSetupUIVisible(visible);
+        TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager();
+        if (taskbarManager == null) return;
+        taskbarManager.setSetupUIVisible(visible);
     }
 
     @Override
     protected void onResume() {
         super.onResume();
         maybeResumeOrPauseBackgroundAnimation();
-        if (mBinder != null) {
+        TISBinder binder = mTISBindHelper.getBinder();
+        if (binder != null) {
             setSetupUIVisible(true);
-            mBinder.setSwipeUpProxy(this::createSwipeUpProxy);
+            binder.setSwipeUpProxy(this::createSwipeUpProxy);
         }
     }
 
     private void onTISConnected(TISBinder binder) {
-        mBinder = binder;
-        mTaskbarManager = mBinder.getTaskbarManager();
         setSetupUIVisible(isResumed());
-        mBinder.setSwipeUpProxy(isResumed() ? this::createSwipeUpProxy : null);
-        mBinder.setOverviewTargetChangeListener(mBinder::preloadOverviewForSUWAllSet);
-        mBinder.preloadOverviewForSUWAllSet();
-        if (mTaskbarManager != null) {
-            mLauncherStartAnim = mTaskbarManager.createLauncherStartFromSuwAnim(MAX_SWIPE_DURATION);
+        binder.setSwipeUpProxy(isResumed() ? this::createSwipeUpProxy : null);
+        binder.setOverviewTargetChangeListener(binder::preloadOverviewForSUWAllSet);
+        binder.preloadOverviewForSUWAllSet();
+        TaskbarManager taskbarManager = binder.getTaskbarManager();
+        if (taskbarManager != null) {
+            mLauncherStartAnim = taskbarManager.createLauncherStartFromSuwAnim(MAX_SWIPE_DURATION);
         }
     }
 
@@ -271,10 +271,11 @@
     }
 
     private void clearBinderOverride() {
-        if (mBinder != null) {
+        TISBinder binder = mTISBindHelper.getBinder();
+        if (binder != null) {
             setSetupUIVisible(false);
-            mBinder.setSwipeUpProxy(null);
-            mBinder.setOverviewTargetChangeListener(null);
+            binder.setSwipeUpProxy(null);
+            binder.setOverviewTargetChangeListener(null);
         }
     }
 
@@ -302,7 +303,7 @@
     }
 
     private AnimatedFloat createSwipeUpProxy(GestureState state) {
-        if (state.getRunningTaskId() != getTaskId()) {
+        if (state.getTopRunningTaskId() != getTaskId()) {
             return null;
         }
         mSwipeProgress.updateValue(0);
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
index 5d25279..631cff7 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -23,9 +23,9 @@
 import android.graphics.PointF;
 import android.view.View;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.Interpolators;
 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
 import com.android.quickstep.util.LottieAnimationColorUtils;
@@ -154,7 +154,7 @@
 
     @Override
     public void onBackGestureAttempted(BackGestureResult result) {
-        if (skipGestureAttempt()) {
+        if (isGestureCompleted()) {
             return;
         }
         switch (mTutorialType) {
@@ -172,7 +172,7 @@
 
     @Override
     public void onBackGestureProgress(float diffx, float diffy, boolean isLeftGesture) {
-        if (skipGestureAttempt()) {
+        if (isGestureCompleted()) {
             return;
         }
 
@@ -183,7 +183,7 @@
                 /* upperBound = */ 1f,
                 /* toMin = */ 1f,
                 /* toMax = */ EXITING_APP_MIN_SIZE_PERCENTAGE,
-                Interpolators.DEACCEL);
+                Interpolators.DECELERATE);
 
         // shrink the exiting app as we progress through the back gesture
         mExitingAppView.setPivotX(isLeftGesture ? mScreenWidth : 0);
@@ -197,7 +197,7 @@
                 /* upperBound = */ 1f,
                 /* toMin = */ 0,
                 /* toMax = */ mExitingAppMargin,
-                Interpolators.DEACCEL)
+                Interpolators.DECELERATE)
                 * (isLeftGesture ? -1 : 1));
 
         // round the corners of the exiting app as we progress through the back gesture
@@ -241,7 +241,7 @@
 
     @Override
     public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
-        if (skipGestureAttempt()) {
+        if (isGestureCompleted()) {
             return;
         }
         if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
index a16b239..b379baa 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
@@ -42,6 +42,12 @@
         super(fromTutorialMenu);
     }
 
+    @NonNull
+    @Override
+    TutorialType getDefaultTutorialType() {
+        return TutorialType.BACK_NAVIGATION;
+    }
+
     @Nullable
     @Override
     Integer getEdgeAnimationResId() {
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java
index 8eb4059..a9dcad8 100644
--- a/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java
@@ -40,8 +40,8 @@
 import androidx.dynamicanimation.animation.SpringAnimation;
 import androidx.dynamicanimation.animation.SpringForce;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.R;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.util.VibratorWrapper;
 
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
index 62726a0..d102502 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -53,18 +53,15 @@
     static final String KEY_USE_TUTORIAL_MENU = "use_tutorial_menu";
 
     @Nullable private TutorialType[] mTutorialSteps;
-    private GestureSandboxFragment mFragment;
+    private GestureSandboxFragment mCurrentFragment;
+    private GestureSandboxFragment mPendingFragment;
 
     private int mCurrentStep;
     private int mNumSteps;
-    private boolean mShowRotationPrompt;
 
     private SharedPreferences mSharedPrefs;
     private StatsLogManager mStatsLogManager;
-
-    private View mRotationPrompt;
     private TISBindHelper mTISBindHelper;
-    private TISBinder mBinder;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -83,7 +80,7 @@
                 && args.getBoolean(KEY_USE_TUTORIAL_MENU, false)) {
             mTutorialSteps = null;
             TutorialType tutorialTypeOverride = (TutorialType) args.get(KEY_TUTORIAL_TYPE);
-            mFragment = tutorialTypeOverride == null
+            mCurrentFragment = tutorialTypeOverride == null
                     ? new MenuFragment()
                     : makeTutorialFragment(
                             tutorialTypeOverride,
@@ -91,16 +88,15 @@
                             /* fromMenu= */ true);
         } else {
             mTutorialSteps = getTutorialSteps(args);
-            mFragment = makeTutorialFragment(
+            mCurrentFragment = makeTutorialFragment(
                     mTutorialSteps[mCurrentStep - 1],
                     gestureComplete,
                     /* fromMenu= */ false);
         }
         getSupportFragmentManager().beginTransaction()
-                .add(R.id.gesture_tutorial_fragment_container, mFragment)
+                .add(R.id.gesture_tutorial_fragment_container, mCurrentFragment)
                 .commit();
 
-        mRotationPrompt = findViewById(R.id.rotation_prompt);
         if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
             correctUserOrientation();
         }
@@ -128,34 +124,45 @@
         DeviceProfile deviceProfile = InvariantDeviceProfile.INSTANCE.get(
                 getApplicationContext()).getDeviceProfile(this);
         if (deviceProfile.isTablet) {
-            mShowRotationPrompt = getResources().getConfiguration().orientation
+            boolean showRotationPrompt = getResources().getConfiguration().orientation
                     == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
-            updateVisibility(mRotationPrompt, mShowRotationPrompt ? View.VISIBLE : View.GONE);
+
+            GestureSandboxFragment recreatedFragment =
+                    showRotationPrompt || mPendingFragment == null
+                            ? null : mPendingFragment.recreateFragment();
+            showFragment(showRotationPrompt
+                    ? new RotationPromptFragment()
+                    : recreatedFragment == null
+                            ? mCurrentFragment : recreatedFragment);
         } else {
             setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
         }
     }
 
-    void updateVisibility(View view, int visibility) {
-        if (view == null || view.getVisibility() == visibility) {
-            return;
+    private void showFragment(@NonNull GestureSandboxFragment fragment) {
+        if (mCurrentFragment.recreateFragment() != null) {
+            mPendingFragment = mCurrentFragment;
         }
-        view.setVisibility(visibility);
+        mCurrentFragment = fragment;
+        getSupportFragmentManager().beginTransaction()
+                .replace(R.id.gesture_tutorial_fragment_container, mCurrentFragment)
+                .runOnCommit(() -> mCurrentFragment.onAttachedToWindow())
+                .commit();
     }
 
     @Override
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
-        if (mFragment.shouldDisableSystemGestures()) {
+        if (mCurrentFragment.shouldDisableSystemGestures()) {
             disableSystemGestures();
         }
-        mFragment.onAttachedToWindow();
+        mCurrentFragment.onAttachedToWindow();
     }
 
     @Override
     public void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-        mFragment.onDetachedFromWindow();
+        mCurrentFragment.onDetachedFromWindow();
     }
 
     @Override
@@ -170,14 +177,10 @@
     protected void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
         savedInstanceState.putStringArray(KEY_TUTORIAL_STEPS, getTutorialStepNames());
         savedInstanceState.putInt(KEY_CURRENT_STEP, mCurrentStep);
-        mFragment.onSaveInstanceState(savedInstanceState);
+        mCurrentFragment.onSaveInstanceState(savedInstanceState);
         super.onSaveInstanceState(savedInstanceState);
     }
 
-    protected boolean isRotationPromptShowing() {
-        return mShowRotationPrompt;
-    }
-
     protected SharedPreferences getSharedPrefs() {
         return mSharedPrefs;
     }
@@ -206,7 +209,7 @@
      */
     public void continueTutorial() {
         if (isTutorialComplete() || mTutorialSteps == null) {
-            mFragment.close();
+            mCurrentFragment.close();
             return;
         }
         launchTutorialStep(mTutorialSteps[mCurrentStep], false);
@@ -225,20 +228,12 @@
      * the menu when complete.
      */
     public void launchTutorialStep(@NonNull TutorialType tutorialType, boolean fromMenu) {
-        mFragment = makeTutorialFragment(tutorialType, false, fromMenu);
-        getSupportFragmentManager().beginTransaction()
-                .replace(R.id.gesture_tutorial_fragment_container, mFragment)
-                .runOnCommit(() -> mFragment.onAttachedToWindow())
-                .commit();
+        showFragment(makeTutorialFragment(tutorialType, false, fromMenu));
     }
 
     /** Launches the gesture nav tutorial menu page */
     public void launchTutorialMenu() {
-        mFragment = new MenuFragment();
-        getSupportFragmentManager().beginTransaction()
-                .replace(R.id.gesture_tutorial_fragment_container, mFragment)
-                .runOnCommit(() -> mFragment.onAttachedToWindow())
-                .commit();
+        showFragment(new MenuFragment());
     }
 
     private String[] getTutorialStepNames() {
@@ -321,7 +316,6 @@
     }
 
     private void onTISConnected(TISBinder binder) {
-        mBinder = binder;
         updateServiceState(isResumed());
     }
 
@@ -332,8 +326,9 @@
     }
 
     private void updateServiceState(boolean isEnabled) {
-        if (mBinder != null) {
-            mBinder.setGestureBlockedTaskId(isEnabled ? getTaskId() : -1);
+        TISBinder binder = mTISBindHelper.getBinder();
+        if (binder != null) {
+            binder.setGestureBlockedTaskId(isEnabled ? getTaskId() : -1);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxFragment.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxFragment.java
index d52f19a..08f2989 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxFragment.java
@@ -17,6 +17,7 @@
 
 import android.app.Activity;
 
+import androidx.annotation.Nullable;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
 
@@ -27,6 +28,11 @@
 
     void onDetachedFromWindow() {}
 
+    @Nullable
+    GestureSandboxFragment recreateFragment() {
+        return null;
+    }
+
     boolean shouldDisableSystemGestures() {
         return true;
     }
diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
index 0aa80d3..daac99b 100644
--- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
@@ -147,7 +147,7 @@
 
     @Override
     public void onBackGestureAttempted(BackGestureResult result) {
-        if (skipGestureAttempt()) {
+        if (isGestureCompleted()) {
             return;
         }
         switch (mTutorialType) {
@@ -174,7 +174,7 @@
 
     @Override
     public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
-        if (skipGestureAttempt()) {
+        if (isGestureCompleted()) {
             return;
         }
         switch (mTutorialType) {
diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
index bced8c4..3e924d7 100644
--- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
@@ -41,6 +41,12 @@
         super(fromTutorialMenu);
     }
 
+    @NonNull
+    @Override
+    TutorialType getDefaultTutorialType() {
+        return TutorialType.HOME_NAVIGATION;
+    }
+
     @Nullable
     @Override
     Integer getEdgeAnimationResId() {
diff --git a/quickstep/src/com/android/quickstep/interaction/MenuFragment.java b/quickstep/src/com/android/quickstep/interaction/MenuFragment.java
index 46f79b1..dbf141b 100644
--- a/quickstep/src/com/android/quickstep/interaction/MenuFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/MenuFragment.java
@@ -19,7 +19,6 @@
 import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_TUTORIAL_TYPE;
 import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_USE_TUTORIAL_MENU;
 
-import android.graphics.Rect;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -28,33 +27,23 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.R;
 
 /** Displays the gesture nav tutorial menu. */
 public final class MenuFragment extends GestureSandboxFragment {
 
-    @NonNull private Rect mInsets = new Rect();
-
+    @NonNull
     @Override
-    public void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        mInsets = InvariantDeviceProfile.INSTANCE.get(getContext())
-                .getDeviceProfile(getContext()).getInsets();
+    GestureSandboxFragment recreateFragment() {
+        return new MenuFragment();
     }
 
     @Override
     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
             @Nullable Bundle savedInstanceState) {
-        View root = inflater.inflate(
+        final View root = inflater.inflate(
                 R.layout.gesture_tutorial_step_menu, container, false);
 
-        root.setPadding(
-                root.getPaddingLeft() + mInsets.left,
-                root.getPaddingTop() + mInsets.top,
-                root.getPaddingRight() + mInsets.right,
-                root.getPaddingBottom() + mInsets.bottom);
-
         root.findViewById(R.id.gesture_tutorial_menu_home_button).setOnClickListener(
                 v -> launchTutorialStep(TutorialController.TutorialType.HOME_NAVIGATION));
         root.findViewById(R.id.gesture_tutorial_menu_back_button).setOnClickListener(
diff --git a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
index 6cee690..c4a2216 100644
--- a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
@@ -60,12 +60,11 @@
     NavBarGestureHandler(Context context) {
         mContext = context;
         DisplayController.Info displayInfo = DisplayController.INSTANCE.get(mContext).getInfo();
-        final int displayRotation = displayInfo.rotation;
         Point currentSize = displayInfo.currentSize;
         mDisplaySize.set(currentSize.x, currentSize.y);
         mSwipeUpTouchTracker =
                 new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/,
-                        new NavBarPosition(NavigationMode.NO_BUTTON, displayRotation),
+                        new NavBarPosition(NavigationMode.NO_BUTTON, displayInfo),
                         null /*onInterceptTouch*/, this);
         mMotionPauseDetector = new MotionPauseDetector(context);
 
diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
index 454dd17..afdc1e5 100644
--- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
@@ -15,7 +15,7 @@
  */
 package com.android.quickstep.interaction;
 
-import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.app.animation.Interpolators.ACCELERATE;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
 
 import android.animation.Animator;
@@ -179,7 +179,7 @@
 
     @Override
     public void onBackGestureAttempted(BackGestureResult result) {
-        if (skipGestureAttempt()) {
+        if (isGestureCompleted()) {
             return;
         }
         switch (mTutorialType) {
@@ -206,7 +206,7 @@
 
     @Override
     public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
-        if (skipGestureAttempt()) {
+        if (isGestureCompleted()) {
             return;
         }
         switch (mTutorialType) {
@@ -254,7 +254,7 @@
     public void animateTaskViewToOverview(boolean animateDelayedSuccessFeedback) {
         PendingAnimation anim = new PendingAnimation(TASK_VIEW_END_ANIMATION_DURATION_MILLIS);
         anim.setFloat(mTaskViewSwipeUpAnimation
-                .getCurrentShift(), AnimatedFloat.VALUE, 1, ACCEL);
+                .getCurrentShift(), AnimatedFloat.VALUE, 1, ACCELERATE);
 
         if (animateDelayedSuccessFeedback) {
             anim.addListener(new AnimatorListenerAdapter() {
diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java
index 01074dd..ee1c460 100644
--- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java
@@ -41,6 +41,12 @@
         super(fromTutorialMenu);
     }
 
+    @NonNull
+    @Override
+    TutorialType getDefaultTutorialType() {
+        return TutorialType.OVERVIEW_NAVIGATION;
+    }
+
     @Nullable
     @Override
     Integer getEdgeAnimationResId() {
diff --git a/quickstep/src/com/android/quickstep/interaction/RotationPromptFragment.java b/quickstep/src/com/android/quickstep/interaction/RotationPromptFragment.java
new file mode 100644
index 0000000..89af647
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/RotationPromptFragment.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+
+/** Displays the prompt requesting that the user rotates their device. */
+public class RotationPromptFragment extends GestureSandboxFragment {
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.gesture_tutorial_rotation_prompt, container, false);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
index 558d5dc..87defc5 100644
--- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
@@ -18,7 +18,7 @@
 import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
 
-import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.app.animation.Interpolators.ACCELERATE;
 import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 import static com.android.quickstep.AbsSwipeUpHandler.MAX_SWIPE_DURATION;
@@ -171,14 +171,14 @@
         PendingAnimation anim = new PendingAnimation(300);
         if (toOverviewFirst) {
             anim.setFloat(mTaskViewSwipeUpAnimation
-                    .getCurrentShift(), AnimatedFloat.VALUE, 1, ACCEL);
+                    .getCurrentShift(), AnimatedFloat.VALUE, 1, ACCELERATE);
             anim.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation, boolean isReverse) {
                     PendingAnimation fadeAnim =
                             new PendingAnimation(TASK_VIEW_END_ANIMATION_DURATION_MILLIS);
                     fadeAnim.setFloat(mTaskViewSwipeUpAnimation
-                            .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL);
+                            .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCELERATE);
                     if (resetViews) {
                         fadeAnim.addListener(mResetTaskView);
                     }
@@ -213,7 +213,7 @@
             });
         } else {
             anim.setFloat(mTaskViewSwipeUpAnimation
-                    .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL);
+                    .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCELERATE);
             if (resetViews) {
                 anim.addListener(mResetTaskView);
             }
@@ -239,8 +239,8 @@
         mFakeTaskView.setVisibility(View.VISIBLE);
         PendingAnimation anim = new PendingAnimation(300);
         anim.setFloat(mTaskViewSwipeUpAnimation
-                .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL);
-        anim.setViewAlpha(mFakeTaskView, 1, ACCEL);
+                .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCELERATE);
+        anim.setViewAlpha(mFakeTaskView, 1, ACCELERATE);
         anim.addListener(mResetTaskView);
         AnimatorSet animset = anim.buildAnim();
         if (animateTaskbar) {
@@ -260,7 +260,7 @@
                 mTaskViewSwipeUpAnimation.handleSwipeUpToHome(finalVelocity);
         // After home animation finishes, fade out and run onEndRunnable.
         PendingAnimation fadeAnim = new PendingAnimation(300);
-        fadeAnim.setViewAlpha(mFakeIconView, 0, ACCEL);
+        fadeAnim.setViewAlpha(mFakeIconView, 0, ACCELERATE);
         final View hotseatIconView = mHotseatIconView;
         if (hotseatIconView != null) {
             hotseatIconView.setVisibility(INVISIBLE);
@@ -282,7 +282,7 @@
 
     @Override
     public void setNavBarGestureProgress(@Nullable Float displacement) {
-        if (skipGestureAttempt()) {
+        if (isGestureCompleted()) {
             return;
         }
         if (mTutorialType == HOME_NAVIGATION_COMPLETE
@@ -303,7 +303,7 @@
 
     @Override
     public void onMotionPaused(boolean unused) {
-        if (skipGestureAttempt()) {
+        if (isGestureCompleted()) {
             return;
         }
         if (mShowTasks) {
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index 139083b..545a94d 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -487,10 +487,6 @@
         return mGestureCompleted;
     }
 
-    public boolean skipGestureAttempt() {
-        return isGestureCompleted() || mTutorialFragment.isRotationPromptShowing();
-    }
-
     void hideFeedback() {
         if (mFeedbackView.getVisibility() != View.VISIBLE) {
             return;
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
index 84326f5..a28cf58 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
@@ -100,6 +100,19 @@
         return fragment;
     }
 
+    @Nullable
+    @Override
+    GestureSandboxFragment recreateFragment() {
+        TutorialType tutorialType = mTutorialController == null
+                ? (mTutorialType == null
+                        ? getDefaultTutorialType() : mTutorialType)
+                : mTutorialController.mTutorialType;
+        return newInstance(tutorialType, isGestureComplete(), mFromTutorialMenu);
+    }
+
+    @NonNull
+    abstract TutorialType getDefaultTutorialType();
+
     TutorialFragment(boolean fromTutorialMenu) {
         mFromTutorialMenu = fromTutorialMenu;
     }
@@ -498,11 +511,6 @@
         return activity != null ? activity.getStatsLogManager() : null;
     }
 
-    protected boolean isRotationPromptShowing() {
-        GestureSandboxActivity activity = getGestureSandboxActivity();
-        return activity != null && activity.isRotationPromptShowing();
-    }
-
     @Nullable
     private SharedPreferences getSharedPreferences() {
         GestureSandboxActivity activity = getGestureSandboxActivity();
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 6288937..b1daac4 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -62,14 +62,11 @@
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.Executors;
-import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.LogConfig;
 import com.android.launcher3.views.ActivityContext;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.SysUiStatsLog;
 
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Optional;
 import java.util.OptionalInt;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -90,6 +87,7 @@
     private static final String LATENCY_TAG = "StatsLatencyLog";
     private static final String IMPRESSION_TAG = "StatsImpressionLog";
     private static final boolean IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.STATSLOG);
+    private static final boolean DEBUG = !Utilities.isRunningInTestHarness();
     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.
@@ -110,8 +108,6 @@
     public static final CopyOnWriteArrayList<StatsLogConsumer> LOGS_CONSUMER =
             new CopyOnWriteArrayList<>();
 
-    private final Context mContext;
-
     public StatsLogCompatManager(Context context) {
         mContext = context;
     }
@@ -331,6 +327,11 @@
             if (!Utilities.ATLEAST_R) {
                 return;
             }
+            if (DEBUG) {
+                String name = (event instanceof Enum) ? ((Enum) event).name() :
+                        event.getId() + "";
+                Log.d(TAG, name);
+            }
             LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
 
             if (mSlice == null && mSliceItem != null) {
@@ -566,14 +567,16 @@
      * Helps to construct and log statsd compatible impression events.
      */
     private static class StatsCompatImpressionLogger implements StatsImpressionLogger {
-        private int[] mResultTypeList = new int[]{};
-        private int[] mResultCountList = new int[]{};
-        private final List<Boolean> mAboveKeyboardList = new ArrayList<>();
-        private int[] mUidList = new int[]{};
         private InstanceId mInstanceId = DEFAULT_INSTANCE_ID;
         private State mLauncherState = State.UNKNOWN;
         private int mQueryLength = -1;
 
+        // Fields used for Impression Logging V2.
+        private int mResultType;
+        private boolean mAboveKeyboard = false;
+        private int mUid;
+        private int mResultSource;
+
         @Override
         public StatsImpressionLogger withInstanceId(InstanceId instanceId) {
             this.mInstanceId = instanceId;
@@ -593,69 +596,60 @@
         }
 
         @Override
-        public StatsImpressionLogger withResultType(IntArray resultType) {
-            mResultTypeList = resultType.toArray();
+        public StatsImpressionLogger withResultType(int resultType) {
+            mResultType = resultType;
+            return this;
+        }
+
+
+        @Override
+        public StatsImpressionLogger withAboveKeyboard(boolean aboveKeyboard) {
+            mAboveKeyboard = aboveKeyboard;
             return this;
         }
 
         @Override
-        public StatsImpressionLogger withResultCount(IntArray resultCount) {
-            mResultCountList = resultCount.toArray();
+        public StatsImpressionLogger withUid(int uid) {
+            mUid = uid;
             return this;
         }
 
         @Override
-        public StatsImpressionLogger withAboveKeyboard(List<Boolean> aboveKeyboard) {
-            mAboveKeyboardList.clear();
-            this.mAboveKeyboardList.addAll(aboveKeyboard);
-            return this;
-        }
-
-        @Override
-        public StatsImpressionLogger withUids(IntArray uid) {
-            mUidList = uid.toArray();
+        public StatsImpressionLogger withResultSource(int resultSource) {
+            mResultSource = resultSource;
             return this;
         }
 
         @Override
         public void log(EventEnum event) {
-            boolean[] mAboveKeyboard = new boolean[mAboveKeyboardList.size()];
-            for (int i = 0; i < mAboveKeyboardList.size(); i++) {
-                mAboveKeyboard[i] = mAboveKeyboardList.get(i);
-            }
             if (IS_VERBOSE) {
                 String name = (event instanceof Enum) ? ((Enum) event).name() :
                         event.getId() + "";
                 StringBuilder logStringBuilder = new StringBuilder("\n");
                 logStringBuilder.append(String.format("InstanceId:%s ", mInstanceId));
                 logStringBuilder.append(String.format("ImpressionEvent:%s ", name));
-                logStringBuilder.append(String.format("LauncherState = %s ", mLauncherState));
-                logStringBuilder.append(String.format("QueryLength = %s ", mQueryLength));
-                for (int i = 0; i < mResultTypeList.length; i++) {
-                    logStringBuilder.append(String.format(
-                            "\n ResultType = %s with ResultCount = %s with is_above_keyboard = %s"
-                                    + " with uid = %s",
-                            mResultTypeList[i], mResultCountList[i],
-                            mAboveKeyboard[i], mUidList[i]));
-                }
+                logStringBuilder.append(String.format("\n\tLauncherState = %s ", mLauncherState));
+                logStringBuilder.append(String.format("\tQueryLength = %s ", mQueryLength));
+                logStringBuilder.append(String.format(
+                        "\n\t ResultType = %s is_above_keyboard = %s"
+                                + " uid = %s result_source = %s",
+                        mResultType,
+                        mAboveKeyboard, mUid, mResultSource));
+
                 Log.d(IMPRESSION_TAG, logStringBuilder.toString());
             }
 
 
-
-            SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_IMPRESSION_EVENT,
+            SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_IMPRESSION_EVENT_V2,
                     event.getId(), // event_id
                     mInstanceId.getId(), // instance_id
                     mLauncherState.getLauncherState(), // state
                     mQueryLength, // query_length
-                    //result type list
-                    mResultTypeList,
-                    // result count list
-                    mResultCountList,
-                    // above keyboard list
-                    mAboveKeyboard,
-                    // uid list
-                    mUidList
+                    mResultType, //result type
+                    mAboveKeyboard, // above keyboard
+                    mUid, // uid
+                    mResultSource // result source
+
             );
         }
     }
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java b/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java
index 409bf9c..cca4d52 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java
+++ b/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java
@@ -18,12 +18,9 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.config.FeatureFlags;
-
 import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
@@ -150,10 +147,6 @@
         lastEventEntries.add(eventEntry);
     }
 
-    public void clear() {
-        Arrays.fill(logs, null);
-    }
-
     public void dump(String prefix, PrintWriter writer) {
         writer.println(prefix + "ActiveGestureErrorDetector:");
         for (int i = 0; i < logs.length; i++) {
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
index a92ab2a..cb35ec8 100644
--- a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -15,9 +15,9 @@
  */
 package com.android.quickstep.util;
 
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.AbsSwipeUpHandler.ALL_APPS_SHIFT_THRESHOLD;
+import static com.android.app.animation.Interpolators.DECELERATE;
+import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.LauncherPrefs.ALL_APPS_OVERVIEW_THRESHOLD;
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 
@@ -34,6 +34,7 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -55,7 +56,7 @@
 
     private enum RecentsResistanceParams {
         FROM_APP(0.75f, 0.5f, 1f, false),
-        FROM_APP_TO_ALL_APPS(0.75f, 0.5f, 0.8f, false),
+        FROM_APP_TO_ALL_APPS(1f, 0.6f, 0.8f, false),
         FROM_APP_TABLET(1f, 0.7f, 1f, true),
         FROM_APP_TO_ALL_APPS_TABLET(1f, 0.5f, 0.5f, false),
         FROM_OVERVIEW(1f, 0.75f, 0.5f, false);
@@ -91,7 +92,7 @@
         public final boolean stopScalingAtTop;
     }
 
-    private static final TimeInterpolator RECENTS_SCALE_RESIST_INTERPOLATOR = DEACCEL;
+    private static final TimeInterpolator RECENTS_SCALE_RESIST_INTERPOLATOR = DECELERATE;
     private static final TimeInterpolator RECENTS_TRANSLATE_RESIST_INTERPOLATOR = LINEAR;
 
     private static final Rect TEMP_RECT = new Rect();
@@ -188,7 +189,8 @@
                         recentsOrientedState.getOrientationHandler());
         float dragLengthFactor = (float) dp.heightPx / transitionDragLength;
         // -1s are because 0-1 is reserved for the normal transition.
-        return (ALL_APPS_SHIFT_THRESHOLD - 1) / (dragLengthFactor - 1);
+        float threshold = LauncherPrefs.get(context).get(ALL_APPS_OVERVIEW_THRESHOLD) / 100f;
+        return (threshold - 1) / (dragLengthFactor - 1);
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index cbde257..1a7099d 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -17,19 +17,30 @@
 
 package com.android.quickstep.util;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_LAUNCH;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
+import android.app.ActivityTaskManager;
 import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.Arrays;
 
 /**
  * Mini controller class that handles app pair interactions: saving, modifying, deleting, etc.
@@ -52,10 +63,13 @@
 
     private final Context mContext;
     private final SplitSelectStateController mSplitSelectStateController;
+    private final StatsLogManager mStatsLogManager;
     public AppPairsController(Context context,
-            SplitSelectStateController splitSelectStateController) {
+            SplitSelectStateController splitSelectStateController,
+            StatsLogManager statsLogManager) {
         mContext = context;
         mSplitSelectStateController = splitSelectStateController;
+        mStatsLogManager = statsLogManager;
     }
 
     /**
@@ -84,11 +98,51 @@
                 LauncherAccessibilityDelegate delegate =
                         Launcher.getLauncher(mContext).getAccessibilityDelegate();
                 if (delegate != null) {
-                    MAIN_EXECUTOR.execute(() -> delegate.addToWorkspace(newAppPair, true));
+                    delegate.addToWorkspace(newAppPair, true);
+                    mStatsLogManager.logger().withItemInfo(newAppPair)
+                            .log(StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_SAVE);
                 }
             });
         });
+    }
 
+    /**
+     * Launches an app pair by searching the RecentsModel for running instances of each app, and
+     * staging either those running instances or launching the apps as new Intents.
+     */
+    public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
+        ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user);
+        ComponentKey app2Key = new ComponentKey(app2.getTargetComponent(), app2.user);
+        mSplitSelectStateController.findLastActiveTasksAndRunCallback(
+                Arrays.asList(app1Key, app2Key),
+                foundTasks -> {
+                    @Nullable Task foundTask1 = foundTasks.get(0);
+                    Intent task1Intent;
+                    int task1Id;
+                    if (foundTask1 != null) {
+                        task1Id = foundTask1.key.id;
+                        task1Intent = null;
+                    } else {
+                        task1Id = ActivityTaskManager.INVALID_TASK_ID;
+                        task1Intent = app1.intent;
+                    }
+
+                    mSplitSelectStateController.setInitialTaskSelect(task1Intent,
+                            SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT,
+                            app1,
+                            LAUNCHER_APP_PAIR_LAUNCH,
+                            task1Id);
+
+                    @Nullable Task foundTask2 = foundTasks.get(1);
+                    if (foundTask2 != null) {
+                        mSplitSelectStateController.setSecondTask(foundTask2);
+                    } else {
+                        mSplitSelectStateController.setSecondTask(
+                                app2.intent, app2.user);
+                    }
+
+                    mSplitSelectStateController.launchSplitTasks();
+                });
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/util/AssistStateManager.java b/quickstep/src/com/android/quickstep/util/AssistStateManager.java
new file mode 100644
index 0000000..e8c1a78
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AssistStateManager.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.ResourceBasedOverride;
+
+import java.io.PrintWriter;
+
+/** Class to manage Assistant states. */
+public class AssistStateManager implements ResourceBasedOverride {
+
+    public static final MainThreadInitializedObject<AssistStateManager> INSTANCE =
+            forOverride(AssistStateManager.class, R.string.assist_state_manager_class);
+
+    public AssistStateManager() {}
+
+    /** Whether search is available. */
+    public boolean isSearchAvailable() {
+        return false;
+    }
+
+    /** Return {@code true} if the Settings toggle is enabled. */
+    public boolean isSettingsNavHandleEnabled() {
+        return false;
+    }
+
+    /** Return {@code true} if the Settings toggle is enabled. */
+    public boolean isSettingsHomeButtonEnabled() {
+        return false;
+    }
+
+    /** Dump states. */
+    public void dump(String prefix, PrintWriter writer) {}
+}
diff --git a/quickstep/src/com/android/quickstep/util/AssistUtils.java b/quickstep/src/com/android/quickstep/util/AssistUtils.java
new file mode 100644
index 0000000..11b6ea7
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AssistUtils.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import android.content.Context;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.ResourceBasedOverride;
+
+/** Utilities to work with Assistant functionality. */
+public class AssistUtils implements ResourceBasedOverride {
+
+    public AssistUtils() {}
+
+    /** Creates AssistUtils as specified by overrides */
+    public static AssistUtils newInstance(Context context) {
+        return Overrides.getObject(AssistUtils.class, context, R.string.assist_utils_class);
+    }
+
+    /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */
+    public int[] getSysUiAssistOverrideInvocationTypes() {
+        return new int[0];
+    }
+
+    /**
+     * @return {@code true} if the override was handled, i.e. an assist surface was shown or the
+     * request should be ignored. {@code false} means the caller should start assist another way.
+     */
+    public boolean tryStartAssistOverride(int invocationType) {
+        return false;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
new file mode 100644
index 0000000..0dee5b3
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
+import static android.content.Intent.ACTION_TIME_CHANGED;
+
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.ArrayMap;
+import android.widget.TextClock.ClockEventDelegate;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.util.SettingsCache;
+import com.android.launcher3.util.SettingsCache.OnChangeListener;
+import com.android.launcher3.util.SimpleBroadcastReceiver;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Extension of {@link ClockEventDelegate} to support async event registration
+ */
+public class AsyncClockEventDelegate extends ClockEventDelegate implements OnChangeListener {
+
+    private final Context mContext;
+    private final SimpleBroadcastReceiver mReceiver =
+            new SimpleBroadcastReceiver(this::onClockEventReceived);
+
+    private final ArrayMap<BroadcastReceiver, Handler> mTimeEventReceivers = new ArrayMap<>();
+    private final List<ContentObserver> mFormatObservers = new ArrayList<>();
+    private final Uri mFormatUri = Settings.System.getUriFor(Settings.System.TIME_12_24);
+
+    private boolean mFormatRegistered = false;
+    private boolean mDestroyed = false;
+
+    public AsyncClockEventDelegate(Context context) {
+        super(context);
+        mContext = context;
+
+        UI_HELPER_EXECUTOR.execute(() ->
+                mReceiver.register(mContext, ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED));
+    }
+
+    @Override
+    public void registerTimeChangeReceiver(BroadcastReceiver receiver, Handler handler) {
+        synchronized (mTimeEventReceivers) {
+            mTimeEventReceivers.put(receiver, handler == null ? new Handler() : handler);
+        }
+    }
+
+    @Override
+    public void unregisterTimeChangeReceiver(BroadcastReceiver receiver) {
+        synchronized (mTimeEventReceivers) {
+            mTimeEventReceivers.remove(receiver);
+        }
+    }
+
+    @Override
+    public void registerFormatChangeObserver(ContentObserver observer, int userHandle) {
+        synchronized (mFormatObservers) {
+            if (!mFormatRegistered && !mDestroyed) {
+                SettingsCache.INSTANCE.get(mContext).register(mFormatUri, this);
+                mFormatRegistered = true;
+            }
+            mFormatObservers.add(observer);
+        }
+    }
+
+    @Override
+    public void unregisterFormatChangeObserver(ContentObserver observer) {
+        synchronized (mFormatObservers) {
+            mFormatObservers.remove(observer);
+        }
+    }
+
+    @Override
+    public void onSettingsChanged(boolean isEnabled) {
+        if (mDestroyed) {
+            return;
+        }
+        synchronized (mFormatObservers) {
+            mFormatObservers.forEach(o -> o.dispatchChange(false, mFormatUri));
+        }
+    }
+    @WorkerThread
+    private void onClockEventReceived(Intent intent) {
+        if (mDestroyed) {
+            return;
+        }
+        synchronized (mReceiver) {
+            mTimeEventReceivers.forEach((r, h) -> h.post(() -> r.onReceive(mContext, intent)));
+        }
+    }
+
+    /**
+     * Unregisters all system callbacks and destroys this delegate
+     */
+    public void onDestroy() {
+        mDestroyed = true;
+        SettingsCache.INSTANCE.get(mContext).unregister(mFormatUri, this);
+        UI_HELPER_EXECUTOR.execute(() -> mReceiver.unregisterReceiverSafely(mContext));
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/BaseDepthController.java b/quickstep/src/com/android/quickstep/util/BaseDepthController.java
index 931e468..99f564c 100644
--- a/quickstep/src/com/android/quickstep/util/BaseDepthController.java
+++ b/quickstep/src/com/android/quickstep/util/BaseDepthController.java
@@ -18,6 +18,7 @@
 import android.app.WallpaperManager;
 import android.os.IBinder;
 import android.util.FloatProperty;
+import android.util.Log;
 import android.view.AttachedSurfaceControl;
 import android.view.SurfaceControl;
 
@@ -50,6 +51,9 @@
     private static final int DEPTH_INDEX_WIDGET = 1;
     private static final int DEPTH_INDEX_COUNT = 2;
 
+    // b/291401432
+    private static final String TAG = "BaseDepthController";
+
     protected final Launcher mLauncher;
     /** Property to set the depth for state transition. */
     public final MultiProperty stateDepth;
@@ -88,7 +92,7 @@
      */
     protected boolean mInEarlyWakeUp;
 
-    private boolean mWaitingOnSurfaceValidity;
+    protected boolean mWaitingOnSurfaceValidity;
 
     public BaseDepthController(Launcher activity) {
         mLauncher = activity;
@@ -133,9 +137,11 @@
             return;
         }
         if (mSurface == null) {
+            Log.d(TAG, "mSurface is null and mCurrentBlur is: " + mCurrentBlur);
             return;
         }
         if (!mSurface.isValid()) {
+            Log.d(TAG, "mSurface is not valid");
             mWaitingOnSurfaceValidity = true;
             onInvalidSurface();
             return;
@@ -186,6 +192,8 @@
     protected void setSurface(SurfaceControl surface) {
         if (mSurface != surface || mWaitingOnSurfaceValidity) {
             mSurface = surface;
+            Log.d(TAG, "setSurface:\n\tmWaitingOnSurfaceValidity: " + mWaitingOnSurfaceValidity
+                    + "\n\tmSurface: " + mSurface);
             applyDepthAndBlur();
         }
     }
diff --git a/quickstep/src/com/android/quickstep/util/BinderTracker.java b/quickstep/src/com/android/quickstep/util/BinderTracker.java
deleted file mode 100644
index cb04e5b..0000000
--- a/quickstep/src/com/android/quickstep/util/BinderTracker.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.util;
-
-import android.os.Binder;
-import android.os.IBinder;
-import android.os.Looper;
-import android.util.Log;
-
-import com.android.launcher3.config.FeatureFlags;
-
-/**
- * Utility class to test and check binder calls during development.
- */
-public class BinderTracker {
-
-    private static final String TAG = "BinderTracker";
-
-    public static void start() {
-        if (!FeatureFlags.IS_STUDIO_BUILD) {
-            Log.wtf(TAG, "Accessing tracker in released code.", new Exception());
-            return;
-        }
-
-        Binder.setProxyTransactListener(new Tracker());
-    }
-
-    public static void stop() {
-        if (!FeatureFlags.IS_STUDIO_BUILD) {
-            Log.wtf(TAG, "Accessing tracker in released code.", new Exception());
-            return;
-        }
-        Binder.setProxyTransactListener(null);
-    }
-
-    private static class Tracker implements Binder.ProxyTransactListener {
-
-        @Override
-        public Object onTransactStarted(IBinder iBinder, int code) {
-            if (Looper.myLooper() == Looper.getMainLooper()) {
-                Log.e(TAG, "Binder call on ui thread", new Exception());
-            }
-            return null;
-        }
-
-        @Override
-        public void onTransactEnded(Object session) { }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.java b/quickstep/src/com/android/quickstep/util/BorderAnimator.java
index 011d45c..7563187 100644
--- a/quickstep/src/com/android/quickstep/util/BorderAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/BorderAnimator.java
@@ -29,9 +29,9 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Px;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimatorListeners;
-import com.android.launcher3.anim.Interpolators;
 
 /**
  * Utility class for drawing a rounded-rect border around a view.
diff --git a/quickstep/src/com/android/quickstep/util/FadeOutRemoteTransition.kt b/quickstep/src/com/android/quickstep/util/FadeOutRemoteTransition.kt
new file mode 100644
index 0000000..59ff81d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/FadeOutRemoteTransition.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.animation.ValueAnimator
+import android.os.IBinder
+import android.os.RemoteException
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+import android.window.IRemoteTransition
+import android.window.IRemoteTransitionFinishedCallback
+import android.window.TransitionInfo
+import com.android.launcher3.anim.AnimatorListeners.forEndCallback
+import com.android.launcher3.util.Executors
+import com.android.wm.shell.util.TransitionUtil
+
+/** Remote animation which fades out the closing targets */
+class FadeOutRemoteTransition : IRemoteTransition.Stub() {
+
+    override fun mergeAnimation(
+        iBinder: IBinder,
+        transitionInfo: TransitionInfo,
+        transaction: Transaction,
+        mergeTarget: IBinder,
+        finishCB: IRemoteTransitionFinishedCallback
+    ) {
+
+        try {
+            finishCB.onTransitionFinished(null, Transaction())
+        } catch (e: RemoteException) {
+            // Ignore
+        }
+    }
+
+    override fun startAnimation(
+        transition: IBinder,
+        info: TransitionInfo,
+        startT: Transaction,
+        finishCB: IRemoteTransitionFinishedCallback
+    ) {
+        val anim = ValueAnimator.ofFloat(1f, 0f)
+
+        val closingControls: MutableList<SurfaceControl> = mutableListOf()
+        for (chg in info.changes) {
+            startT.show(chg.leash)
+            if (TransitionUtil.isClosingType(chg.mode)) {
+                closingControls.add(chg.leash)
+            }
+        }
+        startT.apply()
+
+        anim.addUpdateListener {
+            val t = Transaction()
+            closingControls.forEach { t.setAlpha(it, anim.animatedValue as Float) }
+            t.apply()
+        }
+        anim.addListener(
+            forEndCallback(
+                Runnable {
+                    val t = Transaction()
+                    closingControls.forEach { t.hide(it) }
+                    try {
+                        finishCB.onTransitionFinished(null, t)
+                    } catch (e: RemoteException) {
+                        // Ignore
+                    }
+                }
+            )
+        )
+
+        Executors.MAIN_EXECUTOR.execute { anim.start() }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java
index 2be4f0a..9c49647 100644
--- a/quickstep/src/com/android/quickstep/util/GroupTask.java
+++ b/quickstep/src/com/android/quickstep/util/GroupTask.java
@@ -37,6 +37,10 @@
     @TaskView.Type
     public final int taskViewType;
 
+    public GroupTask(@NonNull Task task) {
+        this(task, null, null);
+    }
+
     public GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds) {
         this(t1, t2, splitBounds, t2 != null ? TaskView.Type.GROUPED : TaskView.Type.SINGLE);
     }
diff --git a/quickstep/src/com/android/quickstep/util/LogUtils.kt b/quickstep/src/com/android/quickstep/util/LogUtils.kt
index 23a41f6..e34c4ec 100644
--- a/quickstep/src/com/android/quickstep/util/LogUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/LogUtils.kt
@@ -20,6 +20,11 @@
 import com.android.launcher3.logging.InstanceId
 
 object LogUtils {
+    @JvmStatic
+    fun splitFailureMessage(caller: String, reason: String): String {
+        return "($caller) Splitscreen aborted: $reason"
+    }
+
     /**
      * @return a [Pair] of two InstanceIds but with different types, one that can be used by
      *   framework (if needing to pass through an intent or such) and one used in Launcher
diff --git a/quickstep/src/com/android/quickstep/util/NavBarPosition.java b/quickstep/src/com/android/quickstep/util/NavBarPosition.java
index 59c8263..9418512 100644
--- a/quickstep/src/com/android/quickstep/util/NavBarPosition.java
+++ b/quickstep/src/com/android/quickstep/util/NavBarPosition.java
@@ -27,25 +27,22 @@
  */
 public class NavBarPosition {
 
+    private final boolean mIsTablet;
     private final NavigationMode mMode;
     private final int mDisplayRotation;
 
     public NavBarPosition(NavigationMode mode, Info info) {
+        mIsTablet = info.isTablet(info.realBounds);
         mMode = mode;
         mDisplayRotation = info.rotation;
     }
 
-    public NavBarPosition(NavigationMode mode, int displayRotation) {
-        mMode = mode;
-        mDisplayRotation = displayRotation;
-    }
-
     public boolean isRightEdge() {
-        return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_90;
+        return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_90 && !mIsTablet;
     }
 
     public boolean isLeftEdge() {
-        return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_270;
+        return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_270 && !mIsTablet;
     }
 
     public float getRotation() {
diff --git a/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java b/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java
index 3cec1a4..6d9ecd9 100644
--- a/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java
+++ b/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java
@@ -22,12 +22,16 @@
 import android.animation.AnimatorSet;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.states.StateAnimationConfig;
 
+import java.util.function.Consumer;
+
 /**
  * Runs an animation from overview to home. Currently, this animation is just a wrapper around the
  * normal state transition and may play a {@link WorkspaceRevealAnim} if we're starting from an
@@ -39,14 +43,18 @@
 
     private final Launcher mLauncher;
     private final Runnable mOnReachedHome;
+    @Nullable
+    private final Consumer<AnimatorSet> mSplitCancelConsumer;
 
     // Only run mOnReachedHome when both of these are true.
     private boolean mIsHomeStaggeredAnimFinished;
     private boolean mIsOverviewHidden;
 
-    public OverviewToHomeAnim(Launcher launcher, Runnable onReachedHome) {
+    public OverviewToHomeAnim(Launcher launcher, Runnable onReachedHome,
+            @Nullable Consumer<AnimatorSet> splitCancelConsumer) {
         mLauncher = launcher;
         mOnReachedHome = onReachedHome;
+        mSplitCancelConsumer = splitCancelConsumer;
     }
 
     /**
@@ -92,6 +100,11 @@
                 maybeOverviewToHomeAnimComplete();
             }
         });
+
+        if (mSplitCancelConsumer != null) {
+            // Clear split state when swiping to home
+            mSplitCancelConsumer.accept(anim);
+        }
         anim.play(stateAnim);
         stateManager.setCurrentAnimation(anim, NORMAL);
         anim.start();
diff --git a/quickstep/src/com/android/quickstep/util/OverviewToSplitTimings.java b/quickstep/src/com/android/quickstep/util/OverviewToSplitTimings.java
index e189a66..3027f79 100644
--- a/quickstep/src/com/android/quickstep/util/OverviewToSplitTimings.java
+++ b/quickstep/src/com/android/quickstep/util/OverviewToSplitTimings.java
@@ -16,8 +16,8 @@
 
 package com.android.quickstep.util;
 
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED;
-import static com.android.launcher3.anim.Interpolators.INSTANT;
+import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.app.animation.Interpolators.INSTANT;
 
 import android.view.animation.Interpolator;
 
diff --git a/quickstep/src/com/android/quickstep/util/PhoneOverviewToSplitTimings.java b/quickstep/src/com/android/quickstep/util/PhoneOverviewToSplitTimings.java
index f1dde53..a38f437 100644
--- a/quickstep/src/com/android/quickstep/util/PhoneOverviewToSplitTimings.java
+++ b/quickstep/src/com/android/quickstep/util/PhoneOverviewToSplitTimings.java
@@ -16,7 +16,7 @@
 
 package com.android.quickstep.util;
 
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED;
+import static com.android.app.animation.Interpolators.EMPHASIZED;
 
 import android.view.animation.Interpolator;
 
diff --git a/quickstep/src/com/android/quickstep/util/ProtoTracer.java b/quickstep/src/com/android/quickstep/util/ProtoTracer.java
deleted file mode 100644
index ef9586d..0000000
--- a/quickstep/src/com/android/quickstep/util/ProtoTracer.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.util;
-
-import static com.android.launcher3.tracing.LauncherTraceFileProto.MagicNumber.MAGIC_NUMBER_H_VALUE;
-import static com.android.launcher3.tracing.LauncherTraceFileProto.MagicNumber.MAGIC_NUMBER_L_VALUE;
-
-import android.content.Context;
-import android.os.SystemClock;
-
-import android.os.Trace;
-import com.android.launcher3.tracing.LauncherTraceProto;
-import com.android.launcher3.tracing.LauncherTraceEntryProto;
-import com.android.launcher3.tracing.LauncherTraceFileProto;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.systemui.shared.tracing.FrameProtoTracer;
-import com.android.systemui.shared.tracing.FrameProtoTracer.ProtoTraceParams;
-import com.android.systemui.shared.tracing.ProtoTraceable;
-import com.google.protobuf.MessageLite;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Queue;
-
-
-/**
- * Controller for coordinating winscope proto tracing.
- */
-public class ProtoTracer implements ProtoTraceParams<MessageLite.Builder,
-        LauncherTraceFileProto.Builder, LauncherTraceEntryProto.Builder,
-                LauncherTraceProto.Builder> {
-
-    public static final MainThreadInitializedObject<ProtoTracer> INSTANCE =
-            new MainThreadInitializedObject<>(ProtoTracer::new);
-
-    private static final String TAG = "ProtoTracer";
-    private static final long MAGIC_NUMBER_VALUE =
-            ((long) MAGIC_NUMBER_H_VALUE << 32) | MAGIC_NUMBER_L_VALUE;
-
-    private final Context mContext;
-    private final FrameProtoTracer<MessageLite.Builder, LauncherTraceFileProto.Builder,
-        LauncherTraceEntryProto.Builder, LauncherTraceProto.Builder> mProtoTracer;
-
-    public ProtoTracer(Context context) {
-        mContext = context;
-        mProtoTracer = new FrameProtoTracer<>(this);
-    }
-
-    @Override
-    public File getTraceFile() {
-        return new File(mContext.getFilesDir(), "launcher_trace.pb");
-    }
-
-    @Override
-    public LauncherTraceFileProto.Builder getEncapsulatingTraceProto() {
-        return LauncherTraceFileProto.newBuilder();
-    }
-
-    @Override
-    public LauncherTraceEntryProto.Builder updateBufferProto(
-            LauncherTraceEntryProto.Builder reuseObj,
-            ArrayList<ProtoTraceable<LauncherTraceProto.Builder>> traceables) {
-        Trace.beginSection("ProtoTracer.updateBufferProto");
-        LauncherTraceEntryProto.Builder proto = LauncherTraceEntryProto.newBuilder();
-        proto.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
-        LauncherTraceProto.Builder launcherProto = LauncherTraceProto.newBuilder();
-        for (ProtoTraceable t : traceables) {
-            t.writeToProto(launcherProto);
-        }
-        proto.setLauncher(launcherProto);
-        Trace.endSection();
-        return proto;
-    }
-
-    @Override
-    public byte[] serializeEncapsulatingProto(LauncherTraceFileProto.Builder encapsulatingProto,
-            Queue<LauncherTraceEntryProto.Builder> buffer) {
-        Trace.beginSection("ProtoTracer.serializeEncapsulatingProto");
-        encapsulatingProto.setMagicNumber(MAGIC_NUMBER_VALUE);
-        for (LauncherTraceEntryProto.Builder entry : buffer) {
-            encapsulatingProto.addEntry(entry);
-        }
-        byte[] bytes = encapsulatingProto.build().toByteArray();
-        Trace.endSection();
-        return bytes;
-    }
-
-    @Override
-    public byte[] getProtoBytes(MessageLite.Builder proto) {
-        return proto.build().toByteArray();
-    }
-
-    @Override
-    public int getProtoSize(MessageLite.Builder proto) {
-        return proto.build().getSerializedSize();
-    }
-
-    public void start() {
-        mProtoTracer.start();
-    }
-
-    public void stop() {
-        mProtoTracer.stop();
-    }
-
-    public void add(ProtoTraceable<LauncherTraceProto.Builder> traceable) {
-        mProtoTracer.add(traceable);
-    }
-
-    public void remove(ProtoTraceable<LauncherTraceProto.Builder> traceable) {
-        mProtoTracer.remove(traceable);
-    }
-
-    public void scheduleFrameUpdate() {
-        mProtoTracer.scheduleFrameUpdate();
-    }
-
-    public void update() {
-        mProtoTracer.update();
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/ProxyScreenStatusProvider.java b/quickstep/src/com/android/quickstep/util/ProxyScreenStatusProvider.java
deleted file mode 100644
index 8f79ccf..0000000
--- a/quickstep/src/com/android/quickstep/util/ProxyScreenStatusProvider.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import androidx.annotation.NonNull;
-
-import com.android.systemui.unfold.updates.screen.ScreenStatusProvider;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Screen status provider implementation that exposes methods to provide screen
- * status updates to listeners. It is used to receive screen turned on event from
- * SystemUI to Launcher.
- */
-public class ProxyScreenStatusProvider implements ScreenStatusProvider {
-
-    public static final ProxyScreenStatusProvider INSTANCE = new ProxyScreenStatusProvider();
-    private final List<ScreenListener> mListeners = new ArrayList<>();
-
-    /**
-     * Called when the screen is on and ready (windows are drawn and screen blocker is removed)
-     */
-    public void onScreenTurnedOn() {
-        mListeners.forEach(ScreenListener::onScreenTurnedOn);
-    }
-
-    /** Called when the screen is starting to turn on. */
-    public void onScreenTurningOn() {
-        mListeners.forEach(ScreenListener::onScreenTurningOn);
-    }
-
-    /** Called when the screen is starting to turn off. */
-    public void onScreenTurningOff() {
-        mListeners.forEach(ScreenListener::onScreenTurningOff);
-    }
-
-    @Override
-    public void addCallback(@NonNull ScreenListener listener) {
-        mListeners.add(listener);
-    }
-
-    @Override
-    public void removeCallback(@NonNull ScreenListener listener) {
-        mListeners.remove(listener);
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
deleted file mode 100644
index 10f2eaa..0000000
--- a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
+++ /dev/null
@@ -1,47 +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.animation.AnimatorSet;
-import android.view.RemoteAnimationTarget;
-
-public abstract class RemoteAnimationProvider {
-
-    public abstract AnimatorSet createWindowAnimation(RemoteAnimationTarget[] appTargets,
-            RemoteAnimationTarget[] wallpaperTargets);
-
-    /**
-     * @return the target with the lowest opaque layer for a certain app animation, or null.
-     */
-    public static RemoteAnimationTarget findLowestOpaqueLayerTarget(
-            RemoteAnimationTarget[] appTargets, int mode) {
-        int lowestLayer = Integer.MAX_VALUE;
-        int lowestLayerIndex = -1;
-        for (int i = appTargets.length - 1; i >= 0; i--) {
-            RemoteAnimationTarget target = appTargets[i];
-            if (target.mode == mode && !target.isTranslucent) {
-                int layer = target.prefixOrderIndex;
-                if (layer < lowestLayer) {
-                    lowestLayer = layer;
-                    lowestLayerIndex = i;
-                }
-            }
-        }
-        return lowestLayerIndex != -1
-                ? appTargets[lowestLayerIndex]
-                : null;
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java b/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java
deleted file mode 100644
index 382cf79..0000000
--- a/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static android.view.RemoteAnimationTarget.MODE_CLOSING;
-
-import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl.Transaction;
-
-import com.android.quickstep.RemoteAnimationTargets;
-
-/**
- * Animation listener which fades out the closing targets
- */
-public class RemoteFadeOutAnimationListener implements AnimatorUpdateListener {
-
-    private final RemoteAnimationTargets mTarget;
-    private boolean mFirstFrame = true;
-
-    public RemoteFadeOutAnimationListener(RemoteAnimationTarget[] appTargets,
-            RemoteAnimationTarget[] wallpaperTargets) {
-        mTarget = new RemoteAnimationTargets(appTargets, wallpaperTargets,
-                new RemoteAnimationTarget[0], MODE_CLOSING);
-    }
-
-    @Override
-    public void onAnimationUpdate(ValueAnimator valueAnimator) {
-        Transaction t = new Transaction();
-        if (mFirstFrame) {
-            for (RemoteAnimationTarget target : mTarget.unfilteredApps) {
-                t.show(target.leash);
-            }
-            mFirstFrame = false;
-        }
-
-        float alpha = 1 - valueAnimator.getAnimatedFraction();
-        for (RemoteAnimationTarget app : mTarget.apps) {
-            t.setAlpha(app.leash, alpha);
-        }
-        t.apply();
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index b76fe5c..c3774eb 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -17,14 +17,26 @@
 
 package com.android.quickstep.util
 
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
 import android.animation.ObjectAnimator
 import android.graphics.Bitmap
+import android.graphics.Rect
+import android.graphics.RectF
 import android.graphics.drawable.Drawable
 import android.view.View
+import com.android.app.animation.Interpolators
 import com.android.launcher3.DeviceProfile
+import com.android.launcher3.Utilities
 import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.statemanager.StatefulActivity
 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource
+import com.android.launcher3.views.BaseDragLayer
+import com.android.quickstep.views.FloatingTaskView
 import com.android.quickstep.views.IconView
+import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.SplitInstructionsView
 import com.android.quickstep.views.TaskThumbnailView
 import com.android.quickstep.views.TaskView
 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
@@ -176,4 +188,91 @@
                     TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, 0f))
         }
     }
+
+    /** Does not play any animation if user is not currently in split selection state. */
+    fun playPlaceholderDismissAnim(launcher: StatefulActivity<*>) {
+        if (!splitSelectStateController.isSplitSelectActive) {
+            return
+        }
+
+        val anim = createPlaceholderDismissAnim(launcher)
+        anim.start()
+    }
+
+    /** Returns [AnimatorSet] which slides initial split placeholder view offscreen. */
+    fun createPlaceholderDismissAnim(launcher: StatefulActivity<*>) : AnimatorSet {
+        val animatorSet = AnimatorSet()
+        val recentsView : RecentsView<*, *> = launcher.getOverviewPanel()
+        val floatingTask: FloatingTaskView = splitSelectStateController.firstFloatingTaskView
+                ?: return animatorSet
+
+        // We are in split selection state currently, transitioning to another state
+        val dragLayer: BaseDragLayer<*> = launcher.dragLayer
+        val onScreenRectF = RectF()
+        Utilities.getBoundsForViewInDragLayer(dragLayer, floatingTask,
+                Rect(0, 0, floatingTask.width, floatingTask.height),
+                false, null, onScreenRectF)
+        // Get the part of the floatingTask that intersects with the DragLayer (i.e. the
+        // on-screen portion)
+        onScreenRectF.intersect(
+                dragLayer.left.toFloat(),
+                dragLayer.top.toFloat(),
+                dragLayer.right.toFloat(),
+                dragLayer.bottom
+                        .toFloat()
+        )
+        animatorSet.play(ObjectAnimator.ofFloat(floatingTask,
+                FloatingTaskView.PRIMARY_TRANSLATE_OFFSCREEN,
+                recentsView.pagedOrientationHandler
+                        .getFloatingTaskOffscreenTranslationTarget(
+                                floatingTask,
+                                onScreenRectF,
+                                floatingTask.stagePosition,
+                                launcher.deviceProfile
+                        )))
+        animatorSet.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator) {
+                splitSelectStateController.resetState()
+                safeRemoveViewFromDragLayer(launcher,
+                        splitSelectStateController.splitInstructionsView)
+            }
+        })
+        return animatorSet
+    }
+
+    /**
+     * Returns a [PendingAnimation] to animate in the chip to instruct a user to select a second
+     * app for splitscreen
+     */
+    fun getShowSplitInstructionsAnim(launcher: StatefulActivity<*>) : PendingAnimation {
+        safeRemoveViewFromDragLayer(launcher, splitSelectStateController.splitInstructionsView)
+        val splitInstructionsView = SplitInstructionsView.getSplitInstructionsView(launcher)
+        splitSelectStateController.splitInstructionsView = splitInstructionsView
+        val timings = AnimUtils.getDeviceOverviewToSplitTimings(launcher.deviceProfile.isTablet)
+        val anim = PendingAnimation(100 /*duration */)
+        anim.setViewAlpha(splitInstructionsView, 1f,
+                Interpolators.clampToProgress(Interpolators.LINEAR,
+                        timings.instructionsContainerFadeInStartOffset,
+                        timings.instructionsContainerFadeInEndOffset))
+        anim.setViewAlpha(splitInstructionsView!!.textView, 1f,
+                Interpolators.clampToProgress(Interpolators.LINEAR,
+                        timings.instructionsTextFadeInStartOffset,
+                        timings.instructionsTextFadeInEndOffset))
+        anim.addFloat(splitInstructionsView, SplitInstructionsView.UNFOLD, 0.1f, 1f,
+                Interpolators.clampToProgress(Interpolators.EMPHASIZED_DECELERATE,
+                        timings.instructionsUnfoldStartOffset,
+                        timings.instructionsUnfoldEndOffset))
+        return anim
+    }
+
+    /** Removes the split instructions view from [launcher] drag layer. */
+    fun removeSplitInstructionsView(launcher: StatefulActivity<*>) {
+        safeRemoveViewFromDragLayer(launcher, splitSelectStateController.splitInstructionsView)
+    }
+
+    private fun safeRemoveViewFromDragLayer(launcher: StatefulActivity<*>, view: View?) {
+        if (view != null) {
+            launcher.dragLayer.removeView(view)
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
index 7dc1b32..93f2255 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
@@ -16,7 +16,7 @@
 
 package com.android.quickstep.util;
 
-import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.LINEAR;
 
 import android.view.animation.Interpolator;
 
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
index e073264..ae500a1 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
@@ -45,7 +45,11 @@
  *
  * After setting the correct fields for initial/second.* variables, this converts them into the
  * correct [PendingIntent] and [ShortcutInfo] objects where applicable and sends the necessary
- * data back via [getSplitLaunchData].
+ * data back via [getSplitLaunchData]. Note: there should be only one "initial" field and one
+ * "second" field set, with the rest remaining null. (Exception: [Intent] and [UserHandle] are
+ * always passed in together as a set, and are converted to a single [PendingIntent] or
+ * [ShortcutInfo]+[PendingIntent] before launch.)
+ *
  * [SplitLaunchType] indicates the type of tasks/apps/intents being launched given the provided
  * state
  */
@@ -80,21 +84,21 @@
         const val SPLIT_SINGLE_SHORTCUT_FULLSCREEN = 8
     }
 
-
     @StagePosition
     private var initialStagePosition: Int = STAGE_POSITION_UNDEFINED
-    private var initialTaskId: Int = INVALID_TASK_ID
-    private var secondTaskId: Int = INVALID_TASK_ID
-    private var initialUser: UserHandle? = null
-    private var secondUser: UserHandle? = null
-    private var initialIntent: Intent? = null
-    private var secondIntent: Intent? = null
-    private var secondPendingIntent: PendingIntent? = null
     private var itemInfo: ItemInfo? = null
     private var splitEvent: EventEnum? = null
+
+    private var initialTaskId: Int = INVALID_TASK_ID
+    private var secondTaskId: Int = INVALID_TASK_ID
+    private var initialIntent: Intent? = null
+    private var secondIntent: Intent? = null
+    private var initialUser: UserHandle? = null
+    private var secondUser: UserHandle? = null
+    private var initialPendingIntent: PendingIntent? = null
+    private var secondPendingIntent: PendingIntent? = null
     private var initialShortcut: ShortcutInfo? = null
     private var secondShortcut: ShortcutInfo? = null
-    private var initialPendingIntent: PendingIntent? = null
 
     /**
      * @param alreadyRunningTask if set to [android.app.ActivityTaskManager.INVALID_TASK_ID]
@@ -217,7 +221,7 @@
      *   split task in fullscreen
      */
     fun getFullscreenLaunchData() : SplitLaunchData {
-        // Convert all intents to shortcut infos to see if determine if we launch shortcut or intent
+        // Convert all intents to shortcut infos to determine if we launch shortcut or intent
         convertIntentsToFinalTypes()
         val splitLaunchType = getFullscreenLaunchType()
 
@@ -370,6 +374,18 @@
         return secondTaskId
     }
 
+    fun getSplitEvent(): EventEnum? {
+        return splitEvent
+    }
+
+    fun getInitialStagePosition(): Int {
+        return initialStagePosition
+    }
+
+    fun getItemInfo(): ItemInfo? {
+        return itemInfo
+    }
+
     private fun isSecondTaskIntentSet(): Boolean {
         return secondTaskId != INVALID_TASK_ID || secondIntent != null
                 || secondPendingIntent != null
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index f25619d..d4ddf76 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -16,14 +16,16 @@
 
 package com.android.quickstep.util;
 
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT;
-import static android.app.PendingIntent.FLAG_MUTABLE;
-
 import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM;
+import static com.android.launcher3.testing.shared.TestProtocol.LAUNCH_SPLIT_PAIR;
+import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO;
-import static com.android.launcher3.util.SplitConfigurationOptions.getOppositeStagePosition;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_PENDINGINTENT;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_TASK;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SHORTCUT_TASK;
@@ -34,6 +36,8 @@
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_SHORTCUT;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_TASK;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
@@ -43,10 +47,14 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.util.Log;
 import android.util.Pair;
@@ -61,28 +69,42 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.InstanceId;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
+import com.android.quickstep.OverviewComponentObserver;
+import com.android.quickstep.RecentsAnimationCallbacks;
+import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.RecentsModel;
+import com.android.quickstep.SplitSelectionListener;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.TaskViewUtils;
 import com.android.quickstep.views.FloatingTaskView;
 import com.android.quickstep.views.GroupedTaskView;
-import com.android.quickstep.views.TaskView;
+import com.android.quickstep.views.SplitInstructionsView;
+import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
+import com.android.wm.shell.splitscreen.ISplitSelectListener;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.function.Consumer;
 
 /**
@@ -92,36 +114,18 @@
 public class SplitSelectStateController {
     private static final String TAG = "SplitSelectStateCtor";
 
-    private final Context mContext;
+    private Context mContext;
     private final Handler mHandler;
     private final RecentsModel mRecentTasksModel;
     private final SplitAnimationController mSplitAnimationController;
     private final AppPairsController mAppPairsController;
     private final SplitSelectDataHolder mSplitSelectDataHolder;
-    private StatsLogManager mStatsLogManager;
+    private final StatsLogManager mStatsLogManager;
     private final SystemUiProxy mSystemUiProxy;
     private final StateManager mStateManager;
+    private SplitFromDesktopController mSplitFromDesktopController;
     @Nullable
     private DepthController mDepthController;
-    private @StagePosition int mInitialStagePosition;
-    private ItemInfo mItemInfo;
-    /** {@link #mInitialTaskIntent} and {@link #mInitialUser} (the user of the Intent) are set
-     * together when split is initiated from an Intent. */
-    private Intent mInitialTaskIntent;
-    private UserHandle mInitialUser;
-    private int mInitialTaskId = INVALID_TASK_ID;
-    /** {@link #mSecondTaskIntent} and {@link #mSecondUser} (the user of the Intent) are set
-     * together when split is confirmed with an Intent. Either this or {@link #mSecondPendingIntent}
-     * will be set, but not both
-     */
-    private Intent mSecondTaskIntent;
-    /**
-     * Set when split is confirmed via a widget. Either this or {@link #mSecondTaskIntent} will be
-     * set, but not both
-     */
-    private PendingIntent mSecondPendingIntent;
-    private UserHandle mSecondUser;
-    private int mSecondTaskId = INVALID_TASK_ID;
     private boolean mRecentsAnimationRunning;
     /** If {@code true}, animates the existing task view split placeholder view */
     private boolean mAnimateCurrentTaskDismissal;
@@ -133,10 +137,11 @@
     /** If not null, this is the TaskView we want to launch from */
     @Nullable
     private GroupedTaskView mLaunchingTaskView;
-    /** Represents where split is intended to be invoked from. */
-    private StatsLogManager.EventEnum mSplitEvent;
 
     private FloatingTaskView mFirstFloatingTaskView;
+    private SplitInstructionsView mSplitInstructionsView;
+
+    private final List<SplitSelectionListener> mSplitSelectionListeners = new ArrayList<>();
 
     public SplitSelectStateController(Context context, Handler handler, StateManager stateManager,
             DepthController depthController, StatsLogManager statsLogManager,
@@ -149,10 +154,14 @@
         mDepthController = depthController;
         mRecentTasksModel = recentsModel;
         mSplitAnimationController = new SplitAnimationController(this);
-        mAppPairsController = new AppPairsController(context, this);
+        mAppPairsController = new AppPairsController(context, this, statsLogManager);
         mSplitSelectDataHolder = new SplitSelectDataHolder(mContext);
     }
 
+    public void onDestroy() {
+        mContext = null;
+    }
+
     /**
      * @param alreadyRunningTask if set to {@link android.app.ActivityTaskManager#INVALID_TASK_ID}
      *                           then @param intent will be used to launch the initial task
@@ -161,19 +170,8 @@
     public void setInitialTaskSelect(@Nullable Intent intent, @StagePosition int stagePosition,
             @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent,
             int alreadyRunningTask) {
-        if (alreadyRunningTask != INVALID_TASK_ID) {
-            mInitialTaskId = alreadyRunningTask;
-        } else {
-            mInitialTaskIntent = intent;
-            mInitialUser = itemInfo.user;
-        }
-
-        setInitialData(stagePosition, splitEvent, itemInfo);
-
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            mSplitSelectDataHolder.setInitialTaskSelect(intent, stagePosition, itemInfo, splitEvent,
-                    alreadyRunningTask);
-        }
+        mSplitSelectDataHolder.setInitialTaskSelect(intent, stagePosition, itemInfo, splitEvent,
+                alreadyRunningTask);
     }
 
     /**
@@ -183,53 +181,50 @@
     public void setInitialTaskSelect(ActivityManager.RunningTaskInfo info,
             @StagePosition int stagePosition, @NonNull ItemInfo itemInfo,
             StatsLogManager.EventEnum splitEvent) {
-        mInitialTaskId = info.taskId;
-        setInitialData(stagePosition, splitEvent, itemInfo);
-
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            mSplitSelectDataHolder.setInitialTaskSelect(info, stagePosition, itemInfo, splitEvent);
-        }
-    }
-
-    private void setInitialData(@StagePosition int stagePosition,
-            StatsLogManager.EventEnum splitEvent, ItemInfo itemInfo) {
-        mItemInfo = itemInfo;
-        mInitialStagePosition = stagePosition;
-        mSplitEvent = splitEvent;
+        mSplitSelectDataHolder.setInitialTaskSelect(info, stagePosition, itemInfo, splitEvent);
     }
 
     /**
-     * Pulls the list of active Tasks from RecentsModel, and finds the most recently active Task
-     * matching a given ComponentName. Then uses that Task (which could be null) with the given
-     * callback.
+     * Maps a List<ComponentKey> to List<@Nullable Task>, searching through active Tasks in
+     * RecentsModel. If found, the Task will be the most recently-interacted-with instance of that
+     * Task. Then runs the given callback on that List.
      * <p>
      * Used in various task-switching or splitscreen operations when we need to check if there is a
      * currently running Task of a certain type and use the most recent one.
      */
-    public void findLastActiveTaskAndRunCallback(
-            @Nullable ComponentKey componentKey, Consumer<Task> callback) {
+    public void findLastActiveTasksAndRunCallback(
+            @Nullable List<ComponentKey> componentKeys, Consumer<List<Task>> callback) {
         mRecentTasksModel.getTasks(taskGroups -> {
-            if (componentKey == null) {
-                callback.accept(null);
+            if (componentKeys == null || componentKeys.isEmpty()) {
+                callback.accept(Collections.emptyList());
                 return;
             }
-            Task lastActiveTask = null;
-            // Loop through tasks in reverse, since they are ordered with most-recent tasks last.
-            for (int i = taskGroups.size() - 1; i >= 0; i--) {
-                GroupTask groupTask = taskGroups.get(i);
-                Task task1 = groupTask.task1;
-                if (isInstanceOfComponent(task1, componentKey)) {
-                    lastActiveTask = task1;
-                    break;
+
+            List<Task> lastActiveTasks = new ArrayList<>();
+            // For each key we are looking for, add to lastActiveTasks with the corresponding Task
+            // (or null if not found).
+            for (ComponentKey key : componentKeys) {
+                Task lastActiveTask = null;
+                // Loop through tasks in reverse, since they are ordered with most-recent tasks last
+                for (int i = taskGroups.size() - 1; i >= 0; i--) {
+                    GroupTask groupTask = taskGroups.get(i);
+                    Task task1 = groupTask.task1;
+                    // Don't add duplicate Tasks
+                    if (isInstanceOfComponent(task1, key) && !lastActiveTasks.contains(task1)) {
+                        lastActiveTask = task1;
+                        break;
+                    }
+                    Task task2 = groupTask.task2;
+                    if (isInstanceOfComponent(task2, key) && !lastActiveTasks.contains(task2)) {
+                        lastActiveTask = task2;
+                        break;
+                    }
                 }
-                Task task2 = groupTask.task2;
-                if (isInstanceOfComponent(task2, componentKey)) {
-                    lastActiveTask = task2;
-                    break;
-                }
+
+                lastActiveTasks.add(lastActiveTask);
             }
 
-            callback.accept(lastActiveTask);
+            callback.accept(lastActiveTasks);
         });
     }
 
@@ -239,7 +234,7 @@
      */
     public boolean isInstanceOfComponent(@Nullable Task task, @NonNull ComponentKey componentKey) {
         // Exclude the task that is already staged
-        if (task == null || task.key.id == mInitialTaskId) {
+        if (task == null || task.key.id == mSplitSelectDataHolder.getInitialTaskId()) {
             return false;
         }
 
@@ -248,20 +243,48 @@
     }
 
     /**
-     * To be called when the actual tasks ({@link #mInitialTaskId}, {@link #mSecondTaskId}) are
-     * to be launched. Call after launcher side animations are complete.
+     * Listener will only get callbacks going forward from the point of registration. No
+     * methods will be fired upon registering.
      */
-    public void launchSplitTasks(Consumer<Boolean> callback) {
+    public void registerSplitListener(@NonNull SplitSelectionListener listener) {
+        if (mSplitSelectionListeners.contains(listener)) {
+            return;
+        }
+        mSplitSelectionListeners.add(listener);
+    }
+
+    public void unregisterSplitListener(@NonNull SplitSelectionListener listener) {
+        mSplitSelectionListeners.remove(listener);
+    }
+
+    private void dispatchOnSplitSelectionExit() {
+        for (SplitSelectionListener listener : mSplitSelectionListeners) {
+            listener.onSplitSelectionExit(false);
+        }
+    }
+
+    /**
+     * To be called when the both split tasks are ready to be launched. Call after launcher side
+     * animations are complete.
+     */
+    public void launchSplitTasks(@Nullable Consumer<Boolean> callback) {
         Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
                 LogUtils.getShellShareableInstanceId();
-        launchTasks(mInitialTaskId, mInitialTaskIntent, mSecondTaskId, mSecondTaskIntent,
-                mInitialStagePosition, callback, false /* freezeTaskList */, DEFAULT_SPLIT_RATIO,
+        launchTasks(callback, false /* freezeTaskList */, DEFAULT_SPLIT_RATIO,
                 instanceIds.first);
 
         mStatsLogManager.logger()
-                .withItemInfo(mItemInfo)
+                .withItemInfo(mSplitSelectDataHolder.getItemInfo())
                 .withInstanceId(instanceIds.second)
-                .log(mSplitEvent);
+                .log(mSplitSelectDataHolder.getSplitEvent());
+    }
+
+    /**
+     * A version of {@link #launchTasks(Consumer, boolean, float, InstanceId)} with no success
+     * callback.
+     */
+    public void launchSplitTasks() {
+        launchSplitTasks(null);
     }
 
     /**
@@ -269,11 +292,7 @@
      * @param task The second task that will be launched.
      */
     public void setSecondTask(Task task) {
-        mSecondTaskId = task.key.id;
-
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            mSplitSelectDataHolder.setSecondTask(task.key.id);
-        }
+        mSplitSelectDataHolder.setSecondTask(task.key.id);
     }
 
     /**
@@ -282,108 +301,29 @@
      * @param user The user of that intent.
      */
     public void setSecondTask(Intent intent, UserHandle user) {
-        mSecondTaskIntent = intent;
-        mSecondUser = user;
-
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            mSplitSelectDataHolder.setSecondTask(intent, user);
-        }
+        mSplitSelectDataHolder.setSecondTask(intent, user);
     }
 
     /**
      * To be called as soon as user selects the second app (even if animations aren't complete)
-     * Sets {@link #mSecondUser} from that of the pendingIntent
      * @param pendingIntent The second PendingIntent that will be launched.
      */
     public void setSecondTask(PendingIntent pendingIntent) {
-        mSecondPendingIntent = pendingIntent;
-        mSecondUser = pendingIntent.getCreatorUserHandle();
-
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            mSplitSelectDataHolder.setSecondTask(pendingIntent);
-        }
+        mSplitSelectDataHolder.setSecondTask(pendingIntent);
     }
 
     /**
      * To be called when we want to launch split pairs from Overview. Split can be initiated from
      * either Overview or home, or all apps. Either both taskIds are set, or a pending intent + a
      * fill in intent with a taskId2 are set.
-     * @param intent1 is null when split is initiated from Overview
-     * @param stagePosition representing location of task1
      * @param shellInstanceId loggingId to be used by shell, will be non-null for actions that
      *                   create a split instance, null for cases that bring existing instaces to the
      *                   foreground (quickswitch, launching previous pairs from overview)
      */
-    public void launchTasks(int taskId1, @Nullable Intent intent1, int taskId2,
-            @Nullable Intent intent2, @StagePosition int stagePosition,
-            Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio,
-            @Nullable InstanceId shellInstanceId) {
+    public void launchTasks(@Nullable Consumer<Boolean> callback, boolean freezeTaskList,
+            float splitRatio, @Nullable InstanceId shellInstanceId) {
         TestLogging.recordEvent(
                 TestProtocol.SEQUENCE_MAIN, "launchSplitTasks");
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            launchTasksRefactored(callback, freezeTaskList, splitRatio, shellInstanceId);
-            return;
-        }
-
-        final ActivityOptions options1 = ActivityOptions.makeBasic();
-        if (freezeTaskList) {
-            options1.setFreezeRecentTasksReordering();
-        }
-        boolean hasSecondaryPendingIntent = mSecondPendingIntent != null;
-        if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
-            final RemoteTransition remoteTransition = getShellRemoteTransition(taskId1, taskId2,
-                    callback);
-            if (intent1 == null && (intent2 == null && !hasSecondaryPendingIntent)) {
-                mSystemUiProxy.startTasks(taskId1, options1.toBundle(), taskId2,
-                        null /* options2 */, stagePosition, splitRatio, remoteTransition,
-                        shellInstanceId);
-            } else if (intent2 == null && !hasSecondaryPendingIntent) {
-                launchIntentOrShortcut(intent1, mInitialUser, options1, taskId2, stagePosition,
-                        splitRatio, remoteTransition, shellInstanceId);
-            } else if (intent1 == null) {
-                launchIntentOrShortcut(intent2, mSecondUser, options1, taskId1,
-                        getOppositeStagePosition(stagePosition), splitRatio, remoteTransition,
-                        shellInstanceId);
-            } else {
-                mSystemUiProxy.startIntents(getPendingIntent(intent1, mInitialUser),
-                        mInitialUser.getIdentifier(), getShortcutInfo(intent1, mInitialUser),
-                        options1.toBundle(), hasSecondaryPendingIntent
-                                ? mSecondPendingIntent
-                                : getPendingIntent(intent2, mSecondUser),
-                        mSecondUser.getIdentifier(), getShortcutInfo(intent2, mSecondUser),
-                        null /* options2 */, stagePosition, splitRatio, remoteTransition,
-                        shellInstanceId);
-            }
-        } else {
-            final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(taskId1, taskId2,
-                    callback);
-
-            if (intent1 == null && (intent2 == null && !hasSecondaryPendingIntent)) {
-                mSystemUiProxy.startTasksWithLegacyTransition(taskId1, options1.toBundle(),
-                        taskId2, null /* options2 */, stagePosition, splitRatio, adapter,
-                        shellInstanceId);
-            } else if (intent2 == null && !hasSecondaryPendingIntent) {
-                launchIntentOrShortcutLegacy(intent1, mInitialUser, options1, taskId2,
-                        stagePosition, splitRatio, adapter, shellInstanceId);
-            } else if (intent1 == null) {
-                launchIntentOrShortcutLegacy(intent2, mSecondUser, options1, taskId1,
-                        getOppositeStagePosition(stagePosition), splitRatio, adapter,
-                        shellInstanceId);
-            } else {
-                mSystemUiProxy.startIntentsWithLegacyTransition(
-                        getPendingIntent(intent1, mInitialUser), mInitialUser.getIdentifier(),
-                        getShortcutInfo(intent1, mInitialUser), options1.toBundle(),
-                        hasSecondaryPendingIntent
-                                ? mSecondPendingIntent
-                                : getPendingIntent(intent2, mSecondUser),
-                        mSecondUser.getIdentifier(), getShortcutInfo(intent2, mSecondUser),
-                        null /* options2 */, stagePosition, splitRatio, adapter, shellInstanceId);
-            }
-        }
-    }
-
-    private void launchTasksRefactored(Consumer<Boolean> callback, boolean freezeTaskList,
-            float splitRatio, @Nullable InstanceId shellInstanceId) {
         final ActivityOptions options1 = ActivityOptions.makeBasic();
         if (freezeTaskList) {
             options1.setFreezeRecentTasksReordering();
@@ -404,7 +344,7 @@
 
         if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
             final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId,
-                    secondTaskId, callback);
+                    secondTaskId, callback, "LaunchSplitPair");
             switch (launchData.getSplitLaunchType()) {
                 case SPLIT_TASK_TASK ->
                         mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
@@ -477,9 +417,8 @@
 
     /**
      * Used to launch split screen from a split pair that already exists (usually accessible through
-     * Overview). This is different than
-     * {@link #launchTasks(int, Intent, int, Intent, int, Consumer, boolean, float, InstanceId)} in
-     * that this only launches split screen that are existing tasks. This doesn't determine which
+     * Overview). This is different than {@link #launchTasks(Consumer, boolean, float, InstanceId)}
+     * in that this only launches split screen that are existing tasks. This doesn't determine which
      * API should be used (i.e. launching split with existing tasks vs intents vs shortcuts, etc).
      *
      * <p/>
@@ -497,7 +436,7 @@
 
         if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
             final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId,
-                    secondTaskId, callback);
+                    secondTaskId, callback, "LaunchExistingPair");
             mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
                     null /* options2 */, stagePosition, splitRatio,
                     remoteTransition, null /*shellInstanceId*/);
@@ -516,11 +455,6 @@
      * split and fullscreen tasks)
      */
     public void launchInitialAppFullscreen(Consumer<Boolean> callback) {
-        if (!FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            launchSplitTasks(callback);
-            return;
-        }
-
         final ActivityOptions options1 = ActivityOptions.makeBasic();
         SplitSelectDataHolder.SplitLaunchData launchData =
                 mSplitSelectDataHolder.getFullscreenLaunchData();
@@ -536,7 +470,7 @@
                 new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback);
         final RemoteTransition remoteTransition = new RemoteTransition(animationRunner,
                 ActivityThread.currentActivityThread().getApplicationThread(),
-                "LaunchSplitPair");
+                "LaunchAppFullscreen");
         InstanceId instanceId = LogUtils.getShellShareableInstanceId().first;
         if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
             switch (launchData.getSplitLaunchType()) {
@@ -571,111 +505,38 @@
         }
     }
 
+    public void initSplitFromDesktopController(Launcher launcher) {
+        mSplitFromDesktopController = new SplitFromDesktopController(launcher);
+    }
+
     private RemoteTransition getShellRemoteTransition(int firstTaskId, int secondTaskId,
-            Consumer<Boolean> callback) {
+            @Nullable Consumer<Boolean> callback, String transitionName) {
         final RemoteSplitLaunchTransitionRunner animationRunner =
                 new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback);
         return new RemoteTransition(animationRunner,
-                ActivityThread.currentActivityThread().getApplicationThread(), "LaunchSplitPair");
+                ActivityThread.currentActivityThread().getApplicationThread(), transitionName);
     }
 
     private RemoteAnimationAdapter getLegacyRemoteAdapter(int firstTaskId, int secondTaskId,
-            Consumer<Boolean> callback) {
+            @Nullable Consumer<Boolean> callback) {
         final RemoteSplitLaunchAnimationRunner animationRunner =
                 new RemoteSplitLaunchAnimationRunner(firstTaskId, secondTaskId, callback);
         return new RemoteAnimationAdapter(animationRunner, 300, 150,
                 ActivityThread.currentActivityThread().getApplicationThread());
     }
 
-    private void launchIntentOrShortcut(Intent intent, UserHandle user, ActivityOptions options1,
-            int taskId, @StagePosition int stagePosition, float splitRatio,
-            RemoteTransition remoteTransition, @Nullable InstanceId shellInstanceId) {
-        final ShortcutInfo shortcutInfo = getShortcutInfo(intent, user);
-        if (shortcutInfo != null) {
-            mSystemUiProxy.startShortcutAndTask(shortcutInfo,
-                    options1.toBundle(), taskId, null /* options2 */, stagePosition,
-                    splitRatio, remoteTransition, shellInstanceId);
-        } else {
-            mSystemUiProxy.startIntentAndTask(getPendingIntent(intent, user), user.getIdentifier(),
-                    options1.toBundle(), taskId, null /* options2 */, stagePosition, splitRatio,
-                    remoteTransition, shellInstanceId);
-        }
-    }
-
-    private void launchIntentOrShortcutLegacy(Intent intent, UserHandle user,
-            ActivityOptions options1, int taskId, @StagePosition int stagePosition,
-            float splitRatio, RemoteAnimationAdapter adapter,
-            @Nullable InstanceId shellInstanceId) {
-        final ShortcutInfo shortcutInfo = getShortcutInfo(intent, user);
-        if (shortcutInfo != null) {
-            mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(shortcutInfo,
-                    options1.toBundle(), taskId, null /* options2 */, stagePosition,
-                    splitRatio, adapter, shellInstanceId);
-        } else {
-            mSystemUiProxy.startIntentAndTaskWithLegacyTransition(
-                    getPendingIntent(intent, user), user.getIdentifier(), options1.toBundle(),
-                    taskId, null /* options2 */, stagePosition, splitRatio, adapter,
-                    shellInstanceId);
-        }
-    }
-
-    /**
-     * We treat launching by intents as grouped in two ways,
-     * If {@param intent} represents the first app, we always convert the intent to pending intent
-     * It it represents second app, either the second intent OR mSecondPendingIntent will be used
-     *    convert second intent to a pendingIntent OR return mSecondPendingIntent as is
-     */
-    private PendingIntent getPendingIntent(Intent intent, UserHandle user) {
-        boolean isParamFirstIntent = intent != null && intent == mInitialTaskIntent;
-        if (!isParamFirstIntent && mSecondPendingIntent != null) {
-            // Because mSecondPendingIntent and mSecondTaskIntent can't both be set, we know we need
-            // to be using mSecondPendingIntent
-            return mSecondPendingIntent;
-        }
-
-        // intent param must either be mInitialTaskIntent or mSecondTaskIntent, convert either to
-        // a new PendingIntent
-        return intent == null ? null : (user != null
-                ? PendingIntent.getActivityAsUser(mContext, 0, intent,
-                FLAG_MUTABLE | FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT, null /* options */, user)
-                : PendingIntent.getActivity(mContext, 0, intent,
-                        FLAG_MUTABLE | FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT));
-    }
-
     public @StagePosition int getActiveSplitStagePosition() {
-        return mInitialStagePosition;
+        return mSplitSelectDataHolder.getInitialStagePosition();
     }
 
     public StatsLogManager.EventEnum getSplitEvent() {
-        return mSplitEvent;
+        return mSplitSelectDataHolder.getSplitEvent();
     }
 
     public void setRecentsAnimationRunning(boolean running) {
         mRecentsAnimationRunning = running;
     }
 
-    @Nullable
-    private ShortcutInfo getShortcutInfo(Intent intent, UserHandle user) {
-        if (intent == null || intent.getPackage() == null) {
-            return null;
-        }
-
-        final String shortcutId = intent.getStringExtra(ShortcutKey.EXTRA_SHORTCUT_ID);
-        if (shortcutId == null) {
-            return null;
-        }
-
-        try {
-            final Context context = mContext.createPackageContextAsUser(
-                    intent.getPackage(), 0 /* flags */, user);
-            return new ShortcutInfo.Builder(context, shortcutId).build();
-        } catch (PackageManager.NameNotFoundException e) {
-            Log.w(TAG, "Failed to create a ShortcutInfo for " + intent.getPackage());
-        }
-
-        return null;
-    }
-
     public boolean isAnimateCurrentTaskDismissal() {
         return mAnimateCurrentTaskDismissal;
     }
@@ -706,7 +567,7 @@
         private final Consumer<Boolean> mSuccessCallback;
 
         RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId,
-                Consumer<Boolean> callback) {
+                @Nullable Consumer<Boolean> callback) {
             mInitialTaskId = initialTaskId;
             mSecondTaskId = secondTaskId;
             mSuccessCallback = callback;
@@ -716,6 +577,7 @@
         public void startAnimation(IBinder transition, TransitionInfo info,
                 SurfaceControl.Transaction t,
                 IRemoteTransitionFinishedCallback finishedCallback) {
+            testLogD(LAUNCH_SPLIT_PAIR, "Received split startAnimation");
             final Runnable finishAdapter = () ->  {
                 try {
                     finishedCallback.onTransitionFinished(null /* wct */, null /* sct */);
@@ -731,9 +593,8 @@
                             if (mSuccessCallback != null) {
                                 mSuccessCallback.accept(true);
                             }
+                            resetState();
                         });
-                // After successful launch, call resetState
-                resetState();
             });
         }
 
@@ -754,7 +615,7 @@
         private final Consumer<Boolean> mSuccessCallback;
 
         RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId,
-                Consumer<Boolean> successCallback) {
+                @Nullable Consumer<Boolean> successCallback) {
             mInitialTaskId = initialTaskId;
             mSecondTaskId = secondTaskId;
             mSuccessCallback = successCallback;
@@ -790,26 +651,20 @@
     }
 
     /**
-     * To be called if split select was cancelled
+     * To be called whenever we exit split selection state. If
+     * {@link FeatureFlags#ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE} is set, this should be the
+     * central way split is getting reset, which should then go through the callbacks to reset
+     * other state.
      */
     public void resetState() {
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            mSplitSelectDataHolder.resetState();
-        }
-        mInitialTaskId = INVALID_TASK_ID;
-        mInitialTaskIntent = null;
-        mSecondTaskId = INVALID_TASK_ID;
-        mSecondTaskIntent = null;
-        mInitialUser = null;
-        mSecondUser = null;
-        mInitialStagePosition = SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
+        mSplitSelectDataHolder.resetState();
+        dispatchOnSplitSelectionExit();
         mRecentsAnimationRunning = false;
         mLaunchingTaskView = null;
-        mItemInfo = null;
-        mSplitEvent = null;
         mAnimateCurrentTaskDismissal = false;
         mDismissingFromSplitPair = false;
-        mSecondPendingIntent = null;
+        mFirstFloatingTaskView = null;
+        mSplitInstructionsView = null;
     }
 
     /**
@@ -817,11 +672,7 @@
      *         chosen
      */
     public boolean isSplitSelectActive() {
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            return mSplitSelectDataHolder.isSplitSelectActive();
-        } else {
-            return isInitialTaskIntentSet() && !isSecondTaskIntentSet();
-        }
+        return mSplitSelectDataHolder.isSplitSelectActive();
     }
 
     /**
@@ -829,46 +680,35 @@
      *          be launched
      */
     public boolean isBothSplitAppsConfirmed() {
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            return mSplitSelectDataHolder.isBothSplitAppsConfirmed();
-        } else {
-            return isInitialTaskIntentSet() && isSecondTaskIntentSet();
-        }
-    }
-
-    private boolean isInitialTaskIntentSet() {
-        return (mInitialTaskId != INVALID_TASK_ID || mInitialTaskIntent != null);
+        return mSplitSelectDataHolder.isBothSplitAppsConfirmed();
     }
 
     public int getInitialTaskId() {
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            return mSplitSelectDataHolder.getInitialTaskId();
-        } else {
-            return mInitialTaskId;
-        }
+        return mSplitSelectDataHolder.getInitialTaskId();
     }
 
     public int getSecondTaskId() {
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            return mSplitSelectDataHolder.getSecondTaskId();
-        } else {
-            return mSecondTaskId;
-        }
-    }
-
-    private boolean isSecondTaskIntentSet() {
-        return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null
-                || mSecondPendingIntent != null);
+        return mSplitSelectDataHolder.getSecondTaskId();
     }
 
     public void setFirstFloatingTaskView(FloatingTaskView floatingTaskView) {
         mFirstFloatingTaskView = floatingTaskView;
     }
 
+    public void setSplitInstructionsView(SplitInstructionsView splitInstructionsView) {
+        mSplitInstructionsView = splitInstructionsView;
+    }
+
+    @Nullable
     public FloatingTaskView getFirstFloatingTaskView() {
         return mFirstFloatingTaskView;
     }
 
+    @Nullable
+    public SplitInstructionsView getSplitInstructionsView() {
+        return mSplitInstructionsView;
+    }
+
     public AppPairsController getAppPairsController() {
         return mAppPairsController;
     }
@@ -878,4 +718,128 @@
             mSplitSelectDataHolder.dump(prefix, writer);
         }
     }
+
+    public class SplitFromDesktopController {
+        private static final String TAG = "SplitFromDesktopController";
+
+        private final Launcher mLauncher;
+        private final OverviewComponentObserver mOverviewComponentObserver;
+        private final int mSplitPlaceholderSize;
+        private final int mSplitPlaceholderInset;
+        private ActivityManager.RunningTaskInfo mTaskInfo;
+        private ISplitSelectListener mSplitSelectListener;
+        private Drawable mAppIcon;
+
+        public SplitFromDesktopController(Launcher launcher) {
+            mLauncher = launcher;
+            RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(
+                    launcher.getApplicationContext());
+            mOverviewComponentObserver =
+                    new OverviewComponentObserver(launcher.getApplicationContext(), deviceState);
+            mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize(
+                    R.dimen.split_placeholder_size);
+            mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize(
+                    R.dimen.split_placeholder_inset);
+            mSplitSelectListener = new ISplitSelectListener.Stub() {
+                @Override
+                public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
+                        int splitPosition, Rect taskBounds) {
+                    if (!ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE.get()) return false;
+                    MAIN_EXECUTOR.execute(() -> enterSplitSelect(taskInfo, splitPosition,
+                            taskBounds));
+                    return true;
+                }
+            };
+            SystemUiProxy.INSTANCE.get(mLauncher).registerSplitSelectListener(mSplitSelectListener);
+        }
+
+        /**
+         * Enter split select from desktop mode.
+         * @param taskInfo the desktop task to move to split stage
+         * @param splitPosition the stage position used for this transition
+         * @param taskBounds the bounds of the task, used for {@link FloatingTaskView} animation
+         */
+        public void enterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
+                int splitPosition, Rect taskBounds) {
+            mTaskInfo = taskInfo;
+            String packageName = mTaskInfo.realActivity.getPackageName();
+            PackageManager pm = mLauncher.getApplicationContext().getPackageManager();
+            IconProvider provider = new IconProvider(mLauncher.getApplicationContext());
+            try {
+                mAppIcon = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity,
+                     PackageManager.ComponentInfoFlags.of(0)));
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.w(TAG, "Package not found: " + packageName, e);
+            }
+            RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks(
+                    SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()),
+                    false /* allowMinimizeSplitScreen */);
+
+            DesktopSplitRecentsAnimationListener listener =
+                    new DesktopSplitRecentsAnimationListener(splitPosition, taskBounds);
+
+            MAIN_EXECUTOR.execute(() -> {
+                callbacks.addListener(listener);
+                UI_HELPER_EXECUTOR.execute(
+                        // Transition from app to enter stage split in launcher with
+                        // recents animation.
+                        () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
+                                mOverviewComponentObserver.getOverviewIntent(),
+                                SystemClock.uptimeMillis(), callbacks, null, null));
+            });
+        }
+
+        private class DesktopSplitRecentsAnimationListener implements
+                RecentsAnimationCallbacks.RecentsAnimationListener {
+            private final Rect mTempRect = new Rect();
+            private final RectF mTaskBounds = new RectF();
+            private final int mSplitPosition;
+
+            DesktopSplitRecentsAnimationListener(int splitPosition, Rect taskBounds) {
+                mSplitPosition = splitPosition;
+                mTaskBounds.set(taskBounds);
+            }
+
+            @Override
+            public void onRecentsAnimationStart(RecentsAnimationController controller,
+                    RecentsAnimationTargets targets) {
+                StatsLogManager.LauncherEvent launcherDesktopSplitEvent =
+                        mSplitPosition == STAGE_POSITION_BOTTOM_OR_RIGHT ?
+                        LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM :
+                        LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP;
+                setInitialTaskSelect(mTaskInfo, mSplitPosition,
+                        null, launcherDesktopSplitEvent);
+
+                RecentsView recentsView = mLauncher.getOverviewPanel();
+                recentsView.getPagedOrientationHandler().getInitialSplitPlaceholderBounds(
+                        mSplitPlaceholderSize, mSplitPlaceholderInset,
+                        mLauncher.getDeviceProfile(), getActiveSplitStagePosition(), mTempRect);
+
+                PendingAnimation anim = new PendingAnimation(
+                        SplitAnimationTimings.TABLET_HOME_TO_SPLIT.getDuration());
+                final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView(
+                        mLauncher, mLauncher.getDragLayer(),
+                        null /* thumbnail */,
+                        mAppIcon, new RectF());
+                floatingTaskView.setAlpha(1);
+                floatingTaskView.addStagingAnimation(anim, mTaskBounds, mTempRect,
+                        false /* fadeWithThumbnail */, true /* isStagedTask */);
+                setFirstFloatingTaskView(floatingTaskView);
+
+                anim.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationStart(Animator animation) {
+                        controller.finish(true /* toRecents */, null /* onFinishComplete */,
+                                false /* sendUserLeaveHint */);
+                    }
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext())
+                                .onDesktopSplitSelectAnimComplete(mTaskInfo);
+                    }
+                });
+                anim.buildAnim().start();
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java b/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java
index f5b00cf..d1ec2b6 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java
@@ -16,7 +16,7 @@
 
 package com.android.quickstep.util;
 
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED;
+import static com.android.app.animation.Interpolators.EMPHASIZED;
 
 import android.view.animation.Interpolator;
 
diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
index 148a45a..056f9aa 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.util;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE;
 
@@ -169,6 +170,7 @@
             private void cleanUp() {
                 mLauncher.getDragLayer().removeView(firstFloatingTaskView);
                 mLauncher.getDragLayer().removeView(secondFloatingTaskView);
+                mController.getSplitAnimationController().removeSplitInstructionsView(mLauncher);
                 mController.resetState();
             }
         });
@@ -177,7 +179,8 @@
 
     private boolean shouldIgnoreSecondSplitLaunch() {
         return (!ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS.get()
-                && !ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get())
+                && !ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()
+                && !ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE.get())
                 || !mController.isSplitSelectActive();
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index 24d8326..f3fa86a 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -27,21 +27,15 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.app.ActivityManager;
-import android.content.Intent;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.SystemClock;
-import android.os.UserHandle;
-import android.view.View;
 
 import androidx.annotation.BinderThread;
 
-import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
-import com.android.quickstep.OverviewCommandHelper;
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationController;
@@ -143,11 +137,7 @@
                     .updateIconInBackground(
                             Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo,
                                     false /* isLocked */),
-                            (task) -> {
-                                if (task.thumbnail != null) {
-                                    floatingTaskView.setIcon(task.thumbnail.thumbnail);
-                                }
-                            });
+                            (task) -> floatingTaskView.setIcon(task.icon));
             floatingTaskView.setAlpha(1);
             floatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
                     false /* fadeWithThumbnail */, true /* isStagedTask */);
diff --git a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
index cd5edab..6b3199f 100644
--- a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
+++ b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
@@ -15,11 +15,11 @@
  */
 package com.android.quickstep.util;
 
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
-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.SKIP_DEPTH_CONTROLLER;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
@@ -177,8 +177,8 @@
 
         addDepthAnimationForState(launcher, NORMAL, duration);
 
-        mAnimators.play(launcher.getRootView().getSysUiScrim().createSysuiMultiplierAnim(0f, 1f)
-                .setDuration(duration));
+        mAnimators.play(launcher.getRootView().getSysUiScrim().getSysUIMultiplier()
+                .animateToValue(0f, 1f).setDuration(duration));
     }
 
     private void addAnimationForPage(CellLayout page, int totalRows, long duration) {
diff --git a/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java b/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java
index 441f88d..5fd86c0 100644
--- a/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java
+++ b/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java
@@ -166,5 +166,10 @@
             this.shadowRadius = radius;
             return this;
         }
+
+        @Override
+        public SurfaceProperties setShow() {
+            return this;
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 1112f4d..7cc2c46 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -26,7 +26,6 @@
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.os.SystemProperties;
 import android.util.Log;
 import android.view.Surface;
 import android.view.SurfaceControl;
@@ -153,14 +152,9 @@
             // Create a new overlay layer. We do not call detach on this instance, it's propagated
             // to other classes like PipTaskOrganizer / RecentsAnimationController to complete
             // the cleanup.
-            if (SystemProperties.getBoolean(
-                    "persist.wm.debug.enable_pip_app_icon_overlay", true)) {
-                mPipContentOverlay = new PipContentOverlay.PipAppIconOverlay(view.getContext(),
-                        mAppBounds, new IconProvider(context).getIcon(mActivityInfo),
-                        appIconSizePx);
-            }  else {
-                mPipContentOverlay = new PipContentOverlay.PipColorOverlay(view.getContext());
-            }
+            mPipContentOverlay = new PipContentOverlay.PipAppIconOverlay(view.getContext(),
+                    mAppBounds, new IconProvider(context).getIcon(mActivityInfo),
+                    appIconSizePx);
             final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
             mPipContentOverlay.attach(tx, mLeash);
         } else {
diff --git a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
index 9808b28..c82cdb7 100644
--- a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
+++ b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
@@ -24,11 +24,11 @@
 import android.view.WindowMetrics;
 
 import com.android.internal.policy.SystemBarUtils;
-import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.CachedDisplayInfo;
 import com.android.launcher3.util.window.WindowManagerProxy;
 
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -54,16 +54,15 @@
     }
 
     @Override
-    public ArrayMap<CachedDisplayInfo, WindowBounds[]> estimateInternalDisplayBounds(
+    public ArrayMap<CachedDisplayInfo, List<WindowBounds>> estimateInternalDisplayBounds(
             Context displayInfoContext) {
-        ArrayMap<CachedDisplayInfo, WindowBounds[]> result = new ArrayMap<>();
+        ArrayMap<CachedDisplayInfo, List<WindowBounds>> result = new ArrayMap<>();
         WindowManager windowManager = displayInfoContext.getSystemService(WindowManager.class);
         Set<WindowMetrics> possibleMaximumWindowMetrics =
                 windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY);
-        FileLog.d("b/283944974", "possibleMaximumWindowMetrics: " + possibleMaximumWindowMetrics);
         for (WindowMetrics windowMetrics : possibleMaximumWindowMetrics) {
             CachedDisplayInfo info = getDisplayInfo(windowMetrics, Surface.ROTATION_0);
-            WindowBounds[] bounds = estimateWindowBounds(displayInfoContext, info);
+            List<WindowBounds> bounds = estimateWindowBounds(displayInfoContext, info);
             result.put(info, bounds);
         }
         return result;
diff --git a/quickstep/src/com/android/quickstep/util/TISBindHelper.java b/quickstep/src/com/android/quickstep/util/TISBindHelper.java
index 7b122c6..ddc796f 100644
--- a/quickstep/src/com/android/quickstep/util/TISBindHelper.java
+++ b/quickstep/src/com/android/quickstep/util/TISBindHelper.java
@@ -23,6 +23,10 @@
 import android.os.IBinder;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.taskbar.TaskbarManager;
+import com.android.quickstep.OverviewCommandHelper;
 import com.android.quickstep.TouchInteractionService;
 import com.android.quickstep.TouchInteractionService.TISBinder;
 
@@ -50,6 +54,7 @@
     private short mConnectionAttempts;
     private boolean mTisServiceBound;
     private boolean mIsConnected;
+    @Nullable private TISBinder mBinder;
 
     public TISBindHelper(Context context, Consumer<TISBinder> connectionCallback) {
         mContext = context;
@@ -70,7 +75,8 @@
 
         Log.d(TAG, "TIS service connected");
         mIsConnected = true;
-        mConnectionCallback.accept((TISBinder) iBinder);
+        mBinder = (TISBinder) iBinder;
+        mConnectionCallback.accept(mBinder);
         // Flush the pending callbacks
         for (Runnable r : mPendingConnectedCallbacks) {
             r.run();
@@ -80,7 +86,11 @@
     }
 
     @Override
-    public void onServiceDisconnected(ComponentName componentName) { }
+    public void onServiceDisconnected(ComponentName componentName) {
+        Log.d(TAG, "TIS service disconnected");
+        mBinder = null;
+        mIsConnected = false;
+    }
 
     @Override
     public void onBindingDied(ComponentName name) {
@@ -88,6 +98,21 @@
         internalBindToTIS();
     }
 
+    @Nullable
+    public TISBinder getBinder() {
+        return mBinder;
+    }
+
+    @Nullable
+    public TaskbarManager getTaskbarManager() {
+        return mBinder == null ? null : mBinder.getTaskbarManager();
+    }
+
+    @Nullable
+    public OverviewCommandHelper getOverviewCommandHelper() {
+        return mBinder == null ? null : mBinder.getOverviewCommandHelper();
+    }
+
     /**
      * Runs the given {@param r} runnable when the service is connected.
      */
@@ -139,6 +164,7 @@
     public void onDestroy() {
         internalUnbindToTIS();
         resetServiceBindRetryState();
+        mBinder = null;
         mIsConnected = false;
         mPendingConnectedCallbacks.clear();
     }
diff --git a/quickstep/src/com/android/quickstep/util/TabletHomeToSplitTimings.java b/quickstep/src/com/android/quickstep/util/TabletHomeToSplitTimings.java
index bf8612a..8804049 100644
--- a/quickstep/src/com/android/quickstep/util/TabletHomeToSplitTimings.java
+++ b/quickstep/src/com/android/quickstep/util/TabletHomeToSplitTimings.java
@@ -16,7 +16,7 @@
 
 package com.android.quickstep.util;
 
-import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.LINEAR;
 
 import android.view.animation.Interpolator;
 
diff --git a/quickstep/src/com/android/quickstep/util/TabletOverviewToSplitTimings.java b/quickstep/src/com/android/quickstep/util/TabletOverviewToSplitTimings.java
index cbf46bf..5463d84 100644
--- a/quickstep/src/com/android/quickstep/util/TabletOverviewToSplitTimings.java
+++ b/quickstep/src/com/android/quickstep/util/TabletOverviewToSplitTimings.java
@@ -16,7 +16,7 @@
 
 package com.android.quickstep.util;
 
-import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.app.animation.Interpolators.DECELERATE_2;
 
 import android.view.animation.Interpolator;
 
@@ -36,8 +36,8 @@
     public int getGridSlideDuration() { return 500; }
 
     public int getDuration() { return TABLET_ENTER_DURATION; }
-    public Interpolator getStagedRectXInterpolator() { return DEACCEL_2; }
-    public Interpolator getStagedRectYInterpolator() { return DEACCEL_2; }
-    public Interpolator getStagedRectScaleXInterpolator() { return DEACCEL_2; }
-    public Interpolator getStagedRectScaleYInterpolator() { return DEACCEL_2; }
+    public Interpolator getStagedRectXInterpolator() { return DECELERATE_2; }
+    public Interpolator getStagedRectYInterpolator() { return DECELERATE_2; }
+    public Interpolator getStagedRectScaleXInterpolator() { return DECELERATE_2; }
+    public Interpolator getStagedRectScaleYInterpolator() { return DECELERATE_2; }
 }
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 303a528..48dadd1 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -112,8 +112,7 @@
         mContext = context;
         mSizeStrategy = sizeStrategy;
 
-        // TODO(b/187074722): Don't create this per-TaskViewSimulator
-        mOrientationState = TraceHelper.allowIpcs("",
+        mOrientationState = TraceHelper.allowIpcs("TaskViewSimulator.init",
                 () -> new RecentsOrientedState(context, sizeStrategy, i -> { }));
         mOrientationState.setGestureActive(true);
         mCurrentFullscreenParams = new FullscreenDrawParams(context);
diff --git a/quickstep/src/com/android/quickstep/util/TransformParams.java b/quickstep/src/com/android/quickstep/util/TransformParams.java
index 0f20e43..1cbded6 100644
--- a/quickstep/src/com/android/quickstep/util/TransformParams.java
+++ b/quickstep/src/com/android/quickstep/util/TransformParams.java
@@ -21,8 +21,8 @@
 import android.util.FloatProperty;
 import android.view.RemoteAnimationTarget;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.Interpolators;
 import com.android.quickstep.RemoteAnimationTargets;
 import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
 
@@ -153,7 +153,8 @@
                     // Fade out Assistant overlay.
                     if (activityType == ACTIVITY_TYPE_ASSISTANT && app.isNotInRecents) {
                         float progress = Utilities.boundToRange(getProgress(), 0, 1);
-                        builder.setAlpha(1 - Interpolators.DEACCEL_2_5.getInterpolation(progress));
+                        builder.setAlpha(1 - Interpolators.DECELERATE_QUINT
+                                .getInterpolation(progress));
                     } else {
                         builder.setAlpha(getTargetAlpha());
                     }
diff --git a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
index 34fa7f1..0a97793 100644
--- a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
+++ b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
@@ -32,11 +32,11 @@
 import android.util.FloatProperty;
 import android.view.View;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.states.StateAnimationConfig;
@@ -92,7 +92,8 @@
         }
 
         // Add sysui scrim animation.
-        mAnimators.play(launcher.getRootView().getSysUiScrim().createSysuiMultiplierAnim(0f, 1f));
+        mAnimators.play(launcher.getRootView().getSysUiScrim()
+                .getSysUIMultiplier().animateToValue(0f, 1f));
 
         mAnimators.setDuration(DURATION_MS);
         mAnimators.setInterpolator(Interpolators.DECELERATED_EASE);
diff --git a/quickstep/src/com/android/quickstep/views/AllAppsEduView.java b/quickstep/src/com/android/quickstep/views/AllAppsEduView.java
index 716d389..fdc8f1f 100644
--- a/quickstep/src/com/android/quickstep/views/AllAppsEduView.java
+++ b/quickstep/src/com/android/quickstep/views/AllAppsEduView.java
@@ -15,12 +15,12 @@
  */
 package com.android.quickstep.views;
 
+import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.OVERSHOOT_1_7;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
-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 android.animation.Animator;
diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
index 6813857..19ac1f8 100644
--- a/quickstep/src/com/android/quickstep/views/ClearAllButton.java
+++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
@@ -16,6 +16,8 @@
 
 package com.android.quickstep.views;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_GRID_ONLY_OVERVIEW;
+
 import android.content.Context;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
@@ -248,8 +250,15 @@
      */
     private float getOriginalTranslationY() {
         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
-        return deviceProfile.isTablet
-                ? deviceProfile.overviewRowSpacing
-                : deviceProfile.overviewTaskThumbnailTopMarginPx / 2.0f;
+        if (deviceProfile.isTablet) {
+            if (ENABLE_GRID_ONLY_OVERVIEW.get()) {
+                return (getRecentsView().getLastComputedTaskSize().height()
+                        + deviceProfile.overviewTaskThumbnailTopMarginPx) / 2.0f
+                        + deviceProfile.overviewRowSpacing;
+            } else {
+                return deviceProfile.overviewRowSpacing;
+            }
+        }
+        return deviceProfile.overviewTaskThumbnailTopMarginPx / 2.0f;
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/DesktopAppSelectView.java b/quickstep/src/com/android/quickstep/views/DesktopAppSelectView.java
new file mode 100644
index 0000000..a5be142
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/DesktopAppSelectView.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.app.animation.Interpolators.EMPHASIZED_DECELERATE;
+import static com.android.app.animation.Interpolators.LINEAR;
+
+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.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+
+/**
+ * Floating view show on launcher home screen that notifies the user that an app will be launched to
+ * the desktop.
+ */
+public class DesktopAppSelectView extends LinearLayout {
+
+    private static final int SHOW_INITIAL_HEIGHT_DP = 7;
+    private static final int SHOW_CONTAINER_SCALE_DURATION = 333;
+    private static final int SHOW_CONTAINER_ALPHA_DURATION = 83;
+    private static final int SHOW_CONTENT_ALPHA_DELAY = 67;
+    private static final int SHOW_CONTENT_ALPHA_DURATION = 83;
+    private static final int HIDE_DURATION = 83;
+
+    private final Launcher mLauncher;
+
+    private View mText;
+    private View mCloseButton;
+    @Nullable
+    private Runnable mOnCloseCallback;
+    private AnimatorSet mShowAnimation;
+    private Animator mHideAnimation;
+
+    public DesktopAppSelectView(Context context) {
+        this(context, null);
+    }
+
+    public DesktopAppSelectView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public DesktopAppSelectView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public DesktopAppSelectView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mLauncher = Launcher.getLauncher(context);
+    }
+
+    /**
+     * Show the popup on launcher home screen
+     *
+     * @param onCloseCallback optional callback that is called when user clicks the close button
+     * @return the created view
+     */
+    public static DesktopAppSelectView show(Launcher launcher, @Nullable Runnable onCloseCallback) {
+        DesktopAppSelectView view = (DesktopAppSelectView) launcher.getLayoutInflater().inflate(
+                R.layout.floating_desktop_app_select, launcher.getDragLayer(), false);
+        view.setOnCloseClickCallback(onCloseCallback);
+        view.show();
+        return view;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mText = findViewById(R.id.desktop_app_select_text);
+        mCloseButton = findViewById(R.id.close_button);
+        mCloseButton.setOnClickListener(v -> {
+            if (mHideAnimation == null) {
+                hide();
+                if (mOnCloseCallback != null) {
+                    mOnCloseCallback.run();
+                }
+            }
+        });
+    }
+
+    private void show() {
+        mLauncher.getDragLayer().addView(this);
+
+        // Set up initial values
+        getBackground().setAlpha(0);
+        mText.setAlpha(0);
+        mCloseButton.setAlpha(0);
+        int initialHeightPx = Utilities.dpToPx(SHOW_INITIAL_HEIGHT_DP);
+        int finalHeight = getResources().getDimensionPixelSize(
+                R.dimen.desktop_mode_floating_app_select_height);
+        float initialScale = initialHeightPx / (float) finalHeight;
+        setScaleY(initialScale);
+        setPivotY(0);
+
+        // Animate the container
+        ValueAnimator containerBackground = ValueAnimator.ofInt(0, 255);
+        containerBackground.addUpdateListener(
+                animation -> getBackground().setAlpha((Integer) animation.getAnimatedValue()));
+        containerBackground.setDuration(SHOW_CONTAINER_ALPHA_DURATION);
+        containerBackground.setInterpolator(LINEAR);
+
+        ObjectAnimator containerSize = ObjectAnimator.ofFloat(this, SCALE_Y, 1f);
+        containerSize.setDuration(SHOW_CONTAINER_SCALE_DURATION);
+        containerSize.setInterpolator(EMPHASIZED_DECELERATE);
+
+        // Animate the contents
+        ObjectAnimator textAlpha = ObjectAnimator.ofFloat(mText, ALPHA, 1);
+        ObjectAnimator buttonAlpha = ObjectAnimator.ofFloat(mCloseButton, ALPHA, 1);
+        AnimatorSet contentAlpha = new AnimatorSet();
+        contentAlpha.playTogether(textAlpha, buttonAlpha);
+        contentAlpha.setStartDelay(SHOW_CONTENT_ALPHA_DELAY);
+        contentAlpha.setDuration(SHOW_CONTENT_ALPHA_DURATION);
+        contentAlpha.setInterpolator(LINEAR);
+
+        // Start the animation
+        mShowAnimation = new AnimatorSet();
+        mShowAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                mShowAnimation = null;
+            }
+        });
+        mShowAnimation.playTogether(containerBackground, containerSize, contentAlpha);
+        mShowAnimation.start();
+    }
+
+    /**
+     * Hide the floating view
+     */
+    public void hide() {
+        if (mShowAnimation != null) {
+            mShowAnimation.cancel();
+        }
+        mHideAnimation = ObjectAnimator.ofFloat(this, ALPHA, 0);
+        mHideAnimation.setDuration(HIDE_DURATION).setInterpolator(LINEAR);
+        mHideAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                mLauncher.getDragLayer().removeView(DesktopAppSelectView.this);
+                mHideAnimation = null;
+            }
+        });
+        mHideAnimation.start();
+    }
+
+    /**
+     * Add a callback that is called when close button is clicked
+     */
+    public void setOnCloseClickCallback(@Nullable Runnable callback) {
+        mOnCloseCallback = callback;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index 1cfaf14..dfd4390 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
 
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
@@ -43,6 +44,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.util.RunnableList;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.SystemUiProxy;
@@ -51,6 +53,7 @@
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.QuickStepContract;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -79,7 +82,7 @@
 
     private static final String TAG = DesktopTaskView.class.getSimpleName();
 
-    private static final boolean DEBUG = true;
+    private static final boolean DEBUG = false;
 
     @NonNull
     private List<Task> mTasks = new ArrayList<>();
@@ -91,6 +94,8 @@
 
     private final ArrayList<CancellableTask<?>> mPendingThumbnailRequests = new ArrayList<>();
 
+    private final TaskView.FullscreenDrawParams mSnapshotDrawParams;
+
     private View mBackgroundView;
 
     public DesktopTaskView(Context context) {
@@ -103,6 +108,18 @@
 
     public DesktopTaskView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
+
+        mSnapshotDrawParams = new FullscreenDrawParams(context) {
+            @Override
+            public float computeTaskCornerRadius(Context context) {
+                return QuickStepContract.getWindowCornerRadius(context);
+            }
+
+            @Override
+            public float computeWindowCornerRadius(Context context) {
+                return QuickStepContract.getWindowCornerRadius(context);
+            }
+        };
     }
 
     @Override
@@ -213,7 +230,22 @@
 
     private TaskIdAttributeContainer createAttributeContainer(Task task,
             TaskThumbnailView thumbnailView) {
-        return new TaskIdAttributeContainer(task, thumbnailView, null, STAGE_POSITION_UNDEFINED);
+        return new TaskIdAttributeContainer(task, thumbnailView, createIconView(task),
+                STAGE_POSITION_UNDEFINED);
+    }
+
+    private IconView createIconView(Task task) {
+        IconView iconView = new IconView(mContext);
+        PackageManager pm = mContext.getApplicationContext().getPackageManager();
+        try {
+            IconProvider provider = new IconProvider(mContext);
+            Drawable appIcon = provider.getIcon(pm.getActivityInfo(task.topActivity,
+                    PackageManager.ComponentInfoFlags.of(0)));
+            iconView.setDrawable(appIcon);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Package not found: " + task.topActivity.getPackageName(), e);
+        }
+        return iconView;
     }
 
     @Nullable
@@ -465,14 +497,20 @@
         for (int i = 0; i < mSnapshotViewMap.size(); i++) {
             TaskThumbnailView thumbnailView = mSnapshotViewMap.valueAt(i);
             thumbnailView.getTaskOverlay().setFullscreenProgress(progress);
-            updateSnapshotRadius();
         }
+        updateSnapshotRadius();
     }
 
     @Override
     protected void updateSnapshotRadius() {
+        super.updateSnapshotRadius();
         for (int i = 0; i < mSnapshotViewMap.size(); i++) {
-            mSnapshotViewMap.valueAt(i).setFullscreenParams(mCurrentFullscreenParams);
+            if (i == 0) {
+                // All snapshots share the same params. Only update it with the first snapshot.
+                updateFullscreenParams(mSnapshotDrawParams,
+                        mSnapshotView.getPreviewPositionHelper());
+            }
+            mSnapshotViewMap.valueAt(i).setFullscreenParams(mSnapshotDrawParams);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
index 7bbe36a..e5a0e10 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -145,23 +145,29 @@
         mAppUsageLimitTimeMs = mAppRemainingTimeMs = -1;
         mTask = task;
         THREAD_POOL_EXECUTOR.execute(() -> {
-            final AppUsageLimit usageLimit = mLauncherApps.getAppUsageLimit(
-                    mTask.getTopComponent().getPackageName(),
-                    UserHandle.of(mTask.key.userId));
+                    AppUsageLimit usageLimit = null;
+                    try {
+                        usageLimit = mLauncherApps.getAppUsageLimit(
+                                mTask.getTopComponent().getPackageName(),
+                                UserHandle.of(mTask.key.userId));
+                    } catch (Exception e) {
+                        Log.e(TAG, "Error initializing digital well being toast", e);
+                    }
+                    final long appUsageLimitTimeMs =
+                            usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
+                    final long appRemainingTimeMs =
+                            usageLimit != null ? usageLimit.getUsageRemaining() : -1;
 
-            final long appUsageLimitTimeMs =
-                    usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
-            final long appRemainingTimeMs =
-                    usageLimit != null ? usageLimit.getUsageRemaining() : -1;
+                    mTaskView.post(() -> {
+                        if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
+                            setNoLimit();
+                        } else {
+                            setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
+                        }
+                    });
 
-            mTaskView.post(() -> {
-                if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
-                    setNoLimit();
-                } else {
-                    setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
                 }
-            });
-        });
+        );
     }
 
     public void setSplitConfiguration(SplitBounds splitBounds) {
diff --git a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
index 75a8ea2..f250b8c 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
@@ -1,8 +1,8 @@
 package com.android.quickstep.views;
 
+import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.clampToProgress;
 import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.clampToProgress;
 
 import android.animation.ValueAnimator;
 import android.content.Context;
@@ -11,7 +11,6 @@
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
@@ -213,8 +212,8 @@
         mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated());
     }
 
-    public void setIcon(Bitmap icon) {
-        mSplitPlaceholderView.setIcon(new BitmapDrawable(icon), mSplitHolderSize);
+    public void setIcon(Drawable drawable) {
+        mSplitPlaceholderView.setIcon(drawable, mSplitHolderSize);
     }
 
     protected void initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp) {
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index c91b183..01f6ae8 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -223,11 +223,12 @@
         // Callbacks run from remote animation when recents animation not currently running
         InteractionJankMonitorWrapper.begin(this,
                 InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "Enter form GroupedTaskView");
-        launchTask(success -> {
+        launchTaskInternal(success -> {
             endCallback.executeAllAndDestroy();
             InteractionJankMonitorWrapper.end(
                     InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER);
-        }, false /* freezeTaskList */);
+        }, false /* freezeTaskList */, true /*launchingExistingTaskview*/);
+
 
         // Callbacks get run from recentsView for case when recents animation already running
         recentsView.addSideTaskLaunchCallback(endCallback);
@@ -236,7 +237,19 @@
 
     @Override
     public void launchTask(@NonNull Consumer<Boolean> callback, boolean isQuickswitch) {
-        getRecentsView().getSplitSelectController().launchExistingSplitPair(this, mTask.key.id,
+        launchTaskInternal(callback, isQuickswitch, false /*launchingExistingTaskview*/);
+    }
+
+    /**
+     * @param launchingExistingTaskView {@link SplitSelectStateController#launchExistingSplitPair}
+     * uses existence of GroupedTaskView as control flow of how to animate in the incoming task. If
+     * we're launching from overview (from overview thumbnails) then pass in {@code true},
+     * otherwise pass in {@code false} for case like quickswitching from home to task
+     */
+    private void launchTaskInternal(@NonNull Consumer<Boolean> callback, boolean isQuickswitch,
+            boolean launchingExistingTaskView) {
+        getRecentsView().getSplitSelectController().launchExistingSplitPair(
+                launchingExistingTaskView ? this : null, mTask.key.id,
                 mSecondaryTask.key.id, SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT,
                 callback, isQuickswitch, getSplitRatio());
     }
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 4dbf4e3..ebe4c66 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -30,18 +30,20 @@
 import android.content.Context;
 import android.os.Build;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.MotionEvent;
-import android.view.Surface;
 
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.PendingSplitSelectInfo;
 import com.android.launcher3.util.SplitConfigurationOptions;
@@ -81,14 +83,23 @@
     }
 
     @Override
-    public void startHome(boolean animated) {
+    protected void handleStartHome(boolean animated) {
         StateManager stateManager = mActivity.getStateManager();
         animated &= stateManager.shouldAnimateStateChange();
         stateManager.goToState(NORMAL, animated);
+        if (FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+            mSplitSelectStateController.getSplitAnimationController()
+                    .playPlaceholderDismissAnim(mActivity);
+        }
         AbstractFloatingView.closeAllOpenViews(mActivity, animated);
     }
 
     @Override
+    protected boolean canStartHomeSafely() {
+        return mActivity.canStartHomeSafely();
+    }
+
+    @Override
     protected void onTaskLaunchAnimationEnd(boolean success) {
         if (success) {
             mActivity.getStateManager().moveToRestState();
@@ -118,7 +129,9 @@
     @Override
     public void reset() {
         super.reset();
-        setLayoutRotation(Surface.ROTATION_0, Surface.ROTATION_0);
+
+        int recentsActivityRotation = getPagedViewOrientedState().getRecentsActivityRotation();
+        setLayoutRotation(recentsActivityRotation, recentsActivityRotation);
     }
 
     @Override
@@ -155,6 +168,8 @@
     @Override
     public void setOverviewStateEnabled(boolean enabled) {
         super.setOverviewStateEnabled(enabled);
+        Log.d(TestProtocol.OVERVIEW_OVER_HOME, "overview state enabled state has changed: "
+                + enabled);
         if (enabled) {
             LauncherState state = mActivity.getStateManager().getState();
             boolean hasClearAllButton = (state.getVisibleElements(mActivity)
@@ -218,7 +233,11 @@
 
     @Override
     protected boolean canLaunchFullscreenTask() {
-        return !mActivity.isInState(OVERVIEW_SPLIT_SELECT);
+        if (FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+            return !mSplitSelectStateController.isSplitSelectActive();
+        } else {
+            return !mActivity.isInState(OVERVIEW_SPLIT_SELECT);
+        }
     }
 
     @Override
@@ -228,7 +247,7 @@
         DesktopVisibilityController desktopVisibilityController =
                 mActivity.getDesktopVisibilityController();
         if (desktopVisibilityController != null) {
-            desktopVisibilityController.setGestureInProgress(true);
+            desktopVisibilityController.setRecentsGestureStart();
         }
     }
 
@@ -236,9 +255,11 @@
     public void onGestureAnimationEnd() {
         DesktopVisibilityController desktopVisibilityController = null;
         boolean showDesktopApps = false;
+        GestureState.GestureEndTarget endTarget = null;
         if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) {
             desktopVisibilityController = mActivity.getDesktopVisibilityController();
-            if (mCurrentGestureEndTarget == GestureState.GestureEndTarget.LAST_TASK
+            endTarget = mCurrentGestureEndTarget;
+            if (endTarget == GestureState.GestureEndTarget.LAST_TASK
                     && desktopVisibilityController.areFreeformTasksVisible()) {
                 // Recents gesture was cancelled and we are returning to the previous task.
                 // After super class has handled clean up, show desktop apps on top again
@@ -247,7 +268,7 @@
         }
         super.onGestureAnimationEnd();
         if (desktopVisibilityController != null) {
-            desktopVisibilityController.setGestureInProgress(false);
+            desktopVisibilityController.setRecentsGestureEnd(endTarget);
         }
         if (showDesktopApps) {
             SystemUiProxy.INSTANCE.get(mActivity).showDesktopApps(mActivity.getDisplayId());
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index e47c089..b31791a 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -258,6 +258,13 @@
     }
 
     /**
+     * Returns the visibility of the overview actions buttons.
+     */
+    public @Visibility int getActionsButtonVisibility() {
+        return findViewById(R.id.action_buttons).getVisibility();
+    }
+
+    /**
      * Offsets OverviewActionsView horizontal position based on 3 button nav container in taskbar.
      */
     private void updatePadding() {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 55b9f95..76f9c2c 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -21,6 +21,16 @@
 import static android.view.View.MeasureSpec.EXACTLY;
 import static android.view.View.MeasureSpec.makeMeasureSpec;
 
+import static com.android.app.animation.Interpolators.ACCELERATE;
+import static com.android.app.animation.Interpolators.ACCELERATE_0_75;
+import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
+import static com.android.app.animation.Interpolators.DECELERATE_2;
+import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
+import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.app.animation.Interpolators.FINAL_FRAME;
+import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.OVERSHOOT_0_75;
+import static com.android.app.animation.Interpolators.clampToProgress;
 import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU;
 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
@@ -32,18 +42,7 @@
 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_DEACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED_DECELERATE;
-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.LINEAR;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_0_75;
-import static com.android.launcher3.anim.Interpolators.clampToProgress;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_GRID_ONLY_OVERVIEW;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_LAUNCH_FROM_STAGED_APP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_ACTIONS_SPLIT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP;
@@ -54,6 +53,7 @@
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
+import static com.android.quickstep.util.LogUtils.splitFailureMessage;
 import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA;
 import static com.android.quickstep.views.DesktopTaskView.DESKTOP_MODE_SUPPORTED;
 import static com.android.quickstep.views.OverviewActionsView.FLAG_IS_NOT_TABLET;
@@ -165,6 +165,7 @@
 import com.android.launcher3.util.ViewPool;
 import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.GestureState;
+import com.android.quickstep.OverviewCommandHelper;
 import com.android.quickstep.RecentsAnimationController;
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.RecentsFilterState;
@@ -173,6 +174,7 @@
 import com.android.quickstep.RemoteTargetGluer;
 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
 import com.android.quickstep.RotationTouchHelper;
+import com.android.quickstep.SplitSelectionListener;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskThumbnailCache;
@@ -211,6 +213,7 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
@@ -661,7 +664,8 @@
     /**
      * Placeholder view indicating where the first split screen selected app will be placed
      */
-    private SplitSelectStateController mSplitSelectStateController;
+    protected SplitSelectStateController mSplitSelectStateController;
+
     /**
      * The first task that split screen selection was initiated with. When split select state is
      * initialized, we create a
@@ -679,11 +683,22 @@
     private final Toast mSplitUnsupportedToast = Toast.makeText(getContext(),
             R.string.toast_split_app_unsupported, Toast.LENGTH_SHORT);
 
-    private SplitInstructionsView mSplitInstructionsView;
-
     @Nullable
     private SplitSelectSource mSplitSelectSource;
 
+    private final SplitSelectionListener mSplitSelectionListener = new SplitSelectionListener() {
+        @Override
+        public void onSplitSelectionConfirmed() { }
+
+        @Override
+        public void onSplitSelectionActive() { }
+
+        @Override
+        public void onSplitSelectionExit(boolean launchedSplit) {
+            resetFromSplitSelectionState();
+        }
+    };
+
     /**
      * Keeps track of the index of the TaskView that split screen was initialized with so we know
      * where to insert it back into list of taskViews in case user backs out of entering split
@@ -694,8 +709,6 @@
      */
     private int mSplitHiddenTaskViewIndex = -1;
     @Nullable
-    private FloatingTaskView mFirstFloatingTaskView;
-    @Nullable
     private FloatingTaskView mSecondFloatingTaskView;
 
     /**
@@ -992,16 +1005,34 @@
     }
 
     /**
-     * Update the thumbnail of the task.
+     * Update the thumbnail(s) of the relevant TaskView.
      * @param refreshNow Refresh immediately if it's true.
      */
     @Nullable
-    public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData, boolean refreshNow) {
-        TaskView taskView = getTaskViewByTaskId(taskId);
-        if (taskView != null) {
-            taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData, refreshNow);
+    public TaskView updateThumbnail(
+            HashMap<Integer, ThumbnailData> thumbnailData, boolean refreshNow) {
+        TaskView updatedTaskView = null;
+        for (Map.Entry<Integer, ThumbnailData> entry : thumbnailData.entrySet()) {
+            Integer id = entry.getKey();
+            ThumbnailData thumbnail = entry.getValue();
+            TaskView taskView = getTaskViewByTaskId(id);
+            if (taskView == null) {
+                continue;
+            }
+            // taskView could be a GroupedTaskView, so select the relevant task by ID
+            TaskIdAttributeContainer taskAttributes = taskView.getTaskAttributesById(id);
+            if (taskAttributes == null) {
+                continue;
+            }
+            Task task = taskAttributes.getTask();
+            TaskThumbnailView taskThumbnailView = taskAttributes.getThumbnailView();
+            taskThumbnailView.setThumbnail(task, thumbnail, refreshNow);
+            // thumbnailData can contain 1-2 ids, but they should correspond to the same
+            // TaskView, so overwriting is ok
+            updatedTaskView = taskView;
         }
-        return taskView;
+
+        return updatedTaskView;
     }
 
     @Override
@@ -1048,6 +1079,9 @@
                 mIPipAnimationListener);
         mOrientationState.initListeners();
         mTaskOverlayFactory.initListeners();
+        if (FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+            mSplitSelectStateController.registerSplitListener(mSplitSelectionListener);
+        }
     }
 
     @Override
@@ -1066,6 +1100,9 @@
         mIPipAnimationListener.setActivityAndRecentsView(null, null);
         mOrientationState.destroyListeners();
         mTaskOverlayFactory.removeListeners();
+        if (FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+            mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener);
+        }
     }
 
     @Override
@@ -1180,7 +1217,7 @@
                     new SurfaceTransactionApplier(mActivity.getDragLayer());
             ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
             appAnimator.setDuration(RECENTS_LAUNCH_DURATION);
-            appAnimator.setInterpolator(ACCEL_DEACCEL);
+            appAnimator.setInterpolator(ACCELERATE_DECELERATE);
             appAnimator.addUpdateListener(valueAnimator -> {
                 float percent = valueAnimator.getAnimatedFraction();
                 SurfaceTransaction transaction = new SurfaceTransaction();
@@ -1330,9 +1367,15 @@
             return null;
         }
 
+        // We're looking for a taskView that matches these ids, regardless of order
+        int[] taskIdsCopy = Arrays.copyOf(taskIds, taskIds.length);
+        Arrays.sort(taskIdsCopy);
+
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView taskView = requireTaskViewAt(i);
-            if (Arrays.equals(taskIds, taskView.getTaskIds())) {
+            int[] taskViewIdsCopy = taskView.getTaskIds();
+            Arrays.sort(taskViewIdsCopy);
+            if (Arrays.equals(taskIdsCopy, taskViewIdsCopy)) {
                 return taskView;
             }
         }
@@ -1353,6 +1396,7 @@
             // its thumbnail
             mTmpRunningTasks = null;
             mSplitBoundsConfig = null;
+            mTaskOverlayFactory.clearAllActiveState();
         }
         updateLocusId();
     }
@@ -1631,6 +1675,10 @@
         int[] runningTaskId = getTaskIdsForTaskViewId(mRunningTaskViewId);
         int[] focusedTaskId = getTaskIdsForTaskViewId(mFocusedTaskViewId);
 
+        // Reset the focused task to avoiding initializing TaskViews layout as focused task during
+        // binding. The focused task view will be updated after all the TaskViews are bound.
+        mFocusedTaskViewId = INVALID_TASK_ID;
+
         // Removing views sets the currentPage to 0, so we save this and restore it after
         // the new set of views are added
         int previousCurrentPage = mCurrentPage;
@@ -1639,8 +1687,8 @@
         // If we are entering Overview as a result of initiating a split from somewhere else
         // (e.g. split from Home), we need to make sure the staged app is not drawn as a thumbnail.
         int stagedTaskIdToBeRemovedFromGrid;
-        if (mSplitSelectSource != null) {
-            stagedTaskIdToBeRemovedFromGrid = mSplitSelectSource.alreadyRunningTaskId;
+        if (isSplitSelectionActive()) {
+            stagedTaskIdToBeRemovedFromGrid = mSplitSelectStateController.getInitialTaskId();
             updateCurrentTaskActionsVisibility();
         } else {
             stagedTaskIdToBeRemovedFromGrid = INVALID_TASK_ID;
@@ -1737,7 +1785,9 @@
         mFocusedTaskViewId = newFocusedTaskView != null && !ENABLE_GRID_ONLY_OVERVIEW.get()
                 ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID;
         updateTaskSize();
-        updateChildTaskOrientations();
+        if (newFocusedTaskView != null) {
+            newFocusedTaskView.setOrientationState(mOrientationState);
+        }
 
         TaskView newRunningTaskView = null;
         if (hasAnyValidTaskIds(runningTaskId)) {
@@ -2322,7 +2372,15 @@
         startHome(mActivity.isStarted());
     }
 
-    public abstract void startHome(boolean animated);
+    public void startHome(boolean animated) {
+        if (!canStartHomeSafely()) return;
+        handleStartHome(animated);
+    }
+
+    protected abstract void handleStartHome(boolean animated);
+
+    /** Returns whether user can start home based on state in {@link OverviewCommandHelper}. */
+    protected abstract boolean canStartHomeSafely();
 
     public void reset() {
         setCurrentTask(-1);
@@ -2344,7 +2402,9 @@
             remoteTargetHandle.getTransformParams().setTargetSet(null);
             remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(false);
         });
-        resetFromSplitSelectionState();
+        if (!FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+            resetFromSplitSelectionState();
+        }
 
         // These are relatively expensive and don't need to be done this frame (RecentsView isn't
         // visible anyway), so defer by a frame to get off the critical path, e.g. app to home.
@@ -3126,7 +3186,7 @@
         // Use setFloat instead of setViewAlpha as we want to keep the view visible even when it's
         // alpha is set to 0 so that it can be recycled in the view pool properly
         anim.setFloat(taskView, VIEW_ALPHA, 0,
-                clampToProgress(isOnGridBottomRow(taskView) ? ACCEL : FINAL_FRAME, 0, 0.5f));
+                clampToProgress(isOnGridBottomRow(taskView) ? ACCELERATE : FINAL_FRAME, 0, 0.5f));
         FloatProperty<TaskView> secondaryViewTranslate =
                 taskView.getSecondaryDismissTranslationProperty();
         int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView);
@@ -3165,7 +3225,7 @@
                 AnimUtils.getDeviceOverviewToSplitTimings(mActivity.getDeviceProfile().isTablet);
 
         RectF startingTaskRect = new RectF();
-        safeRemoveDragLayerView(mFirstFloatingTaskView);
+        safeRemoveDragLayerView(mSplitSelectStateController.getFirstFloatingTaskView());
         SplitAnimInitProps splitAnimInitProps =
                 mSplitSelectStateController.getSplitAnimationController().getFirstAnimInitViews(
                         () -> mSplitHiddenTaskView, () -> mSplitSelectSource);
@@ -3178,33 +3238,34 @@
                     timings.getIconFadeEndOffset()));
         }
 
-        mFirstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity,
+        FloatingTaskView firstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity,
                 splitAnimInitProps.getOriginalView(),
                 splitAnimInitProps.getOriginalBitmap(),
                 splitAnimInitProps.getIconDrawable(), startingTaskRect);
-        mFirstFloatingTaskView.setAlpha(1);
-        mFirstFloatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
+        firstFloatingTaskView.setAlpha(1);
+        firstFloatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
                 splitAnimInitProps.getFadeWithThumbnail(), splitAnimInitProps.isStagedTask());
+        mSplitSelectStateController.setFirstFloatingTaskView(firstFloatingTaskView);
 
         // Allow user to click staged app to launch into fullscreen
-        if (ENABLE_LAUNCH_FROM_STAGED_APP.get()) {
-            mFirstFloatingTaskView.setOnClickListener(this::animateToFullscreen);
-        }
+        firstFloatingTaskView.setOnClickListener(this::animateToFullscreen);
 
         // SplitInstructionsView: animate in
-        safeRemoveDragLayerView(mSplitInstructionsView);
-        mSplitInstructionsView = SplitInstructionsView.getSplitInstructionsView(mActivity);
-        mSplitInstructionsView.setAlpha(0);
-        anim.setViewAlpha(mSplitInstructionsView, 1, clampToProgress(LINEAR,
+        safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
+        SplitInstructionsView splitInstructionsView =
+                SplitInstructionsView.getSplitInstructionsView(mActivity);
+        splitInstructionsView.setAlpha(0);
+        anim.setViewAlpha(splitInstructionsView, 1, clampToProgress(LINEAR,
                 timings.getInstructionsContainerFadeInStartOffset(),
                 timings.getInstructionsContainerFadeInEndOffset()));
-        anim.setViewAlpha(mSplitInstructionsView.getTextView(), 1, clampToProgress(LINEAR,
+        anim.setViewAlpha(splitInstructionsView.getTextView(), 1, clampToProgress(LINEAR,
                 timings.getInstructionsTextFadeInStartOffset(),
                 timings.getInstructionsTextFadeInEndOffset()));
-        anim.addFloat(mSplitInstructionsView, mSplitInstructionsView.UNFOLD, 0.1f, 1,
+        anim.addFloat(splitInstructionsView, splitInstructionsView.UNFOLD, 0.1f, 1,
                 clampToProgress(EMPHASIZED_DECELERATE,
                         timings.getInstructionsUnfoldStartOffset(),
                         timings.getInstructionsUnfoldEndOffset()));
+        mSplitSelectStateController.setSplitInstructionsView(splitInstructionsView);
 
         InteractionJankMonitorWrapper.begin(this,
                 InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "First tile selected");
@@ -3227,7 +3288,11 @@
                         InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER);
             } else {
                 // If transition to split select was interrupted, clean up to prevent glitches
-                resetFromSplitSelectionState();
+                if (FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+                    mSplitSelectStateController.resetState();
+                } else {
+                    resetFromSplitSelectionState();
+                }
                 InteractionJankMonitorWrapper.cancel(
                         InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER);
             }
@@ -3260,8 +3325,13 @@
                 true /* isStagedTask */);
 
         pendingAnimation.addEndListener(animationSuccess ->
-                mSplitSelectStateController.launchInitialAppFullscreen(launchSuccess ->
-                        resetFromSplitSelectionState()));
+                mSplitSelectStateController.launchInitialAppFullscreen(launchSuccess -> {
+                    if (FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+                        mSplitSelectStateController.resetState();
+                    } else {
+                        resetFromSplitSelectionState();
+                    }
+                }));
 
         pendingAnimation.buildAnim().start();
     }
@@ -3968,13 +4038,12 @@
 
     private void removeTaskInternal(int dismissedTaskViewId) {
         int[] taskIds = getTaskIdsForTaskViewId(dismissedTaskViewId);
-        int primaryTaskId = taskIds[0];
-        int secondaryTaskId = taskIds[1];
         UI_HELPER_EXECUTOR.getHandler().post(
                 () -> {
-                    ActivityManagerWrapper.getInstance().removeTask(primaryTaskId);
-                    if (secondaryTaskId != -1) {
-                        ActivityManagerWrapper.getInstance().removeTask(secondaryTaskId);
+                    for (int taskId : taskIds) {
+                        if (taskId != -1) {
+                            ActivityManagerWrapper.getInstance().removeTask(taskId);
+                        }
                     }
                 });
     }
@@ -4294,7 +4363,7 @@
 
     private void updatePageOffsets() {
         float offset = mAdjacentPageHorizontalOffset;
-        float modalOffset = ACCEL_0_75.getInterpolation(mTaskModalness);
+        float modalOffset = ACCELERATE_0_75.getInterpolation(mTaskModalness);
         int count = getChildCount();
         boolean showAsGrid = showAsGrid();
 
@@ -4662,6 +4731,8 @@
             return false;
         }
         if (mSplitSelectStateController.isBothSplitAppsConfirmed()) {
+            Log.w(TAG, splitFailureMessage(
+                    "confirmSplitSelect", "both apps have already been set"));
             return true;
         }
         // Second task is selected either as an already-running Task or an Intent
@@ -4669,6 +4740,9 @@
             if (!task.isDockable) {
                 // Task does not support split screen
                 mSplitUnsupportedToast.show();
+                Log.w(TAG, splitFailureMessage("confirmSplitSelect",
+                        "selected Task (" + task.key.getPackageName()
+                                + ") is not dockable / does not support splitscreen"));
                 return true;
             }
             mSplitSelectStateController.setSecondTask(task);
@@ -4693,8 +4767,10 @@
                 mSplitSelectStateController.getActiveSplitStagePosition(), firstTaskEndingBounds,
                 secondTaskEndingBounds);
 
-        mFirstFloatingTaskView.getBoundsOnScreen(firstTaskStartingBounds);
-        mFirstFloatingTaskView.addConfirmAnimation(pendingAnimation,
+        FloatingTaskView firstFloatingTaskView =
+                mSplitSelectStateController.getFirstFloatingTaskView();
+        firstFloatingTaskView.getBoundsOnScreen(firstTaskStartingBounds);
+        firstFloatingTaskView.addConfirmAnimation(pendingAnimation,
                 new RectF(firstTaskStartingBounds), firstTaskEndingBounds,
                 false /* fadeWithThumbnail */, true /* isStagedTask */);
 
@@ -4706,14 +4782,21 @@
         mSecondFloatingTaskView.addConfirmAnimation(pendingAnimation, secondTaskStartingBounds,
                 secondTaskEndingBounds, true /* fadeWithThumbnail */, false /* isStagedTask */);
 
-        pendingAnimation.setViewAlpha(mSplitInstructionsView, 0, clampToProgress(LINEAR,
-                timings.getInstructionsFadeStartOffset(),
-                timings.getInstructionsFadeEndOffset()));
+        pendingAnimation.setViewAlpha(mSplitSelectStateController.getSplitInstructionsView(), 0,
+                clampToProgress(LINEAR, timings.getInstructionsFadeStartOffset(),
+                        timings.getInstructionsFadeEndOffset()));
 
         pendingAnimation.addEndListener(aBoolean -> {
             mSplitSelectStateController.launchSplitTasks(
-                    aBoolean1 -> RecentsView.this.resetFromSplitSelectionState());
-            InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER);
+                    aBoolean1 -> {
+                        if (FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+                            mSplitSelectStateController.resetState();
+                        } else {
+                            resetFromSplitSelectionState();
+                        }
+                        InteractionJankMonitorWrapper.end(
+                                InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER);
+                    });
         });
 
         mSecondSplitHiddenView = containerTaskView;
@@ -4727,21 +4810,22 @@
 
         // Fade out all other views underneath placeholders
         ObjectAnimator tvFade = ObjectAnimator.ofFloat(this, RecentsView.CONTENT_ALPHA,1, 0);
-        pendingAnimation.add(tvFade, DEACCEL_2, SpringProperty.DEFAULT);
+        pendingAnimation.add(tvFade, DECELERATE_2, SpringProperty.DEFAULT);
         pendingAnimation.buildAnim().start();
         return true;
     }
 
     @SuppressLint("WrongCall")
     protected void resetFromSplitSelectionState() {
-        if (mSplitSelectSource != null || mSplitHiddenTaskViewIndex != -1) {
-            safeRemoveDragLayerView(mFirstFloatingTaskView);
+        if (mSplitSelectSource != null || mSplitHiddenTaskViewIndex != -1 ||
+                FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+            safeRemoveDragLayerView(mSplitSelectStateController.getFirstFloatingTaskView());
             safeRemoveDragLayerView(mSecondFloatingTaskView);
-            safeRemoveDragLayerView(mSplitInstructionsView);
-            mFirstFloatingTaskView = null;
+            safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
             mSecondFloatingTaskView = null;
-            mSplitInstructionsView = null;
             mSplitSelectSource = null;
+            mSplitSelectStateController.getSplitAnimationController()
+                    .removeSplitInstructionsView(mActivity);
         }
 
         if (mSecondSplitHiddenView != null) {
@@ -4754,7 +4838,11 @@
         setTaskViewsPrimarySplitTranslation(0);
         setTaskViewsSecondarySplitTranslation(0);
 
-        mSplitSelectStateController.resetState();
+        if (!FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+            // When flag is on, this method gets called from resetState() call below, let's avoid
+            // infinite recursion today
+            mSplitSelectStateController.resetState();
+        }
         if (mSplitHiddenTaskViewIndex == -1) {
             return;
         }
@@ -4821,8 +4909,10 @@
                 mSplitPlaceholderInset, mActivity.getDeviceProfile(),
                 mSplitSelectStateController.getActiveSplitStagePosition(), mTempRect);
         mTempRectF.set(mTempRect);
-        mFirstFloatingTaskView.updateOrientationHandler(mOrientationHandler);
-        mFirstFloatingTaskView.update(mTempRectF, /*progress=*/1f);
+        FloatingTaskView firstFloatingTaskView =
+                mSplitSelectStateController.getFirstFloatingTaskView();
+        firstFloatingTaskView.updateOrientationHandler(mOrientationHandler);
+        firstFloatingTaskView.update(mTempRectF, /*progress=*/1f);
 
         PagedOrientationHandler orientationHandler = getPagedOrientationHandler();
         Pair<FloatProperty, FloatProperty> taskViewsFloat =
@@ -4832,8 +4922,8 @@
         taskViewsFloat.first.set(this, getSplitSelectTranslation());
         taskViewsFloat.second.set(this, 0f);
 
-        if (mSplitInstructionsView != null) {
-            mSplitInstructionsView.ensureProperRotation();
+        if (mSplitSelectStateController.getSplitInstructionsView() != null) {
+            mSplitSelectStateController.getSplitInstructionsView().ensureProperRotation();
         }
     }
 
@@ -5152,7 +5242,8 @@
                     true /* forDesktop */);
             mRemoteTargetHandles = gluer.assignTargetsForDesktop(recentsAnimationTargets);
         } else {
-            gluer = new RemoteTargetGluer(getContext(), getSizeStrategy());
+            gluer = new RemoteTargetGluer(getContext(), getSizeStrategy(), recentsAnimationTargets,
+                    false);
             mRemoteTargetHandles = gluer.assignTargetsForSplitScreen(recentsAnimationTargets);
         }
         mSplitBoundsConfig = gluer.getSplitBounds();
@@ -5207,6 +5298,8 @@
         cleanupRemoteTargets();
 
         if (mRecentsAnimationController == null) {
+            Log.d(TestProtocol.INCORRECT_HOME_STATE, "finish recents animation but recents "
+                    + "animation controller was null. returning.");
             if (onFinishComplete != null) {
                 onFinishComplete.run();
             }
@@ -5943,13 +6036,8 @@
     }
 
     @Nullable
-    public FloatingTaskView getFirstFloatingTaskView() {
-        return mFirstFloatingTaskView;
-    }
-
-    @Nullable
     public SplitInstructionsView getSplitInstructionsView() {
-        return mSplitInstructionsView;
+        return mSplitSelectStateController.getSplitInstructionsView();
     }
 
     /** Update the current activity locus id to show the enabled state of Overview */
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index 0d9e412..4ca02e0 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -17,15 +17,21 @@
 package com.android.quickstep.views;
 
 import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
+import android.view.MotionEvent;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 
 import androidx.annotation.Nullable;
 import androidx.appcompat.widget.AppCompatTextView;
 
+import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StatefulActivity;
 
 /**
@@ -65,7 +71,7 @@
         mLauncher = (StatefulActivity) context;
     }
 
-    static SplitInstructionsView getSplitInstructionsView(StatefulActivity launcher) {
+    public static SplitInstructionsView getSplitInstructionsView(StatefulActivity launcher) {
         ViewGroup dragLayer = launcher.getDragLayer();
         final SplitInstructionsView splitInstructionsView =
                 (SplitInstructionsView) launcher.getLayoutInflater().inflate(
@@ -73,9 +79,7 @@
                         dragLayer,
                         false
                 );
-
-        splitInstructionsView.mTextView = splitInstructionsView.findViewById(
-                R.id.split_instructions_text);
+        splitInstructionsView.init();
 
         // Since textview overlays base view, and we sometimes manipulate the alpha of each
         // simultaneously, force overlapping rendering to false prevents redrawing of pixels,
@@ -92,6 +96,71 @@
         ensureProperRotation();
     }
 
+    private void init() {
+        mTextView = findViewById(R.id.split_instructions_text);
+
+        if (!FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+            mTextView.setCompoundDrawables(null, null, null, null);
+            return;
+        }
+
+        mTextView.setOnTouchListener((v, event) -> {
+            if (isTouchInsideRightCompoundDrawable(event)) {
+                if (event.getAction() == MotionEvent.ACTION_UP) {
+                    exitSplitSelection();
+                }
+                return true;
+            }
+            return false;
+        });
+    }
+
+    private void exitSplitSelection() {
+        ((RecentsView) mLauncher.getOverviewPanel()).getSplitSelectController()
+                .getSplitAnimationController().playPlaceholderDismissAnim(mLauncher);
+        mLauncher.getStateManager().goToState(LauncherState.NORMAL);
+    }
+
+    private boolean isTouchInsideRightCompoundDrawable(MotionEvent event) {
+        // Get the right compound drawable of the TextView.
+        Drawable rightDrawable = mTextView.getCompoundDrawablesRelative()[2];
+
+        // Check if the touch event intersects with the drawable's bounds.
+        if (rightDrawable != null) {
+            // We can get away w/o caring about the Y bounds since it's such a small view, if it's
+            // above/below the drawable just assume they meant to touch it. ¯\_(ツ)_/¯
+            return  event.getX() >= (mTextView.getWidth() - rightDrawable.getBounds().width());
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        if (!FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+            return;
+        }
+
+        info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+                R.string.toast_split_select_cont_desc,
+                getResources().getString(R.string.toast_split_select_cont_desc)
+        ));
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle arguments) {
+        if (!FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+            return super.performAccessibilityAction(action, arguments);
+        }
+
+        if (action == R.string.toast_split_select_cont_desc) {
+            exitSplitSelection();
+            return true;
+        }
+        return super.performAccessibilityAction(action, arguments);
+    }
+
     void ensureProperRotation() {
         ((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler()
                 .setSplitInstructionsParams(
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index 2c9afb4..b4d24e5 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -36,12 +36,12 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.touch.PagedOrientationHandler;
@@ -256,7 +256,7 @@
 
         final Animator revealAnimator = createOpenCloseOutlineProvider()
                 .createRevealAnimator(this, closing);
-        revealAnimator.setInterpolator(Interpolators.DEACCEL);
+        revealAnimator.setInterpolator(Interpolators.DECELERATE);
         mOpenCloseAnimator.playTogether(revealAnimator,
                 ObjectAnimator.ofFloat(
                         mTaskContainer.getThumbnailView(), DIM_ALPHA,
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 83a5c72..a2976a8 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -20,11 +20,11 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.widget.Toast.LENGTH_SHORT;
 
+import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
+import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -32,6 +32,7 @@
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
 import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition;
+import static com.android.quickstep.TaskOverlayFactory.getEnabledShortcuts;
 import static com.android.quickstep.util.BorderAnimator.DEFAULT_BORDER_COLOR;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -70,12 +71,13 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
+import com.android.app.animation.Interpolators;
 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.Interpolators;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.SystemShortcut;
@@ -88,6 +90,7 @@
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.util.TransformingTouchDelegate;
 import com.android.launcher3.util.ViewPool.Reusable;
 import com.android.quickstep.RecentsModel;
@@ -95,7 +98,6 @@
 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
 import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.TaskIconCache;
-import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.TaskViewUtils;
@@ -132,15 +134,17 @@
 
     public static final int FLAG_UPDATE_ICON = 1;
     public static final int FLAG_UPDATE_THUMBNAIL = FLAG_UPDATE_ICON << 1;
+    public static final int FLAG_UPDATE_CORNER_RADIUS = FLAG_UPDATE_THUMBNAIL << 1;
 
-    public static final int FLAG_UPDATE_ALL = FLAG_UPDATE_ICON | FLAG_UPDATE_THUMBNAIL;
+    public static final int FLAG_UPDATE_ALL = FLAG_UPDATE_ICON | FLAG_UPDATE_THUMBNAIL
+            | FLAG_UPDATE_CORNER_RADIUS;
 
     /**
      * Used in conjunction with {@link #onTaskListVisibilityChanged(boolean, int)}, providing more
      * granularity on which components of this task require an update
      */
     @Retention(SOURCE)
-    @IntDef({FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL})
+    @IntDef({FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS})
     public @interface TaskDataChanges {}
 
     /**
@@ -163,7 +167,7 @@
     public static final long SCALE_ICON_DURATION = 120;
     private static final long DIM_ANIM_DURATION = 700;
 
-    private static final Interpolator GRID_INTERPOLATOR = ACCEL_DEACCEL;
+    private static final Interpolator GRID_INTERPOLATOR = ACCELERATE_DECELERATE;
 
     /**
      * This technically can be a vanilla {@link TouchDelegate} class, however that class requires
@@ -411,7 +415,9 @@
 
     private boolean mIsClickableAsLiveTile = true;
 
-    @Nullable private final BorderAnimator mBorderAnimator;
+    @Nullable private final BorderAnimator mFocusBorderAnimator;
+
+    @Nullable private final BorderAnimator mHoverBorderAnimator;
 
     public TaskView(Context context) {
         this(context, null);
@@ -437,23 +443,40 @@
         boolean keyboardFocusHighlightEnabled = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
                 || DesktopTaskView.DESKTOP_MODE_SUPPORTED;
 
-        setWillNotDraw(!keyboardFocusHighlightEnabled);
+        boolean willDrawBorder =
+                keyboardFocusHighlightEnabled || FeatureFlags.ENABLE_CURSOR_HOVER_STATES.get();
+        setWillNotDraw(!willDrawBorder);
 
-        TypedArray ta = context.obtainStyledAttributes(
-                attrs, R.styleable.TaskView, defStyleAttr, defStyleRes);
+        if (willDrawBorder) {
+            TypedArray styledAttrs = context.obtainStyledAttributes(
+                    attrs, R.styleable.TaskView, defStyleAttr, defStyleRes);
 
-        mBorderAnimator = !keyboardFocusHighlightEnabled
-                ? null
-                : new BorderAnimator(
-                        /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
-                        /* borderColor= */ ta.getColor(
-                                R.styleable.TaskView_borderColor, DEFAULT_BORDER_COLOR),
-                        /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
-                                /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
-                                        R.dimen.keyboard_quick_switch_border_width),
-                                /* boundsBuilder= */ this::updateBorderBounds,
-                                /* targetView= */ this));
-        ta.recycle();
+            mFocusBorderAnimator = keyboardFocusHighlightEnabled ? new BorderAnimator(
+                    /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
+                    /* borderColor= */ styledAttrs.getColor(
+                            R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR),
+                    /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
+                            /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
+                                    R.dimen.keyboard_quick_switch_border_width),
+                            /* boundsBuilder= */ this::updateBorderBounds,
+                            /* targetView= */ this)) : null;
+
+            mHoverBorderAnimator =
+                    FeatureFlags.ENABLE_CURSOR_HOVER_STATES.get() ? new BorderAnimator(
+                            /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
+                            /* borderColor= */ styledAttrs.getColor(
+                                    R.styleable.TaskView_hoverBorderColor, DEFAULT_BORDER_COLOR),
+                            /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
+                                    /* borderWidthPx= */ context.getResources()
+                                            .getDimensionPixelSize(R.dimen.task_hover_border_width),
+                                    /* boundsBuilder= */ this::updateBorderBounds,
+                                    /* targetView= */ this)) : null;
+
+            styledAttrs.recycle();
+        } else {
+            mFocusBorderAnimator = null;
+            mHoverBorderAnimator = null;
+        }
     }
 
     protected void updateBorderBounds(Rect bounds) {
@@ -507,16 +530,48 @@
     @Override
     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
-        if (mBorderAnimator != null) {
-            mBorderAnimator.buildAnimator(gainFocus).start();
+        if (mFocusBorderAnimator != null) {
+            mFocusBorderAnimator.buildAnimator(gainFocus).start();
+        }
+    }
+
+    @Override
+    public boolean onHoverEvent(MotionEvent event) {
+        if (FeatureFlags.ENABLE_CURSOR_HOVER_STATES.get()) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_HOVER_ENTER:
+                    mHoverBorderAnimator.buildAnimator(/* isAppearing= */ true).start();
+                    break;
+                case MotionEvent.ACTION_HOVER_EXIT:
+                    mHoverBorderAnimator.buildAnimator(/* isAppearing= */ false).start();
+                    break;
+                default:
+                    break;
+            }
+        }
+        return super.onHoverEvent(event);
+    }
+
+    @Override
+    public boolean onInterceptHoverEvent(MotionEvent event) {
+        if (FeatureFlags.ENABLE_CURSOR_HOVER_STATES.get()) {
+            // avoid triggering hover event on child elements which would cause HOVER_EXIT for this
+            // task view
+            return true;
+        } else {
+            return super.onInterceptHoverEvent(event);
         }
     }
 
     @Override
     public void draw(Canvas canvas) {
         super.draw(canvas);
-        if (mBorderAnimator != null) {
-            mBorderAnimator.drawBorder(canvas);
+        if (mFocusBorderAnimator != null) {
+            mFocusBorderAnimator.drawBorder(canvas);
+        }
+
+        if (mHoverBorderAnimator != null) {
+            mHoverBorderAnimator.drawBorder(canvas);
         }
     }
 
@@ -580,7 +635,6 @@
                 mIconView, STAGE_POSITION_UNDEFINED);
         mSnapshotView.bind(task);
         setOrientationState(orientedState);
-        mDigitalWellBeingToast.initialize(mTask);
     }
 
     /**
@@ -1028,6 +1082,9 @@
                             mDigitalWellBeingToast.initialize(task);
                         });
             }
+            if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) {
+                mCurrentFullscreenParams.updateCornerRadius(getContext());
+            }
         } else {
             if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
                 mSnapshotView.setThumbnail(null, null);
@@ -1563,8 +1620,8 @@
             if (taskContainer == null) {
                 continue;
             }
-            for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this,
-                    taskContainer)) {
+            for (SystemShortcut s : TraceHelper.allowIpcs(
+                    "TV.a11yInfo", () -> getEnabledShortcuts(this, taskContainer))) {
                 info.addAction(s.createAccessibilityAction(context));
             }
         }
@@ -1601,7 +1658,7 @@
             if (taskContainer == null) {
                 continue;
             }
-            for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this,
+            for (SystemShortcut s : getEnabledShortcuts(this,
                     taskContainer)) {
                 if (s.hasHandlerForAction(action)) {
                     s.onClick(this);
@@ -1656,10 +1713,15 @@
     }
 
     void updateCurrentFullscreenParams(PreviewPositionHelper previewPositionHelper) {
+        updateFullscreenParams(mCurrentFullscreenParams, previewPositionHelper);
+    }
+
+    protected void updateFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams,
+            PreviewPositionHelper previewPositionHelper) {
         if (getRecentsView() == null) {
             return;
         }
-        mCurrentFullscreenParams.setProgress(mFullscreenProgress, getRecentsView().getScaleX(),
+        fullscreenParams.setProgress(mFullscreenProgress, getRecentsView().getScaleX(),
                 getScaleX(), getWidth(), mActivity.getDeviceProfile(), previewPositionHelper);
     }
 
@@ -1803,16 +1865,29 @@
      */
     public static class FullscreenDrawParams {
 
-        private final float mCornerRadius;
-        private final float mWindowCornerRadius;
+        private float mCornerRadius;
+        private float mWindowCornerRadius;
 
         public float mCurrentDrawnCornerRadius;
 
         public FullscreenDrawParams(Context context) {
-            mCornerRadius = TaskCornerRadius.get(context);
-            mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context);
+            updateCornerRadius(context);
+        }
 
-            mCurrentDrawnCornerRadius = mCornerRadius;
+        /** Recomputes the start and end corner radius for the given Context. */
+        public void updateCornerRadius(Context context) {
+            mCornerRadius = computeTaskCornerRadius(context);
+            mWindowCornerRadius = computeWindowCornerRadius(context);
+        }
+
+        @VisibleForTesting
+        public float computeTaskCornerRadius(Context context) {
+            return TaskCornerRadius.get(context);
+        }
+
+        @VisibleForTesting
+        public float computeWindowCornerRadius(Context context) {
+            return QuickStepContract.getWindowCornerRadius(context);
         }
 
         /**
diff --git a/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt b/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
new file mode 100644
index 0000000..a532762
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
@@ -0,0 +1,127 @@
+package com.android.launcher3.model
+
+import android.app.prediction.AppPredictor
+import android.app.prediction.AppTarget
+import android.app.prediction.AppTargetEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WALLPAPERS
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION
+import com.android.launcher3.util.LauncherModelHelper
+import org.junit.After
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertSame
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.MockitoAnnotations
+
+/** Unit tests for [QuickstepModelDelegate]. */
+@RunWith(AndroidJUnit4::class)
+class QuickstepModelDelegateTest {
+
+    private lateinit var underTest: QuickstepModelDelegate
+    private lateinit var modelHelper: LauncherModelHelper
+
+    @Mock private lateinit var target: AppTarget
+    @Mock private lateinit var mockedAppTargetEvent: AppTargetEvent
+    @Mock private lateinit var allAppsPredictor: AppPredictor
+    @Mock private lateinit var hotseatPredictor: AppPredictor
+    @Mock private lateinit var widgetRecommendationPredictor: AppPredictor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        modelHelper = LauncherModelHelper()
+        underTest = QuickstepModelDelegate(modelHelper.sandboxContext)
+        underTest.mAllAppsState.predictor = allAppsPredictor
+        underTest.mHotseatState.predictor = hotseatPredictor
+        underTest.mWidgetsRecommendationState.predictor = widgetRecommendationPredictor
+        underTest.mApp = LauncherAppState.getInstance(modelHelper.sandboxContext)
+        underTest.mDataModel = BgDataModel()
+    }
+
+    @After
+    fun tearDown() {
+        modelHelper.destroy()
+    }
+
+    @Test
+    fun onAppTargetEvent_notifyTarget() {
+        underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_PREDICTION)
+
+        verify(allAppsPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
+        verifyZeroInteractions(hotseatPredictor)
+        verifyZeroInteractions(widgetRecommendationPredictor)
+    }
+
+    @Test
+    fun onWidgetPrediction_notifyWidgetRecommendationPredictor() {
+        underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_WIDGETS_PREDICTION)
+
+        verifyZeroInteractions(allAppsPredictor)
+        verify(widgetRecommendationPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
+        verifyZeroInteractions(hotseatPredictor)
+    }
+
+    @Test
+    fun onHotseatPrediction_notifyHotseatPredictor() {
+        underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_HOTSEAT_PREDICTION)
+
+        verifyZeroInteractions(allAppsPredictor)
+        verifyZeroInteractions(widgetRecommendationPredictor)
+        verify(hotseatPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
+    }
+
+    @Test
+    fun onOtherClient_notifyHotseatPredictor() {
+        underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_WALLPAPERS)
+
+        verifyZeroInteractions(allAppsPredictor)
+        verifyZeroInteractions(widgetRecommendationPredictor)
+        verify(hotseatPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
+    }
+
+    @Test
+    fun hotseatActionPin_recreateHotSeat() {
+        assertSame(underTest.mHotseatState.predictor, hotseatPredictor)
+        val appTargetEvent = AppTargetEvent.Builder(target, AppTargetEvent.ACTION_PIN).build()
+        underTest.markActive()
+
+        underTest.onAppTargetEvent(appTargetEvent, CONTAINER_HOTSEAT_PREDICTION)
+
+        verify(hotseatPredictor).destroy()
+        assertNotSame(underTest.mHotseatState.predictor, hotseatPredictor)
+    }
+
+    @Test
+    fun hotseatActionUnpin_recreateHotSeat() {
+        assertSame(underTest.mHotseatState.predictor, hotseatPredictor)
+        underTest.markActive()
+        val appTargetEvent = AppTargetEvent.Builder(target, AppTargetEvent.ACTION_UNPIN).build()
+
+        underTest.onAppTargetEvent(appTargetEvent, CONTAINER_HOTSEAT_PREDICTION)
+
+        verify(hotseatPredictor).destroy()
+        assertNotSame(underTest.mHotseatState.predictor, hotseatPredictor)
+    }
+
+    @Test
+    fun container_actionPin_notRecreateHotSeat() {
+        assertSame(underTest.mHotseatState.predictor, hotseatPredictor)
+        val appTargetEvent = AppTargetEvent.Builder(target, AppTargetEvent.ACTION_UNPIN).build()
+        underTest.markActive()
+
+        underTest.onAppTargetEvent(appTargetEvent, CONTAINER_PREDICTION)
+
+        verify(allAppsPredictor, never()).destroy()
+        verify(hotseatPredictor, never()).destroy()
+        assertSame(underTest.mHotseatState.predictor, hotseatPredictor)
+    }
+}
diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
index 83341cb..b12d98b 100644
--- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
+++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
 import static com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -40,11 +41,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.icons.ComponentWithLabel;
-import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
+import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.LauncherModelHelper;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
@@ -53,8 +52,6 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
 
 import java.util.Arrays;
 import java.util.List;
@@ -76,17 +73,9 @@
     private LauncherModelHelper mModelHelper;
     private UserHandle mUserHandle;
 
-    @Mock
-    private IconCache mIconCache;
-
     @Before
     public void setup() throws Exception {
         mModelHelper = new LauncherModelHelper();
-        MockitoAnnotations.initMocks(this);
-        doAnswer(invocation -> {
-            ComponentWithLabel componentWithLabel = invocation.getArgument(0);
-            return componentWithLabel.getComponent().getShortClassName();
-        }).when(mIconCache).getTitleNoCache(any());
 
         mUserHandle = myUserHandle();
         mApp1Provider1 = createAppWidgetProviderInfo(
@@ -114,16 +103,12 @@
                     .collect(Collectors.toList());
         }).when(manager).getInstalledProvidersForPackage(any(), eq(myUserHandle()));
 
-        // 2 widgets, app4/provider1 & app5/provider1, have already been added to the workspace.
-        mModelHelper.initializeData("widgets_predication_update_task_data");
-
+        LauncherLayoutBuilder builder = new LauncherLayoutBuilder()
+                .atWorkspace(0, 1, 2).putWidget("app4", "provider1", 1, 1)
+                .atWorkspace(0, 1, 3).putWidget("app5", "provider1", 1, 1);
+        mModelHelper.setupDefaultLayoutProvider(builder);
         MAIN_EXECUTOR.submit(() -> mModelHelper.getModel().addCallbacks(mCallback)).get();
-        MODEL_EXECUTOR.post(() -> mModelHelper.getBgDataModel().widgetsModel.update(
-                LauncherAppState.getInstance(mModelHelper.sandboxContext),
-                /* packageUser= */ null));
-
-        MODEL_EXECUTOR.submit(() -> { }).get();
-        MAIN_EXECUTOR.submit(() -> { }).get();
+        mModelHelper.loadModelSync();
     }
 
     @After
@@ -132,65 +117,72 @@
     }
 
     @Test
-    public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder()
-            throws Exception {
-        // WHEN newPredicationTask is executed with app predication of 5 apps.
-        AppTarget app1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1",
-                mUserHandle);
-        AppTarget app2 = new AppTarget(new AppTargetId("app2"), "app2", "provider1",
-                mUserHandle);
-        AppTarget app3 = new AppTarget(new AppTargetId("app3"), "app3", "className",
-                mUserHandle);
-        AppTarget app4 = new AppTarget(new AppTargetId("app4"), "app4", "provider1",
-                mUserHandle);
-        AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1",
-                mUserHandle);
-        mModelHelper.executeTaskForTest(
-                newWidgetsPredicationTask(List.of(app5, app3, app2, app4, app1)))
-                .forEach(Runnable::run);
+    public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder() {
+        // Run on model executor so that no other task runs in the middle.
+        runOnExecutorSync(MODEL_EXECUTOR, () -> {
+            // WHEN newPredicationTask is executed with app predication of 5 apps.
+            AppTarget app1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1",
+                    mUserHandle);
+            AppTarget app2 = new AppTarget(new AppTargetId("app2"), "app2", "provider1",
+                    mUserHandle);
+            AppTarget app3 = new AppTarget(new AppTargetId("app3"), "app3", "className",
+                    mUserHandle);
+            AppTarget app4 = new AppTarget(new AppTargetId("app4"), "app4", "provider1",
+                    mUserHandle);
+            AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1",
+                    mUserHandle);
+            mCallback.mRecommendedWidgets = null;
+            mModelHelper.getModel().enqueueModelUpdateTask(
+                    newWidgetsPredicationTask(List.of(app5, app3, app2, app4, app1)));
+            runOnExecutorSync(MAIN_EXECUTOR, () -> { });
 
-        // THEN only 2 widgets are returned because
-        // 1. app5/provider1 & app4/provider1 have already been added to workspace. They are
-        //    excluded from the result.
-        // 2. app3 doesn't have a widget.
-        // 3. only 1 widget is picked from app1 because we only want to promote one widget per app.
-        List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
-                .stream()
-                .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
-                .collect(Collectors.toList());
-        assertThat(recommendedWidgets).hasSize(2);
-        assertWidgetInfo(recommendedWidgets.get(0).info, mApp2Provider1);
-        assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1);
+            // THEN only 2 widgets are returned because
+            // 1. app5/provider1 & app4/provider1 have already been added to workspace. They are
+            //    excluded from the result.
+            // 2. app3 doesn't have a widget.
+            // 3. only 1 widget is picked from app1 because we only want to promote one widget per app.
+            List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
+                    .stream()
+                    .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
+                    .collect(Collectors.toList());
+            assertThat(recommendedWidgets).hasSize(2);
+            assertWidgetInfo(recommendedWidgets.get(0).info, mApp2Provider1);
+            assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1);
+        });
     }
 
     @Test
-    public void widgetsRecommendationRan_shouldReturnPackageWidgetsWhenEmpty()
-            throws Exception {
+    public void widgetsRecommendationRan_shouldReturnPackageWidgetsWhenEmpty() {
+        runOnExecutorSync(MODEL_EXECUTOR, () -> {
 
-        // Not installed widget
-        AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider3",
-                mUserHandle);
-        // Not installed app
-        AppTarget widget3 = new AppTarget(new AppTargetId("app2"), "app3", "provider1",
-                mUserHandle);
-        // Workspace added widgets
-        AppTarget widget4 = new AppTarget(new AppTargetId("app4"), "app4", "provider1",
-                mUserHandle);
-        AppTarget widget5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1",
-                mUserHandle);
-        mModelHelper.executeTaskForTest(
-                newWidgetsPredicationTask(List.of(widget5, widget3, widget4, widget1)))
-                .forEach(Runnable::run);
+            // Not installed widget
+            AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider3",
+                    mUserHandle);
+            // Not installed app
+            AppTarget widget3 = new AppTarget(new AppTargetId("app2"), "app3", "provider1",
+                    mUserHandle);
+            // Workspace added widgets
+            AppTarget widget4 = new AppTarget(new AppTargetId("app4"), "app4", "provider1",
+                    mUserHandle);
+            AppTarget widget5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1",
+                    mUserHandle);
 
-        // THEN only 2 widgets are returned because the launcher only filters out non-exist widgets.
-        List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
-                .stream()
-                .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
-                .collect(Collectors.toList());
-        assertThat(recommendedWidgets).hasSize(2);
-        // Another widget from the same package
-        assertWidgetInfo(recommendedWidgets.get(0).info, mApp4Provider2);
-        assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1);
+            mCallback.mRecommendedWidgets = null;
+            mModelHelper.getModel().enqueueModelUpdateTask(
+                    newWidgetsPredicationTask(List.of(widget5, widget3, widget4, widget1)));
+            runOnExecutorSync(MAIN_EXECUTOR, () -> { });
+
+            // THEN only 2 widgets are returned because the launcher only filters out
+            // non-exist widgets.
+            List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
+                    .stream()
+                    .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
+                    .collect(Collectors.toList());
+            assertThat(recommendedWidgets).hasSize(2);
+            // Another widget from the same package
+            assertWidgetInfo(recommendedWidgets.get(0).info, mApp4Provider2);
+            assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1);
+        });
     }
 
     private void assertWidgetInfo(
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
new file mode 100644
index 0000000..6c0d44d
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS;
+import static com.android.launcher3.taskbar.TaskbarHoverToolTipController.HOVER_TOOL_TIP_REVEAL_START_DELAY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Display;
+import android.view.MotionEvent;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.folder.Folder;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.util.ActivityContextWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+/**
+ * Tests for TaskbarHoverToolTipController.
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase {
+
+    private TaskbarHoverToolTipController mTaskbarHoverToolTipController;
+    private TestableLooper mTestableLooper;
+
+    @Mock private TaskbarView mTaskbarView;
+    @Mock private MotionEvent mMotionEvent;
+    @Mock private BubbleTextView mHoverBubbleTextView;
+    @Mock private FolderIcon mHoverFolderIcon;
+    @Mock private Display mDisplay;
+    @Mock private TaskbarDragLayer mTaskbarDragLayer;
+    private Folder mSpyFolderView;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        Context context = getApplicationContext();
+
+        doAnswer((Answer<Object>) invocation -> context.getSystemService(
+                (String) invocation.getArgument(0)))
+                .when(taskbarActivityContext).getSystemService(anyString());
+        when(taskbarActivityContext.getResources()).thenReturn(context.getResources());
+        when(taskbarActivityContext.getApplicationInfo()).thenReturn(
+                context.getApplicationInfo());
+        when(taskbarActivityContext.getDragLayer()).thenReturn(mTaskbarDragLayer);
+        when(taskbarActivityContext.getMainLooper()).thenReturn(context.getMainLooper());
+        when(taskbarActivityContext.getDisplay()).thenReturn(mDisplay);
+
+        when(mTaskbarDragLayer.getChildCount()).thenReturn(1);
+        mSpyFolderView = spy(new Folder(new ActivityContextWrapper(context), null));
+        when(mTaskbarDragLayer.getChildAt(anyInt())).thenReturn(mSpyFolderView);
+        doReturn(false).when(mSpyFolderView).isOpen();
+
+        when(mHoverBubbleTextView.getText()).thenReturn("tooltip");
+        doAnswer((Answer<Void>) invocation -> {
+            Object[] args = invocation.getArguments();
+            ((int[]) args[0])[0] = 0;
+            ((int[]) args[0])[1] = 0;
+            return null;
+        }).when(mHoverBubbleTextView).getLocationOnScreen(any(int[].class));
+        when(mHoverBubbleTextView.getWidth()).thenReturn(100);
+        when(mHoverBubbleTextView.getHeight()).thenReturn(100);
+
+        mHoverFolderIcon.mInfo = new FolderInfo();
+        mHoverFolderIcon.mInfo.title = "tooltip";
+        doAnswer((Answer<Void>) invocation -> {
+            Object[] args = invocation.getArguments();
+            ((int[]) args[0])[0] = 0;
+            ((int[]) args[0])[1] = 0;
+            return null;
+        }).when(mHoverFolderIcon).getLocationOnScreen(any(int[].class));
+        when(mHoverFolderIcon.getWidth()).thenReturn(100);
+        when(mHoverFolderIcon.getHeight()).thenReturn(100);
+
+        when(mTaskbarView.getTop()).thenReturn(200);
+
+        mTaskbarHoverToolTipController = new TaskbarHoverToolTipController(
+                taskbarActivityContext, mTaskbarView, mHoverBubbleTextView);
+        mTestableLooper = TestableLooper.get(this);
+    }
+
+    @Test
+    public void onHover_hoverEnterIcon_revealToolTip() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
+
+        // Verify fullscreen is not set until the delayed runnable to reveal the tooltip has run
+        verify(taskbarActivityContext, never()).setTaskbarWindowFullscreen(true);
+        waitForIdleSync();
+        assertThat(hoverHandled).isTrue();
+        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+                true);
+        verify(taskbarActivityContext).setTaskbarWindowFullscreen(true);
+    }
+
+    @Test
+    public void onHover_hoverExitIcon_closeToolTip() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
+        waitForIdleSync();
+
+        assertThat(hoverHandled).isTrue();
+        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+                false);
+    }
+
+    @Test
+    public void onHover_hoverEnterFolderIcon_revealToolTip() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
+
+        // Verify fullscreen is not set until the delayed runnable to reveal the tooltip has run
+        verify(taskbarActivityContext, never()).setTaskbarWindowFullscreen(true);
+        waitForIdleSync();
+        assertThat(hoverHandled).isTrue();
+        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+                true);
+        verify(taskbarActivityContext).setTaskbarWindowFullscreen(true);
+    }
+
+    @Test
+    public void onHover_hoverExitFolderIcon_closeToolTip() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
+        waitForIdleSync();
+
+        assertThat(hoverHandled).isTrue();
+        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+                false);
+    }
+
+    @Test
+    public void onHover_hoverExitFolderOpen_closeToolTip() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+        doReturn(true).when(mSpyFolderView).isOpen();
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
+        waitForIdleSync();
+
+        assertThat(hoverHandled).isTrue();
+        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+                false);
+    }
+
+    @Test
+    public void onHover_hoverEnterFolderOpen_noToolTip() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+        doReturn(true).when(mSpyFolderView).isOpen();
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
+
+        assertThat(hoverHandled).isFalse();
+    }
+
+    @Test
+    public void onHover_hoverMove_noUpdate() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_MOVE);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_MOVE);
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
+
+        assertThat(hoverHandled).isFalse();
+    }
+
+    private void waitForIdleSync() {
+        mTestableLooper.moveTimeForward(HOVER_TOOL_TIP_REVEAL_START_DELAY + 1);
+        mTestableLooper.processAllMessages();
+    }
+}
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
index 9622619..58be345 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
@@ -17,6 +17,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -26,12 +27,14 @@
 import android.os.Handler;
 import android.view.View;
 
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.quickstep.OverviewCommandHelper;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TouchInteractionService;
+import com.android.quickstep.util.AssistUtils;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -53,6 +56,8 @@
     @Mock
     Handler mockHandler;
     @Mock
+    AssistUtils mockAssistUtils;
+    @Mock
     StatsLogManager mockStatsLogManager;
     @Mock
     StatsLogManager.StatsLogger mockStatsLogger;
@@ -70,12 +75,15 @@
         MockitoAnnotations.initMocks(this);
         when(mockService.getDisplayId()).thenReturn(DISPLAY_ID);
         when(mockService.getOverviewCommandHelper()).thenReturn(mockCommandHelper);
+        when(mockService.getApplicationContext())
+                .thenReturn(InstrumentationRegistry.getInstrumentation().getTargetContext()
+                        .getApplicationContext());
         when(mockStatsLogManager.logger()).thenReturn(mockStatsLogger);
         when(mockTaskbarControllers.getTaskbarActivityContext())
                 .thenReturn(mockTaskbarActivityContext);
         doReturn(mockStatsLogManager).when(mockTaskbarActivityContext).getStatsLogManager();
         mNavButtonController = new TaskbarNavButtonController(mockService,
-                mockSystemUiProxy, mockHandler);
+                mockSystemUiProxy, mockHandler, mockAssistUtils);
     }
 
     @Test
@@ -104,16 +112,42 @@
     }
 
     @Test
-    public void testLongPressHome_enabled() {
+    public void testLongPressHome_enabled_withoutOverride() {
         mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/);
+        when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(false);
+
         mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView);
+        verify(mockAssistUtils, times(1)).tryStartAssistOverride(anyInt());
         verify(mockSystemUiProxy, times(1)).startAssistant(any());
     }
 
     @Test
-    public void testLongPressHome_disabled() {
-        mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/);
+    public void testLongPressHome_enabled_withOverride() {
+        mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/);
+        when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(true);
+
         mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView);
+        verify(mockAssistUtils, times(1)).tryStartAssistOverride(anyInt());
+        verify(mockSystemUiProxy, never()).startAssistant(any());
+    }
+
+    @Test
+    public void testLongPressHome_disabled_withoutOverride() {
+        mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/);
+        when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(false);
+
+        mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView);
+        verify(mockAssistUtils, never()).tryStartAssistOverride(anyInt());
+        verify(mockSystemUiProxy, never()).startAssistant(any());
+    }
+
+    @Test
+    public void testLongPressHome_disabled_withOverride() {
+        mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/);
+        when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(true);
+
+        mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView);
+        verify(mockAssistUtils, never()).tryStartAssistOverride(anyInt());
         verify(mockSystemUiProxy, never()).startAssistant(any());
     }
 
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
index 236b5db..3920b08 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
@@ -1,6 +1,9 @@
 package com.android.launcher3.taskbar.navbutton
 
 import android.content.res.Resources
+import android.view.Surface
+import android.view.Surface.ROTATION_270
+import android.view.Surface.Rotation
 import android.view.View
 import android.view.ViewGroup
 import android.widget.FrameLayout
@@ -32,6 +35,8 @@
     @Mock lateinit var mockRecentsButton: ImageView
     @Mock lateinit var mockHomeButton: ImageView
 
+    private var surfaceRotation = Surface.ROTATION_0
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
@@ -60,7 +65,8 @@
                 isKidsMode = true,
                 isInSetup = false,
                 isThreeButtonNav = false,
-                phoneMode = false
+                phoneMode = false,
+                surfaceRotation = surfaceRotation
             )
         assert(layoutter is KidsNavLayoutter)
     }
@@ -74,7 +80,8 @@
                 isKidsMode = false,
                 isInSetup = true,
                 isThreeButtonNav = false,
-                phoneMode = false
+                phoneMode = false,
+                surfaceRotation = surfaceRotation
             )
         assert(layoutter is SetupNavLayoutter)
     }
@@ -88,7 +95,8 @@
                 isKidsMode = false,
                 isInSetup = false,
                 isThreeButtonNav = false,
-                phoneMode = false
+                phoneMode = false,
+                surfaceRotation = surfaceRotation
             )
         assert(layoutter is TaskbarNavLayoutter)
     }
@@ -101,7 +109,8 @@
             isKidsMode = false,
             isInSetup = false,
             isThreeButtonNav = false,
-            phoneMode = false
+            phoneMode = false,
+            surfaceRotation = surfaceRotation
         )
     }
 
@@ -114,7 +123,8 @@
                 isKidsMode = false,
                 isInSetup = false,
                 isThreeButtonNav = true,
-                phoneMode = true
+                phoneMode = true,
+                surfaceRotation = surfaceRotation
             )
         assert(layoutter is PhonePortraitNavLayoutter)
     }
@@ -129,11 +139,28 @@
                 isKidsMode = false,
                 isInSetup = false,
                 isThreeButtonNav = true,
-                phoneMode = true
+                phoneMode = true,
+                surfaceRotation = surfaceRotation
             )
         assert(layoutter is PhoneLandscapeNavLayoutter)
     }
 
+    @Test
+    fun getTaskbarSeascapeLayoutter() {
+        assumeTrue(TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW)
+        mockDeviceProfile.isTaskbarPresent = false
+        setDeviceProfileLandscape()
+        val layoutter: NavButtonLayoutFactory.NavButtonLayoutter =
+                getLayoutter(
+                        isKidsMode = false,
+                        isInSetup = false,
+                        isThreeButtonNav = true,
+                        phoneMode = true,
+                        surfaceRotation = ROTATION_270
+                )
+        assert(layoutter is PhoneSeascapeNavLayoutter)
+    }
+
     @Test(expected = IllegalStateException::class)
     fun noValidLayoutForPhoneGestureNav() {
         assumeTrue(TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW)
@@ -142,7 +169,8 @@
             isKidsMode = false,
             isInSetup = false,
             isThreeButtonNav = false,
-            phoneMode = true
+            phoneMode = true,
+            surfaceRotation = surfaceRotation
         )
     }
 
@@ -157,7 +185,8 @@
         isKidsMode: Boolean,
         isInSetup: Boolean,
         isThreeButtonNav: Boolean,
-        phoneMode: Boolean
+        phoneMode: Boolean,
+        @Rotation surfaceRotation: Int
     ): NavButtonLayoutFactory.NavButtonLayoutter {
         return NavButtonLayoutFactory.getUiLayoutter(
             deviceProfile = mockDeviceProfile,
@@ -166,7 +195,8 @@
             isKidsMode = isKidsMode,
             isInSetup = isInSetup,
             isThreeButtonNav = isThreeButtonNav,
-            phoneMode = phoneMode
+            phoneMode = phoneMode,
+            surfaceRotation = surfaceRotation
         )
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
index 59f9d5f..3a5fb04 100644
--- a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
+++ b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
@@ -16,11 +16,17 @@
 
 package com.android.quickstep;
 
+import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
+
 import static org.junit.Assert.assertTrue;
 
 import android.os.SystemProperties;
 
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.Until;
+
 import com.android.launcher3.Launcher;
+import com.android.launcher3.tapl.LaunchedAppState;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.tapl.LauncherInstrumentation.ContainerType;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
@@ -76,6 +82,21 @@
         }
     }
 
+    protected void assertTestActivityIsRunning(int activityNumber, String message) {
+        assertTrue(message, mDevice.wait(
+                Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity" + activityNumber)),
+                DEFAULT_UI_TIMEOUT));
+    }
+
+    protected LaunchedAppState getAndAssertLaunchedApp() {
+        final LaunchedAppState launchedAppState = mLauncher.getLaunchedAppState();
+        executeOnLauncher(launcher -> assertTrue(
+                "Launcher activity is the top activity; expecting another activity to be the top "
+                        + "one",
+                isInLaunchedApp(launcher)));
+        return launchedAppState;
+    }
+
     private boolean isInLiveTileMode(Launcher launcher,
             LauncherInstrumentation.ContainerType expectedContainerType) {
         if (expectedContainerType != LauncherInstrumentation.ContainerType.OVERVIEW) {
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index 97e34c5..a67d787 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -116,12 +116,13 @@
             Utilities.enableRunningInTestHarnessForTests();
         }
 
-        final ViewCaptureRule viewCaptureRule = new ViewCaptureRule();
+        final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(
+                RecentsActivity.ACTIVITY_TRACKER::getCreatedActivity);
         mOrderSensitiveRules = RuleChain
                 .outerRule(new SamplerRule())
                 .around(new NavigationModeSwitchRule(mLauncher))
-                .around(viewCaptureRule)
-                .around(new FailureWatcher(mDevice, mLauncher, viewCaptureRule.getViewCapture()));
+                .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
+                .around(viewCaptureRule);
 
         mOtherLauncherActivity = context.getPackageManager().queryIntentActivities(
                 getHomeIntentInPackage(context),
diff --git a/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
index a9dc043..bb1afdf 100644
--- a/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep
 
+import android.content.Context
 import android.graphics.Rect
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -29,7 +30,9 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
 
 /** Test for FullscreenDrawParams class. */
 @SmallTest
@@ -186,4 +189,76 @@
         val expectedRadius = QuickStepContract.getWindowCornerRadius(context)
         assertThat(params.mCurrentDrawnCornerRadius).isEqualTo(expectedRadius)
     }
+
+    @Test
+    fun setStartProgress_correctCornerRadiusForMultiDisplay() {
+        val display1Context = context
+        val display2Context = mock(Context::class.java)
+        val spyParams = spy(params)
+
+        val display1TaskRadius = TaskCornerRadius.get(display1Context)
+        val display1WindowRadius = QuickStepContract.getWindowCornerRadius(display1Context)
+        val display2TaskRadius = display1TaskRadius * 2 + 1 // Arbitrarily different.
+        val display2WindowRadius = display1WindowRadius * 2 + 1 // Arbitrarily different.
+        doReturn(display2TaskRadius).`when`(spyParams).computeTaskCornerRadius(display2Context)
+        doReturn(display2WindowRadius).`when`(spyParams).computeWindowCornerRadius(display2Context)
+
+        spyParams.updateCornerRadius(display1Context)
+        spyParams.setProgress(
+            /* fullscreenProgress= */ 0f,
+            /* parentScale= */ 1.0f,
+            /* taskViewScale= */ 1.0f,
+            /* unused previewWidth= */ -1,
+            /* unusedDp= */ null,
+            /* unused previewPositionHelper= */ null
+        )
+        assertThat(spyParams.mCurrentDrawnCornerRadius).isEqualTo(display1TaskRadius)
+
+        spyParams.updateCornerRadius(display2Context)
+        spyParams.setProgress(
+            /* fullscreenProgress= */ 0f,
+            /* parentScale= */ 1.0f,
+            /* taskViewScale= */ 1.0f,
+            /* unused previewWidth= */ -1,
+            /* unusedDp= */ null,
+            /* unused previewPositionHelper= */ null
+        )
+        assertThat(spyParams.mCurrentDrawnCornerRadius).isEqualTo(display2TaskRadius)
+    }
+
+    @Test
+    fun setFullProgress_correctCornerRadiusForMultiDisplay() {
+        val display1Context = context
+        val display2Context = mock(Context::class.java)
+        val spyParams = spy(params)
+
+        val display1TaskRadius = TaskCornerRadius.get(display1Context)
+        val display1WindowRadius = QuickStepContract.getWindowCornerRadius(display1Context)
+        val display2TaskRadius = display1TaskRadius * 2 + 1 // Arbitrarily different.
+        val display2WindowRadius = display1WindowRadius * 2 + 1 // Arbitrarily different.
+        doReturn(display2TaskRadius).`when`(spyParams).computeTaskCornerRadius(display2Context)
+        doReturn(display2WindowRadius).`when`(spyParams).computeWindowCornerRadius(display2Context)
+
+        spyParams.updateCornerRadius(display1Context)
+        spyParams.setProgress(
+            /* fullscreenProgress= */ 1.0f,
+            /* parentScale= */ 1.0f,
+            /* taskViewScale= */ 1.0f,
+            /* unused previewWidth= */ -1,
+            /* unusedDp= */ null,
+            /* unused previewPositionHelper= */ null
+        )
+        assertThat(spyParams.mCurrentDrawnCornerRadius).isEqualTo(display1WindowRadius)
+
+        spyParams.updateCornerRadius(display2Context)
+        spyParams.setProgress(
+            /* fullscreenProgress= */ 1.0f,
+            /* parentScale= */ 1.0f,
+            /* taskViewScale= */ 1.0f,
+            /* unused previewWidth= */ -1,
+            /* unusedDp= */ null,
+            /* unused previewPositionHelper= */ null
+        )
+        assertThat(spyParams.mCurrentDrawnCornerRadius).isEqualTo(display2WindowRadius)
+    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
index 9c240f0..298dd6c 100644
--- a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
+++ b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
@@ -53,6 +53,8 @@
 import org.junit.runner.RunWith;
 import org.mockito.MockitoAnnotations;
 
+import java.util.List;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class OrientationTouchTransformerTest {
@@ -296,7 +298,7 @@
         WindowManagerProxy wmProxy = mock(WindowManagerProxy.class);
         doReturn(cachedDisplayInfo).when(wmProxy).getDisplayInfo(any());
         doReturn(windowBounds).when(wmProxy).getRealBounds(any(), any());
-        ArrayMap<CachedDisplayInfo, WindowBounds[]> internalDisplayBounds = new ArrayMap<>();
+        ArrayMap<CachedDisplayInfo, List<WindowBounds>> internalDisplayBounds = new ArrayMap<>();
         doReturn(internalDisplayBounds).when(wmProxy).estimateInternalDisplayBounds(any());
         return new DisplayController.Info(
                 getApplicationContext(), wmProxy, new ArrayMap<>());
diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
index df5303f..20aa410 100644
--- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
@@ -16,15 +16,17 @@
 
 package com.android.quickstep;
 
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
+
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.ui.TaplTestsLauncher3;
-import com.android.launcher3.util.RaceConditionReproducer;
+import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -45,21 +47,9 @@
         startTestActivity(2);
     }
 
-    private void runTest(String... eventSequence) {
-        final RaceConditionReproducer eventProcessor = new RaceConditionReproducer(eventSequence);
-
-        // Destroy Launcher activity.
-        closeLauncherActivity();
-
-        // The test action.
-        eventProcessor.startIteration();
-        mLauncher.goHome();
-        eventProcessor.finishIteration();
-    }
-
-    @Ignore
     @Test
     @NavigationModeSwitch
+    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/187761685
     public void testStressPressHome() {
         for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
             // Destroy Launcher activity.
@@ -70,9 +60,9 @@
         }
     }
 
-    @Ignore
     @Test
     @NavigationModeSwitch
+    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/187761685
     public void testStressSwipeToOverview() {
         for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
             // Destroy Launcher activity.
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 32eadce..25f90ca 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -16,7 +16,10 @@
 
 package com.android.quickstep;
 
+import static com.android.launcher3.testing.shared.TestProtocol.FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP;
 import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT;
 
 import static org.junit.Assert.assertEquals;
@@ -27,6 +30,7 @@
 
 import android.content.Intent;
 import android.platform.test.annotations.PlatinumTest;
+import android.util.Log;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -42,9 +46,13 @@
 import com.android.launcher3.tapl.Overview;
 import com.android.launcher3.tapl.OverviewActions;
 import com.android.launcher3.tapl.OverviewTask;
+import com.android.launcher3.tapl.OverviewTaskMenu;
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.ui.TaplTestsLauncher3;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
+import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
 import com.android.quickstep.views.RecentsView;
@@ -143,7 +151,7 @@
         assertNotNull("overview.getCurrentTask() returned null (1)", task);
         assertNotNull("OverviewTask.open returned null", task.open());
         assertTrue("Test activity didn't open from Overview", mDevice.wait(Until.hasObject(
-                By.pkg(getAppPackageName()).text("TestActivity2")),
+                        By.pkg(getAppPackageName()).text("TestActivity2")),
                 DEFAULT_UI_TIMEOUT));
         executeOnLauncher(launcher -> assertTrue(
                 "Launcher activity is the top activity; expecting another activity to be the top "
@@ -177,7 +185,6 @@
     @Test
     @NavigationModeSwitch
     @PortraitLandscape
-    @ScreenRecord // b/195673272
     @PlatinumTest(focusArea = "launcher")
     public void testOverviewActions() throws Exception {
         // Experimenting for b/165029151:
@@ -192,6 +199,19 @@
         actionsView.clickAndDismissScreenshot();
     }
 
+
+    @Test
+    public void testOverviewActionsMenu() throws Exception {
+        startTestAppsWithCheck();
+
+        OverviewTaskMenu menu = mLauncher.goHome().switchToOverview().getCurrentTask().tapMenu();
+
+        assertNotNull("Tapping App info menu item returned null", menu.tapAppInfoMenuItem());
+        executeOnLauncher(launcher -> assertTrue(
+                "Launcher activity is the top activity; expecting another activity to be the top",
+                isInLaunchedApp(launcher)));
+    }
+
     private int getCurrentOverviewPage(Launcher launcher) {
         return launcher.<RecentsView>getOverviewPanel().getCurrentPage();
     }
@@ -237,16 +257,6 @@
                 isInState(() -> LauncherState.OVERVIEW));
     }
 
-    private LaunchedAppState getAndAssertLaunchedApp() {
-        final LaunchedAppState launchedAppState = mLauncher.getLaunchedAppState();
-        assertNotNull("Launcher.getLaunchedApp() returned null", launchedAppState);
-        executeOnLauncher(launcher -> assertTrue(
-                "Launcher activity is the top activity; expecting another activity to be the top "
-                        + "one",
-                isInLaunchedApp(launcher)));
-        return launchedAppState;
-    }
-
     private void quickSwitchToPreviousAppAndAssert(boolean toRight) {
         final LaunchedAppState launchedAppState = getAndAssertLaunchedApp();
         if (toRight) {
@@ -288,22 +298,22 @@
         startTestActivity(4);
 
         quickSwitchToPreviousAppAndAssert(true /* toRight */);
-        assertTrue("The first app we should have quick switched to is not running",
-                isTestActivityRunning(3));
+        assertTestActivityIsRunning(3,
+                "The first app we should have quick switched to is not running");
 
         quickSwitchToPreviousAppAndAssert(true /* toRight */);
         if (mLauncher.getNavigationModel() == NavigationModel.THREE_BUTTON) {
             // 3-button mode toggles between 2 apps, rather than going back further.
-            assertTrue("Second quick switch should have returned to the first app.",
-                    isTestActivityRunning(4));
+            assertTestActivityIsRunning(4,
+                    "Second quick switch should have returned to the first app.");
         } else {
-            assertTrue("The second app we should have quick switched to is not running",
-                    isTestActivityRunning(2));
+            assertTestActivityIsRunning(2,
+                    "The second app we should have quick switched to is not running");
         }
 
         quickSwitchToPreviousAppAndAssert(false /* toRight */);
-        assertTrue("The 2nd app we should have quick switched to is not running",
-                isTestActivityRunning(3));
+        assertTestActivityIsRunning(3,
+                "The 2nd app we should have quick switched to is not running");
 
         final LaunchedAppState launchedAppState = getAndAssertLaunchedApp();
         launchedAppState.switchToOverview();
@@ -312,7 +322,7 @@
     @Test
     @ScreenRecord // b/242163205
     @PlatinumTest(focusArea = "launcher")
-    @TaskbarModeSwitch(mode = PERSISTENT)
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/286084688
     public void testQuickSwitchToPreviousAppForTablet() throws Exception {
         assumeTrue(mLauncher.isTablet());
         startTestActivity(2);
@@ -328,17 +338,20 @@
         // Quick-switch to the test app with swiping to right.
         quickSwitchToPreviousAppAndAssert(true /* toRight */);
 
-        assertTrue("The first app we should have quick switched to is not running",
-                isTestActivityRunning(2));
+        assertTestActivityIsRunning(2,
+                "The first app we should have quick switched to is not running");
         // Expect task bar visible when the launched app was the test activity.
         launchedAppState = getAndAssertLaunchedApp();
-        launchedAppState.assertTaskbarVisible();
-    }
 
-    private boolean isTestActivityRunning(int activityNumber) {
-        return mDevice.wait(Until.hasObject(By.pkg(getAppPackageName())
-                        .text("TestActivity" + activityNumber)),
-                DEFAULT_UI_TIMEOUT);
+        Log.e(FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP,
+                "is Taskbar Transient : " + DisplayController.isTransientTaskbar(mTargetContext));
+        // TODO(b/286084688): Remove this branching check after test corruption is resolved.
+        // Branching this check because of test corruption.
+        if (DisplayController.isTransientTaskbar(mTargetContext)) {
+            launchedAppState.assertTaskbarHidden();
+        } else {
+            launchedAppState.assertTaskbarVisible();
+        }
     }
 
     @Test
@@ -348,8 +361,8 @@
     public void testQuickSwitchFromHome() throws Exception {
         startTestActivity(2);
         mLauncher.goHome().quickSwitchToPreviousApp();
-        assertTrue("The most recent task is not running after quick switching from home",
-                isTestActivityRunning(2));
+        assertTestActivityIsRunning(2,
+                "The most recent task is not running after quick switching from home");
         getAndAssertLaunchedApp();
     }
 
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
index e8cadab..92b598b 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
@@ -15,25 +15,41 @@
  */
 package com.android.quickstep;
 
+
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
 import android.content.Intent;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.ui.TaplTestsLauncher3;
+import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
+@LargeTest
+@RunWith(AndroidJUnit4.class)
 public class TaplTestsSplitscreen extends AbstractQuickStepTest {
     private static final String CALCULATOR_APP_NAME = "Calculator";
     private static final String CALCULATOR_APP_PACKAGE =
             resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
 
+    private static final String READ_DEVICE_CONFIG_PERMISSION =
+            "android.permission.READ_DEVICE_CONFIG";
+
     @Override
     @Before
     public void setUp() throws Exception {
@@ -44,6 +60,8 @@
             mLauncher.enableBlockTimeout(true);
             mLauncher.showTaskbarIfHidden();
         }
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                READ_DEVICE_CONFIG_PERMISSION);
     }
 
     @After
@@ -62,6 +80,7 @@
     @Test
     @PortraitLandscape
     @TaskbarModeSwitch
+    @TestStabilityRule.Stability(flavors = PLATFORM_POSTSUBMIT | LOCAL) // b/295225524
     public void testSplitAppFromHomeWithItself() throws Exception {
         // Currently only tablets have Taskbar in Overview, so test is only active on tablets
         assumeTrue(mLauncher.isTablet());
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
index 40be480..4ff2f9c 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
@@ -20,6 +20,7 @@
 
 import androidx.test.filters.LargeTest;
 
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 
 import org.junit.Test;
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
new file mode 100644
index 0000000..907dbcc
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.Instrumentation;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.tapl.LauncherInstrumentation.TrackpadGestureType;
+import com.android.launcher3.tapl.Workspace;
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
+import com.android.launcher3.ui.TaplTestsLauncher3;
+import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class TaplTestsTrackpad extends AbstractQuickStepTest {
+
+    private static final String READ_DEVICE_CONFIG_PERMISSION =
+            "android.permission.READ_DEVICE_CONFIG";
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        TaplTestsLauncher3.initialize(this);
+    }
+
+    @After
+    public void tearDown() {
+        mLauncher.setTrackpadGestureType(TrackpadGestureType.NONE);
+    }
+
+    @Test
+    @PortraitLandscape
+    @NavigationModeSwitch
+    public void goHome() throws Exception {
+        assumeTrue(mLauncher.isTablet());
+
+        mLauncher.setTrackpadGestureType(TrackpadGestureType.THREE_FINGER);
+        startTestActivity(2);
+        mLauncher.goHome();
+    }
+
+    @Test
+    @PortraitLandscape
+    // TODO(b/291944684): Support back in 3-button mode. It requires triggering the logic to enable
+    //  trackpad gesture back in SysUI. Normally it's triggered by the attachment of a trackpad. We
+    //  need to figure out a way to emulate that in the test, or bypass the logic altogether.
+    @NavigationModeSwitch(mode = ZERO_BUTTON)
+    public void pressBack() throws Exception {
+        assumeTrue(mLauncher.isTablet());
+        assumeFalse(FeatureFlags.ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION.get());
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+
+        try {
+            instrumentation.getUiAutomation().adoptShellPermissionIdentity(
+                    READ_DEVICE_CONFIG_PERMISSION);
+            mLauncher.setTrackpadGestureType(TrackpadGestureType.THREE_FINGER);
+
+            startTestActivity(2);
+            mLauncher.pressBack();
+        } finally {
+            instrumentation.getUiAutomation().dropShellPermissionIdentity();
+        }
+    }
+
+    @Test
+    @PortraitLandscape
+    @NavigationModeSwitch
+    public void switchToOverview() throws Exception {
+        assumeTrue(mLauncher.isTablet());
+
+        mLauncher.setTrackpadGestureType(TrackpadGestureType.THREE_FINGER);
+        startTestActivity(2);
+        mLauncher.goHome().switchToOverview();
+    }
+
+    @Test
+    @PortraitLandscape
+    @NavigationModeSwitch
+    public void testAllAppsFromHome() throws Exception {
+        assumeTrue(mLauncher.isTablet());
+
+        mLauncher.setTrackpadGestureType(TrackpadGestureType.TWO_FINGER);
+        assertNotNull("switchToAllApps() returned null",
+                mLauncher.getWorkspace().switchToAllApps());
+    }
+
+    @Test
+    @NavigationModeSwitch
+    @PortraitLandscape
+    public void testQuickSwitchFromHome() throws Exception {
+        assumeTrue(mLauncher.isTablet());
+
+        startTestActivity(2);
+        Workspace workspace = mLauncher.goHome();
+        mLauncher.setTrackpadGestureType(TrackpadGestureType.FOUR_FINGER);
+        workspace.quickSwitchToPreviousApp();
+        assertTestActivityIsRunning(2,
+                "The most recent task is not running after quick switching from home");
+        getAndAssertLaunchedApp();
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
index b58fe29..3869bf7 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
@@ -18,7 +18,6 @@
 import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT;
 
-
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -64,4 +63,15 @@
             throw new RuntimeException(e);
         }
     }
+
+    @Test
+    @TaskbarModeSwitch(mode = TRANSIENT)
+    public void testClickHoveredTaskbarToGoHome() {
+        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_CURSOR_HOVER_STATES, true)) {
+            getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE);
+            mLauncher.getLaunchedAppState().clickStashedTaskbarToGoHome();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
index 7e408a8..8cc8487 100644
--- a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
+++ b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
@@ -17,8 +17,8 @@
 
 import static androidx.test.InstrumentationRegistry.getContext;
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
-import static androidx.test.InstrumentationRegistry.getTargetContext;
 
+import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID;
 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.util.WidgetUtils.createWidgetInfo;
@@ -32,7 +32,6 @@
 import static org.mockito.Mockito.spy;
 
 import android.appwidget.AppWidgetManager;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
@@ -42,14 +41,16 @@
 import android.view.ViewConfiguration;
 import android.widget.RemoteViews;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.Suppress;
-import androidx.test.runner.AndroidJUnit4;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.Until;
 
-import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.celllayout.FavoriteItemsTransaction;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.tapl.LaunchedAppState;
 import com.android.launcher3.testcomponent.ListViewService;
@@ -57,6 +58,7 @@
 import com.android.launcher3.testcomponent.TestCommandReceiver;
 import com.android.launcher3.ui.TaplTestsLauncher3;
 import com.android.launcher3.ui.TestViewHelpers;
+import com.android.launcher3.util.Executors;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 
@@ -67,6 +69,7 @@
 import org.mockito.stubbing.Answer;
 
 import java.lang.reflect.Field;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.IntConsumer;
 
 /**
@@ -84,9 +87,9 @@
 @RunWith(AndroidJUnit4.class)
 public class ViewInflationDuringSwipeUp extends AbstractQuickStepTest {
 
-    private ContentResolver mResolver;
     private SparseArray<ViewConfiguration> mConfigMap;
     private InitTracker mInitTracker;
+    private LauncherModel mModel;
 
     @Before
     public void setUp() throws Exception {
@@ -101,8 +104,8 @@
 
         TaplTestsLauncher3.initialize(this);
 
-        mResolver = mTargetContext.getContentResolver();
-        LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
+        mModel = LauncherAppState.getInstance(mTargetContext).getModel();
+        Executors.MODEL_EXECUTOR.submit(mModel.getModelDbController()::createEmptyDB).get();
 
         // Get static configuration map
         Field field = ViewConfiguration.class.getDeclaredField("sConfigurations");
@@ -182,26 +185,30 @@
     private void executeSwipeUpTestWithWidget(IntConsumer widgetIdCreationCallback,
             IntConsumer updateBeforeSwipeUp, String finalWidgetText) {
         try {
-            // Clear all existing data
-            LauncherSettings.Settings.call(mResolver,
-                    LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
-            LauncherSettings.Settings.call(mResolver,
-                    LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
-            LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
+            LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(false);
+
             // Make sure the widget is big enough to show a list of items
             info.minSpanX = 2;
             info.minSpanY = 2;
             info.spanX = 2;
             info.spanY = 2;
-            LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);
+            AtomicInteger widgetId = new AtomicInteger();
+            new FavoriteItemsTransaction(mTargetContext)
+                    .addItem(() -> {
+                        LauncherAppWidgetInfo item = createWidgetInfo(info, mTargetContext, true);
+                        item.screenId = FIRST_SCREEN_ID;
+                        widgetId.set(item.appWidgetId);
+                        return item;
+                    })
+                    .commitAndLoadHome(mLauncher);
 
-            addItemToScreen(item);
+
+
             assertTrue("Widget is not present",
                     mLauncher.goHome().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT) != null);
-            int widgetId = item.appWidgetId;
 
             // Verify widget id
-            widgetIdCreationCallback.accept(widgetId);
+            widgetIdCreationCallback.accept(widgetId.get());
 
             // Go to overview once so that all views are initialized and cached
             startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
@@ -214,7 +221,7 @@
             LaunchedAppState launchedAppState = mLauncher.getLaunchedAppState();
 
             // Update widget
-            updateBeforeSwipeUp.accept(widgetId);
+            updateBeforeSwipeUp.accept(widgetId.get());
 
             launchedAppState.switchToOverview();
             assertEquals("Views inflated during swipe up", 0, mInitTracker.viewInitCount);
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt
new file mode 100644
index 0000000..fc767fa
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt
@@ -0,0 +1,395 @@
+/*
+ *  Copyright (C) 2023 The Android Open Source Project
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS 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.app.ActivityManager.RunningTaskInfo
+import android.app.ActivityTaskManager.INVALID_TASK_ID
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.Intent
+import android.os.UserHandle
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.ui.AbstractLauncherUiTest
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
+import com.android.quickstep.util.SplitSelectDataHolder.Companion.SPLIT_PENDINGINTENT_PENDINGINTENT
+import com.android.quickstep.util.SplitSelectDataHolder.Companion.SPLIT_PENDINGINTENT_TASK
+import com.android.quickstep.util.SplitSelectDataHolder.Companion.SPLIT_SHORTCUT_TASK
+import com.android.quickstep.util.SplitSelectDataHolder.Companion.SPLIT_SINGLE_INTENT_FULLSCREEN
+import com.android.quickstep.util.SplitSelectDataHolder.Companion.SPLIT_SINGLE_SHORTCUT_FULLSCREEN
+import com.android.quickstep.util.SplitSelectDataHolder.Companion.SPLIT_SINGLE_TASK_FULLSCREEN
+import com.android.quickstep.util.SplitSelectDataHolder.Companion.SPLIT_TASK_PENDINGINTENT
+import com.android.quickstep.util.SplitSelectDataHolder.Companion.SPLIT_TASK_SHORTCUT
+import com.android.quickstep.util.SplitSelectDataHolder.Companion.SPLIT_TASK_TASK
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+class SplitSelectDataHolderTest {
+    private lateinit var splitSelectDataHolder: SplitSelectDataHolder
+
+    private val context: Context =
+        ContextWrapper(InstrumentationRegistry.getInstrumentation().targetContext)
+    private val sampleTaskInfo = RunningTaskInfo()
+    private val sampleTaskId = 10
+    private val sampleTaskId2 = 11
+    private val sampleUser = UserHandle(0)
+    private val sampleIntent = Intent()
+    private val sampleIntent2 = Intent()
+    private val sampleShortcut = Intent()
+    private val sampleShortcut2 = Intent()
+    private val sampleItemInfo = ItemInfo()
+    private val samplePackage =
+        AbstractLauncherUiTest.resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)
+
+    @Before
+    fun setup() {
+        splitSelectDataHolder = SplitSelectDataHolder(context)
+
+        sampleTaskInfo.taskId = sampleTaskId
+        sampleItemInfo.user = sampleUser
+        sampleIntent.setPackage(samplePackage)
+        sampleIntent2.setPackage(samplePackage)
+        sampleShortcut.setPackage(samplePackage)
+        sampleShortcut2.setPackage(samplePackage)
+        sampleShortcut.putExtra(ShortcutKey.EXTRA_SHORTCUT_ID, "sampleShortcut")
+        sampleShortcut2.putExtra(ShortcutKey.EXTRA_SHORTCUT_ID, "sampleShortcut2")
+    }
+
+    @Test
+    fun setInitialAsTask() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleTaskInfo,
+            STAGE_POSITION_TOP_OR_LEFT,
+            null,
+            null
+        )
+        assertTrue(splitSelectDataHolder.isSplitSelectActive())
+    }
+
+    @Test
+    fun setInitialAsIntent() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleIntent,
+            STAGE_POSITION_TOP_OR_LEFT,
+            sampleItemInfo,
+            null,
+            INVALID_TASK_ID
+        )
+        assertTrue(splitSelectDataHolder.isSplitSelectActive())
+    }
+
+    @Test
+    fun setInitialAsIntentWithAlreadyRunningTask() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleIntent,
+            STAGE_POSITION_TOP_OR_LEFT,
+            sampleItemInfo,
+            null,
+            sampleTaskId
+        )
+        assertTrue(splitSelectDataHolder.isSplitSelectActive())
+    }
+
+    @Test
+    fun setInitialAsShortcut() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleShortcut,
+            STAGE_POSITION_TOP_OR_LEFT,
+            sampleItemInfo,
+            null,
+            INVALID_TASK_ID
+        )
+        assertTrue(splitSelectDataHolder.isSplitSelectActive())
+    }
+
+    @Test
+    fun setSecondAsTask() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleShortcut,
+            STAGE_POSITION_TOP_OR_LEFT,
+            sampleItemInfo,
+            null,
+            INVALID_TASK_ID
+        )
+        splitSelectDataHolder.setSecondTask(sampleTaskId)
+        assertTrue(splitSelectDataHolder.isBothSplitAppsConfirmed())
+    }
+
+    @Test
+    fun setSecondAsIntent() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleTaskInfo,
+            STAGE_POSITION_TOP_OR_LEFT,
+            null,
+            null
+        )
+        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser)
+        assertTrue(splitSelectDataHolder.isBothSplitAppsConfirmed())
+    }
+
+    @Test
+    fun setSecondAsShortcut() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleIntent,
+            STAGE_POSITION_TOP_OR_LEFT,
+            sampleItemInfo,
+            null,
+            INVALID_TASK_ID
+        )
+        splitSelectDataHolder.setSecondTask(sampleShortcut, sampleUser)
+        assertTrue(splitSelectDataHolder.isBothSplitAppsConfirmed())
+    }
+
+    @Test
+    fun generateLaunchData_Task_Task() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleTaskInfo,
+            STAGE_POSITION_TOP_OR_LEFT,
+            sampleItemInfo,
+            null
+        )
+        splitSelectDataHolder.setSecondTask(sampleTaskId2)
+        val launchData = splitSelectDataHolder.getSplitLaunchData()
+
+        assertEquals(launchData.splitLaunchType, SPLIT_TASK_TASK)
+
+        // should contain a valid task ID for first app, and no intent or shortcut
+        assertNotEquals(launchData.initialTaskId, INVALID_TASK_ID)
+        assertNull(launchData.initialPendingIntent)
+        assertNull(launchData.initialShortcut)
+
+        // should contain a valid task ID for second app, and no intent or shortcut
+        assertNotEquals(launchData.secondTaskId, INVALID_TASK_ID)
+        assertNull(launchData.secondPendingIntent)
+        assertNull(launchData.secondShortcut)
+    }
+
+    @Test
+    fun generateLaunchData_Task_Intent() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleTaskInfo,
+            STAGE_POSITION_TOP_OR_LEFT,
+            sampleItemInfo,
+            null
+        )
+        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser)
+        val launchData = splitSelectDataHolder.getSplitLaunchData()
+
+        assertEquals(launchData.splitLaunchType, SPLIT_TASK_PENDINGINTENT)
+
+        // should contain a valid task ID for first app, and no intent or shortcut
+        assertNotEquals(launchData.initialTaskId, INVALID_TASK_ID)
+        assertNull(launchData.initialPendingIntent)
+        assertNull(launchData.initialShortcut)
+
+        // should contain a valid intent for second app, and no task ID or shortcut
+        assertNotNull(launchData.secondPendingIntent)
+        assertEquals(launchData.secondTaskId, INVALID_TASK_ID)
+        assertNull(launchData.secondShortcut)
+    }
+
+    @Test
+    fun generateLaunchData_Task_Shortcut() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleTaskInfo,
+            STAGE_POSITION_TOP_OR_LEFT,
+            sampleItemInfo,
+            null
+        )
+        splitSelectDataHolder.setSecondTask(sampleShortcut, sampleUser)
+        val launchData = splitSelectDataHolder.getSplitLaunchData()
+
+        assertEquals(launchData.splitLaunchType, SPLIT_TASK_SHORTCUT)
+
+        // should contain a valid task ID for first app, and no intent or shortcut
+        assertNotEquals(launchData.initialTaskId, INVALID_TASK_ID)
+        assertNull(launchData.initialPendingIntent)
+        assertNull(launchData.initialShortcut)
+
+        // should contain a valid shortcut and intent for second app, and no task ID
+        assertNotNull(launchData.secondShortcut)
+        assertNotNull(launchData.secondPendingIntent)
+        assertEquals(launchData.secondTaskId, INVALID_TASK_ID)
+    }
+
+    @Test
+    fun generateLaunchData_Intent_Task() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleIntent,
+            STAGE_POSITION_TOP_OR_LEFT,
+            sampleItemInfo,
+            null,
+            INVALID_TASK_ID
+        )
+        splitSelectDataHolder.setSecondTask(sampleTaskId)
+        val launchData = splitSelectDataHolder.getSplitLaunchData()
+
+        assertEquals(launchData.splitLaunchType, SPLIT_PENDINGINTENT_TASK)
+
+        // should contain a valid intent for first app, and no task ID or shortcut
+        assertNotNull(launchData.initialPendingIntent)
+        assertEquals(launchData.initialTaskId, INVALID_TASK_ID)
+        assertNull(launchData.initialShortcut)
+
+        // should contain a valid task ID for second app, and no intent or shortcut
+        assertNotEquals(launchData.secondTaskId, INVALID_TASK_ID)
+        assertNull(launchData.secondPendingIntent)
+        assertNull(launchData.secondShortcut)
+    }
+
+    @Test
+    fun generateLaunchData_Shortcut_Task() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleShortcut,
+            STAGE_POSITION_TOP_OR_LEFT,
+            sampleItemInfo,
+            null,
+            INVALID_TASK_ID
+        )
+        splitSelectDataHolder.setSecondTask(sampleTaskId)
+        val launchData = splitSelectDataHolder.getSplitLaunchData()
+
+        assertEquals(launchData.splitLaunchType, SPLIT_SHORTCUT_TASK)
+
+        // should contain a valid shortcut and intent for first app, and no task ID
+        assertNotNull(launchData.initialShortcut)
+        assertNotNull(launchData.initialPendingIntent)
+        assertEquals(launchData.initialTaskId, INVALID_TASK_ID)
+
+        // should contain a valid task ID for second app, and no intent or shortcut
+        assertNotEquals(launchData.secondTaskId, INVALID_TASK_ID)
+        assertNull(launchData.secondPendingIntent)
+        assertNull(launchData.secondShortcut)
+    }
+
+    @Test
+    fun generateLaunchData_Intent_Intent() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleIntent,
+            STAGE_POSITION_TOP_OR_LEFT,
+            sampleItemInfo,
+            null,
+            INVALID_TASK_ID
+        )
+        splitSelectDataHolder.setSecondTask(sampleIntent2, sampleUser)
+        val launchData = splitSelectDataHolder.getSplitLaunchData()
+
+        assertEquals(launchData.splitLaunchType, SPLIT_PENDINGINTENT_PENDINGINTENT)
+
+        // should contain a valid intent for first app, and no task ID or shortcut
+        assertNotNull(launchData.initialPendingIntent)
+        assertEquals(launchData.initialTaskId, INVALID_TASK_ID)
+        assertNull(launchData.initialShortcut)
+
+        // should contain a valid intent for second app, and no task ID or shortcut
+        assertNotNull(launchData.secondPendingIntent)
+        assertEquals(launchData.secondTaskId, INVALID_TASK_ID)
+        assertNull(launchData.secondShortcut)
+    }
+
+    @Test
+    fun generateLaunchData_Single_Task() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleTaskInfo,
+            STAGE_POSITION_TOP_OR_LEFT,
+            sampleItemInfo,
+            null
+        )
+        val launchData = splitSelectDataHolder.getFullscreenLaunchData()
+
+        assertEquals(launchData.splitLaunchType, SPLIT_SINGLE_TASK_FULLSCREEN)
+
+        // should contain a valid task ID for first app, and no intent or shortcut
+        assertNotEquals(launchData.initialTaskId, INVALID_TASK_ID)
+        assertNull(launchData.initialPendingIntent)
+        assertNull(launchData.initialShortcut)
+
+        // should contain no task ID, intent, or shortcut for second app
+        assertEquals(launchData.secondTaskId, INVALID_TASK_ID)
+        assertNull(launchData.secondPendingIntent)
+        assertNull(launchData.secondShortcut)
+    }
+
+    @Test
+    fun generateLaunchData_Single_Intent() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleIntent,
+            STAGE_POSITION_TOP_OR_LEFT,
+            sampleItemInfo,
+            null,
+            INVALID_TASK_ID
+        )
+        val launchData = splitSelectDataHolder.getFullscreenLaunchData()
+
+        assertEquals(launchData.splitLaunchType, SPLIT_SINGLE_INTENT_FULLSCREEN)
+
+        // should contain a valid intent for first app, and no task ID or shortcut
+        assertNotNull(launchData.initialPendingIntent)
+        assertEquals(launchData.initialTaskId, INVALID_TASK_ID)
+        assertNull(launchData.initialShortcut)
+
+        // should contain no task ID, intent, or shortcut for second app
+        assertEquals(launchData.secondTaskId, INVALID_TASK_ID)
+        assertNull(launchData.secondPendingIntent)
+        assertNull(launchData.secondShortcut)
+    }
+
+    @Test
+    fun generateLaunchData_Single_Shortcut() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleShortcut,
+            STAGE_POSITION_TOP_OR_LEFT,
+            sampleItemInfo,
+            null,
+            INVALID_TASK_ID
+        )
+        val launchData = splitSelectDataHolder.getFullscreenLaunchData()
+
+        assertEquals(launchData.splitLaunchType, SPLIT_SINGLE_SHORTCUT_FULLSCREEN)
+
+        // should contain a valid shortcut and intent for first app, and no task ID
+        assertNotNull(launchData.initialShortcut)
+        assertNotNull(launchData.initialPendingIntent)
+        assertEquals(launchData.initialTaskId, INVALID_TASK_ID)
+
+        // should contain no task ID, intent, or shortcut for second app
+        assertEquals(launchData.secondTaskId, INVALID_TASK_ID)
+        assertNull(launchData.secondPendingIntent)
+        assertNull(launchData.secondShortcut)
+    }
+
+    @Test
+    fun clearState() {
+        splitSelectDataHolder.setInitialTaskSelect(
+            sampleTaskInfo,
+            STAGE_POSITION_TOP_OR_LEFT,
+            null,
+            null
+        )
+        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser)
+        splitSelectDataHolder.resetState()
+        assertFalse(splitSelectDataHolder.isSplitSelectActive())
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index 65542cf..69109c2 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -37,6 +37,7 @@
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.SystemUiProxy
 import com.android.systemui.shared.recents.model.Task
+import java.util.function.Consumer
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNull
@@ -48,7 +49,6 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
-import java.util.function.Consumer
 
 @RunWith(AndroidJUnit4::class)
 class SplitSelectStateControllerTest {
@@ -67,6 +67,9 @@
     private val primaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId)
     private val nonPrimaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId + 10)
 
+    private var taskIdCounter = 0
+    private fun getUniqueId(): Int { return ++taskIdCounter }
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
@@ -100,15 +103,15 @@
         tasks.add(groupTask2)
 
         // Assertions happen in the callback we get from what we pass into
-        // #findLastActiveTaskAndRunCallback
+        // #findLastActiveTasksAndRunCallback
         val taskConsumer =
-            Consumer<Task> { assertNull("No tasks should have matched", it /*task*/) }
+            Consumer<List<Task>> { assertNull("No tasks should have matched", it[0] /*task*/) }
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
             withArgCaptor<Consumer<ArrayList<GroupTask>>> {
-                splitSelectStateController.findLastActiveTaskAndRunCallback(
-                    nonMatchingComponent,
+                splitSelectStateController.findLastActiveTasksAndRunCallback(
+                    listOf(nonMatchingComponent),
                     taskConsumer
                 )
                 verify(recentsModel).getTasks(capture())
@@ -139,27 +142,27 @@
         tasks.add(groupTask2)
 
         // Assertions happen in the callback we get from what we pass into
-        // #findLastActiveTaskAndRunCallback
+        // #findLastActiveTasksAndRunCallback
         val taskConsumer =
-            Consumer<Task> {
+            Consumer<List<Task>> {
                 assertEquals(
                     "ComponentName package mismatched",
-                    it.key.baseIntent.component.packageName,
+                    it[0].key.baseIntent.component?.packageName,
                     matchingPackage
                 )
                 assertEquals(
                     "ComponentName class mismatched",
-                    it.key.baseIntent.component.className,
+                    it[0].key.baseIntent.component?.className,
                     matchingClass
                 )
-                assertEquals(it, groupTask1.task1)
+                assertEquals(it[0], groupTask1.task1)
             }
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
             withArgCaptor<Consumer<ArrayList<GroupTask>>> {
-                splitSelectStateController.findLastActiveTaskAndRunCallback(
-                    matchingComponent,
+                splitSelectStateController.findLastActiveTasksAndRunCallback(
+                    listOf(matchingComponent),
                     taskConsumer
                 )
                 verify(recentsModel).getTasks(capture())
@@ -190,15 +193,15 @@
         tasks.add(groupTask2)
 
         // Assertions happen in the callback we get from what we pass into
-        // #findLastActiveTaskAndRunCallback
+        // #findLastActiveTasksAndRunCallback
         val taskConsumer =
-            Consumer<Task> { assertNull("No tasks should have matched", it /*task*/) }
+            Consumer<List<Task>> { assertNull("No tasks should have matched", it[0] /*task*/) }
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
             withArgCaptor<Consumer<ArrayList<GroupTask>>> {
-                splitSelectStateController.findLastActiveTaskAndRunCallback(
-                    nonPrimaryUserComponent,
+                splitSelectStateController.findLastActiveTasksAndRunCallback(
+                    listOf(nonPrimaryUserComponent),
                     taskConsumer
                 )
                 verify(recentsModel).getTasks(capture())
@@ -231,28 +234,28 @@
         tasks.add(groupTask2)
 
         // Assertions happen in the callback we get from what we pass into
-        // #findLastActiveTaskAndRunCallback
+        // #findLastActiveTasksAndRunCallback
         val taskConsumer =
-            Consumer<Task> {
+            Consumer<List<Task>> {
                 assertEquals(
                     "ComponentName package mismatched",
-                    it.key.baseIntent.component.packageName,
+                    it[0].key.baseIntent.component?.packageName,
                     matchingPackage
                 )
                 assertEquals(
                     "ComponentName class mismatched",
-                    it.key.baseIntent.component.className,
+                    it[0].key.baseIntent.component?.className,
                     matchingClass
                 )
-                assertEquals("userId mismatched", it.key.userId, nonPrimaryUserHandle.identifier)
-                assertEquals(it, groupTask1.task1)
+                assertEquals("userId mismatched", it[0].key.userId, nonPrimaryUserHandle.identifier)
+                assertEquals(it[0], groupTask1.task1)
             }
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
             withArgCaptor<Consumer<ArrayList<GroupTask>>> {
-                splitSelectStateController.findLastActiveTaskAndRunCallback(
-                    nonPrimaryUserComponent,
+                splitSelectStateController.findLastActiveTasksAndRunCallback(
+                    listOf(nonPrimaryUserComponent),
                     taskConsumer
                 )
                 verify(recentsModel).getTasks(capture())
@@ -283,27 +286,200 @@
         tasks.add(groupTask1)
 
         // Assertions happen in the callback we get from what we pass into
-        // #findLastActiveTaskAndRunCallback
+        // #findLastActiveTasksAndRunCallback
         val taskConsumer =
-            Consumer<Task> {
+            Consumer<List<Task>> {
                 assertEquals(
                     "ComponentName package mismatched",
-                    it.key.baseIntent.component.packageName,
+                    it[0].key.baseIntent.component?.packageName,
                     matchingPackage
                 )
                 assertEquals(
                     "ComponentName class mismatched",
-                    it.key.baseIntent.component.className,
+                    it[0].key.baseIntent.component?.className,
                     matchingClass
                 )
-                assertEquals(it, groupTask2.task2)
+                assertEquals(it[0], groupTask1.task1)
             }
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
             withArgCaptor<Consumer<ArrayList<GroupTask>>> {
-                splitSelectStateController.findLastActiveTaskAndRunCallback(
-                    matchingComponent,
+                splitSelectStateController.findLastActiveTasksAndRunCallback(
+                    listOf(matchingComponent),
+                    taskConsumer
+                )
+                verify(recentsModel).getTasks(capture())
+            }
+
+        // Send our mocked tasks
+        consumer.accept(tasks)
+    }
+
+    @Test
+    fun activeTasks_multipleSearchShouldFindTask() {
+        val nonMatchingComponent = ComponentKey(ComponentName("no", "match"), primaryUserHandle)
+        val matchingPackage = "hotdog"
+        val matchingClass = "juice"
+        val matchingComponent =
+            ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
+
+        val groupTask1 =
+            generateGroupTask(
+                ComponentName("hotdog", "pie"),
+                ComponentName("pumpkin", "pie")
+            )
+        val groupTask2 =
+            generateGroupTask(
+                ComponentName("pomegranate", "juice"),
+                ComponentName(matchingPackage, matchingClass)
+            )
+        val tasks: ArrayList<GroupTask> = ArrayList()
+        tasks.add(groupTask2)
+        tasks.add(groupTask1)
+
+        // Assertions happen in the callback we get from what we pass into
+        // #findLastActiveTasksAndRunCallback
+        val taskConsumer =
+            Consumer<List<Task>> {
+                assertEquals("Expected array length 2", 2, it.size)
+                assertNull("No tasks should have matched", it[0] /*task*/)
+                assertEquals(
+                    "ComponentName package mismatched",
+                    it[1].key.baseIntent.component?.packageName,
+                    matchingPackage
+                )
+                assertEquals(
+                    "ComponentName class mismatched",
+                    it[1].key.baseIntent.component?.className,
+                    matchingClass
+                )
+                assertEquals(it[1], groupTask2.task2)
+            }
+
+        // Capture callback from recentsModel#getTasks()
+        val consumer =
+            withArgCaptor<Consumer<ArrayList<GroupTask>>> {
+                splitSelectStateController.findLastActiveTasksAndRunCallback(
+                    listOf(nonMatchingComponent, matchingComponent),
+                    taskConsumer
+                )
+                verify(recentsModel).getTasks(capture())
+            }
+
+        // Send our mocked tasks
+        consumer.accept(tasks)
+    }
+
+    @Test
+    fun activeTasks_multipleSearchShouldNotFindSameTaskTwice() {
+        val matchingPackage = "hotdog"
+        val matchingClass = "juice"
+        val matchingComponent =
+            ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
+
+        val groupTask1 =
+            generateGroupTask(
+                ComponentName("hotdog", "pie"),
+                ComponentName("pumpkin", "pie")
+            )
+        val groupTask2 =
+            generateGroupTask(
+                ComponentName("pomegranate", "juice"),
+                ComponentName(matchingPackage, matchingClass)
+            )
+        val tasks: ArrayList<GroupTask> = ArrayList()
+        tasks.add(groupTask2)
+        tasks.add(groupTask1)
+
+        // Assertions happen in the callback we get from what we pass into
+        // #findLastActiveTasksAndRunCallback
+        val taskConsumer =
+            Consumer<List<Task>> {
+                assertEquals("Expected array length 2", 2, it.size)
+                assertEquals(
+                    "ComponentName package mismatched",
+                    it[0].key.baseIntent.component?.packageName,
+                    matchingPackage
+                )
+                assertEquals(
+                    "ComponentName class mismatched",
+                    it[0].key.baseIntent.component?.className,
+                    matchingClass
+                )
+                assertEquals(it[0], groupTask2.task2)
+                assertNull("No tasks should have matched", it[1] /*task*/)
+            }
+
+        // Capture callback from recentsModel#getTasks()
+        val consumer =
+            withArgCaptor<Consumer<ArrayList<GroupTask>>> {
+                splitSelectStateController.findLastActiveTasksAndRunCallback(
+                    listOf(matchingComponent, matchingComponent),
+                    taskConsumer
+                )
+                verify(recentsModel).getTasks(capture())
+            }
+
+        // Send our mocked tasks
+        consumer.accept(tasks)
+    }
+
+    @Test
+    fun activeTasks_multipleSearchShouldFindDifferentInstancesOfSameTask() {
+        val matchingPackage = "hotdog"
+        val matchingClass = "juice"
+        val matchingComponent =
+            ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
+
+        val groupTask1 =
+            generateGroupTask(
+                ComponentName(matchingPackage, matchingClass),
+                ComponentName("pumpkin", "pie")
+            )
+        val groupTask2 =
+            generateGroupTask(
+                ComponentName("pomegranate", "juice"),
+                ComponentName(matchingPackage, matchingClass)
+            )
+        val tasks: ArrayList<GroupTask> = ArrayList()
+        tasks.add(groupTask2)
+        tasks.add(groupTask1)
+
+        // Assertions happen in the callback we get from what we pass into
+        // #findLastActiveTasksAndRunCallback
+        val taskConsumer =
+            Consumer<List<Task>> {
+                assertEquals("Expected array length 2", 2, it.size)
+                assertEquals(
+                    "ComponentName package mismatched",
+                    it[0].key.baseIntent.component?.packageName,
+                    matchingPackage
+                )
+                assertEquals(
+                    "ComponentName class mismatched",
+                    it[0].key.baseIntent.component?.className,
+                    matchingClass
+                )
+                assertEquals(it[0], groupTask1.task1)
+                assertEquals(
+                    "ComponentName package mismatched",
+                    it[1].key.baseIntent.component?.packageName,
+                    matchingPackage
+                )
+                assertEquals(
+                    "ComponentName class mismatched",
+                    it[1].key.baseIntent.component?.className,
+                    matchingClass
+                )
+                assertEquals(it[1], groupTask2.task2)
+            }
+
+        // Capture callback from recentsModel#getTasks()
+        val consumer =
+            withArgCaptor<Consumer<ArrayList<GroupTask>>> {
+                splitSelectStateController.findLastActiveTasksAndRunCallback(
+                    listOf(matchingComponent, matchingComponent),
                     taskConsumer
                 )
                 verify(recentsModel).getTasks(capture())
@@ -366,6 +542,7 @@
     ): GroupTask {
         val task1 = Task()
         var taskInfo = ActivityManager.RunningTaskInfo()
+        taskInfo.taskId = getUniqueId()
         var intent = Intent()
         intent.component = task1ComponentName
         taskInfo.baseIntent = intent
@@ -373,6 +550,7 @@
 
         val task2 = Task()
         taskInfo = ActivityManager.RunningTaskInfo()
+        taskInfo.taskId = getUniqueId()
         intent = Intent()
         intent.component = task2ComponentName
         taskInfo.baseIntent = intent
@@ -393,6 +571,7 @@
     ): GroupTask {
         val task1 = Task()
         var taskInfo = ActivityManager.RunningTaskInfo()
+        taskInfo.taskId = getUniqueId()
         // Apply custom userHandle1
         taskInfo.userId = userHandle1.identifier
         var intent = Intent()
@@ -401,6 +580,7 @@
         task1.key = Task.TaskKey(taskInfo)
         val task2 = Task()
         taskInfo = ActivityManager.RunningTaskInfo()
+        taskInfo.taskId = getUniqueId()
         // Apply custom userHandle2
         taskInfo.userId = userHandle2.identifier
         intent = Intent()
diff --git a/quickstep/tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java b/quickstep/tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
index 83602be..a54dc2d 100644
--- a/quickstep/tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
+++ b/quickstep/tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
@@ -50,6 +50,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.List;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class TaskViewSimulatorTest {
@@ -150,7 +153,7 @@
                 WindowBounds wm = new WindowBounds(
                         new Rect(0, 0, mDisplaySize.x, mDisplaySize.y),
                         mDisplayInsets);
-                WindowBounds[] allBounds = new WindowBounds[4];
+                List<WindowBounds> allBounds = new ArrayList<>(4);
                 for (int i = 0; i < 4; i++) {
                     Rect boundsR = new Rect(wm.bounds);
                     Rect insetsR = new Rect(wm.insets);
@@ -158,7 +161,7 @@
                     RotationUtils.rotateRect(insetsR, RotationUtils.deltaRotation(rotation, i));
                     RotationUtils.rotateRect(boundsR, RotationUtils.deltaRotation(rotation, i));
                     boundsR.set(0, 0, Math.abs(boundsR.width()), Math.abs(boundsR.height()));
-                    allBounds[i] = new WindowBounds(boundsR, insetsR);
+                    allBounds.add(new WindowBounds(boundsR, insetsR));
                 }
 
                 WindowManagerProxy wmProxy = mock(WindowManagerProxy.class);
@@ -166,7 +169,7 @@
                 doReturn(wm).when(wmProxy).getRealBounds(any(), any());
                 doReturn(NavigationMode.NO_BUTTON).when(wmProxy).getNavigationMode(any());
 
-                ArrayMap<CachedDisplayInfo, WindowBounds[]> perDisplayBoundsCache =
+                ArrayMap<CachedDisplayInfo, List<WindowBounds>> perDisplayBoundsCache =
                         new ArrayMap<>();
                 perDisplayBoundsCache.put(cdi.normalize(), allBounds);
 
diff --git a/res/anim-v33/shared_x_axis_activity_close_enter.xml b/res/anim-v33/shared_x_axis_activity_close_enter.xml
index 94ef06c..3d7ad2b 100644
--- a/res/anim-v33/shared_x_axis_activity_close_enter.xml
+++ b/res/anim-v33/shared_x_axis_activity_close_enter.xml
@@ -25,7 +25,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/standard_decelerate"
+        android:interpolator="@interpolator/standard_decelerate_interpolator"
         android:startOffset="100"
         android:duration="350" />
 
@@ -35,7 +35,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:interpolator="@interpolator/emphasized_interpolator"
         android:startOffset="0"
         android:duration="450" />
 
diff --git a/res/anim-v33/shared_x_axis_activity_close_exit.xml b/res/anim-v33/shared_x_axis_activity_close_exit.xml
index 19eb09e..fb63602 100644
--- a/res/anim-v33/shared_x_axis_activity_close_exit.xml
+++ b/res/anim-v33/shared_x_axis_activity_close_exit.xml
@@ -24,7 +24,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/standard_accelerate"
+        android:interpolator="@interpolator/standard_accelerate_interpolator"
         android:startOffset="0"
         android:duration="100" />
 
@@ -34,7 +34,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:interpolator="@interpolator/emphasized_interpolator"
         android:startOffset="0"
         android:duration="450" />
 
diff --git a/res/anim-v33/shared_x_axis_activity_open_enter.xml b/res/anim-v33/shared_x_axis_activity_open_enter.xml
index f699cec..cba74ba 100644
--- a/res/anim-v33/shared_x_axis_activity_open_enter.xml
+++ b/res/anim-v33/shared_x_axis_activity_open_enter.xml
@@ -25,7 +25,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/standard_decelerate"
+        android:interpolator="@interpolator/standard_decelerate_interpolator"
         android:startOffset="100"
         android:duration="350" />
 
@@ -35,7 +35,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:interpolator="@interpolator/emphasized_interpolator"
         android:startOffset="0"
         android:duration="450" />
 
diff --git a/res/anim-v33/shared_x_axis_activity_open_exit.xml b/res/anim-v33/shared_x_axis_activity_open_exit.xml
index 85988ec..22e878d 100644
--- a/res/anim-v33/shared_x_axis_activity_open_exit.xml
+++ b/res/anim-v33/shared_x_axis_activity_open_exit.xml
@@ -24,7 +24,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/standard_accelerate"
+        android:interpolator="@interpolator/standard_accelerate_interpolator"
         android:startOffset="0"
         android:duration="100" />
 
@@ -34,7 +34,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:interpolator="@interpolator/emphasized_interpolator"
         android:startOffset="0"
         android:duration="450" />
 
diff --git a/res/drawable-hdpi/workspace_bg.9.png b/res/drawable-hdpi/workspace_bg.9.png
deleted file mode 100755
index 1d82fd4..0000000
--- a/res/drawable-hdpi/workspace_bg.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/workspace_bg.9.png b/res/drawable-mdpi/workspace_bg.9.png
deleted file mode 100755
index 116ce44..0000000
--- a/res/drawable-mdpi/workspace_bg.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/workspace_bg.9.png b/res/drawable-xhdpi/workspace_bg.9.png
deleted file mode 100755
index b1b3b85..0000000
--- a/res/drawable-xhdpi/workspace_bg.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/workspace_bg.9.png b/res/drawable-xxhdpi/workspace_bg.9.png
deleted file mode 100755
index d47f6b2..0000000
--- a/res/drawable-xxhdpi/workspace_bg.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxxhdpi/workspace_bg.9.png b/res/drawable-xxxhdpi/workspace_bg.9.png
deleted file mode 100755
index 3281548..0000000
--- a/res/drawable-xxxhdpi/workspace_bg.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/all_apps_tabs_background.xml b/res/drawable/all_apps_tabs_background.xml
index 8471cd4..1e7cff2 100644
--- a/res/drawable/all_apps_tabs_background.xml
+++ b/res/drawable/all_apps_tabs_background.xml
@@ -30,7 +30,7 @@
                 android:state_selected="false">
                 <shape android:shape="rectangle">
                     <corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
-                    <solid android:color="@color/all_apps_tabs_background" />
+                    <solid android:color="@color/material_color_surface_bright" />
                 </shape>
             </item>
 
@@ -39,7 +39,7 @@
                 android:state_selected="true">
                 <shape android:shape="rectangle">
                     <corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
-                    <solid android:color="@color/all_apps_tab_background_selected" />
+                    <solid android:color="@color/material_color_primary" />
                 </shape>
             </item>
         </selector>
diff --git a/res/drawable/arrow_toast_rounded_background.xml b/res/drawable/arrow_toast_rounded_background.xml
index 1206ddd..d7d6255 100644
--- a/res/drawable/arrow_toast_rounded_background.xml
+++ b/res/drawable/arrow_toast_rounded_background.xml
@@ -14,6 +14,6 @@
     limitations under the License.
 -->
 <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
-    <solid android:color="@color/arrow_tip_view_bg" />
+    <solid android:color="?attr/arrowTipBackground" />
     <corners android:radius="@dimen/dialogCornerRadius" />
 </shape>
diff --git a/res/drawable/bottom_rounded_popup_ripple.xml b/res/drawable/bottom_rounded_popup_ripple.xml
deleted file mode 100644
index 739833a..0000000
--- a/res/drawable/bottom_rounded_popup_ripple.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2023 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
-    android:color="?android:attr/colorControlHighlight">
-    <item android:id="@android:id/mask">
-        <shape android:shape="rectangle">
-            <solid android:color="#FFFFFFFF"/>
-            <corners android:bottomLeftRadius="@dimen/dialogCornerRadius"
-                android:bottomRightRadius="@dimen/dialogCornerRadius"
-                android:topLeftRadius="0dp"
-                android:topRightRadius="0dp"/>
-        </shape>
-    </item>
-</ripple>
\ No newline at end of file
diff --git a/res/drawable/bubble_ic_overflow_button.xml b/res/drawable/bubble_ic_overflow_button.xml
new file mode 100644
index 0000000..475639e
--- /dev/null
+++ b/res/drawable/bubble_ic_overflow_button.xml
@@ -0,0 +1,24 @@
+<!--
+Copyright (C) 2023 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+  android:viewportWidth="24"
+  android:viewportHeight="24"
+  android:width="24dp"
+  android:height="24dp">
+  <path
+      android:fillColor="#1A73E8"
+      android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+</vector>
diff --git a/quickstep/res/drawable/taskbar_divider_bg.xml b/res/drawable/ic_split_exit.xml
similarity index 61%
rename from quickstep/res/drawable/taskbar_divider_bg.xml
rename to res/drawable/ic_split_exit.xml
index 52e230d..d7e8b03 100644
--- a/quickstep/res/drawable/taskbar_divider_bg.xml
+++ b/res/drawable/ic_split_exit.xml
@@ -13,8 +13,13 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle" >
-    <solid android:color="@color/taskbar_divider_background"/>
-    <corners android:radius="1dp" />
-</shape>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="20dp"
+    android:tint="#000000"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="20dp">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_wallpaper.xml b/res/drawable/ic_wallpaper.xml
deleted file mode 100644
index 9543f88..0000000
--- a/res/drawable/ic_wallpaper.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<!--
-   Copyright (C) 2016 The Android Open Source Project
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-         http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="@dimen/options_menu_icon_size"
-        android:height="@dimen/options_menu_icon_size"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0"
-        android:tint="?android:attr/textColorPrimary">
-    <path
-        android:fillColor="@android:color/white"
-        android:pathData="M9,12.71l2.14,2.58l3-3.87L18,16.57H6L9,12.71z M5,5h6V3H5C3.9,3,3,3.9,3,5v6h2V5z M19,19h-6v2h6c1.1,0,2-0.9,2-2v-6h-2V19z
-            M5,19v-6H3v6c0,1.1,0.9,2,2,2h6v-2H5z M19,5v6h2V5c0-1.1-0.9-2-2-2h-6v2H19z M16,9c0.55,0,1-0.45,1-1s-0.45-1-1-1
-            c-0.55,0-1,0.45-1,1S15.45,9,16,9z"/>
-</vector>
diff --git a/res/drawable/top_rounded_popup_ripple.xml b/res/drawable/rounded_popup_ripple.xml
similarity index 80%
rename from res/drawable/top_rounded_popup_ripple.xml
rename to res/drawable/rounded_popup_ripple.xml
index 7468480..b0dcc80 100644
--- a/res/drawable/top_rounded_popup_ripple.xml
+++ b/res/drawable/rounded_popup_ripple.xml
@@ -18,10 +18,7 @@
     <item android:id="@android:id/mask">
         <shape android:shape="rectangle">
             <solid android:color="#FFFFFFFF"/>
-            <corners android:bottomLeftRadius="0dp"
-                android:bottomRightRadius="0dp"
-                android:topLeftRadius="@dimen/dialogCornerRadius"
-                android:topRightRadius="@dimen/dialogCornerRadius"/>
+            <corners android:radius="@dimen/dialogCornerRadius" />
         </shape>
     </item>
 </ripple>
\ No newline at end of file
diff --git a/res/interpolator/back_cancel.xml b/res/interpolator/back_cancel.xml
deleted file mode 100644
index 2165457..0000000
--- a/res/interpolator/back_cancel.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 2022, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:controlX1="0.2"
-    android:controlY1="0"
-    android:controlX2="0"
-    android:controlY2="1"/>
\ No newline at end of file
diff --git a/res/interpolator/fast_out_extra_slow_in.xml b/res/interpolator/fast_out_extra_slow_in.xml
deleted file mode 100644
index f296a82..0000000
--- a/res/interpolator/fast_out_extra_slow_in.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License
-  -->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:pathData="M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1"/>
\ No newline at end of file
diff --git a/res/interpolator/folder_interpolator.xml b/res/interpolator/folder_interpolator.xml
deleted file mode 100644
index b95d454..0000000
--- a/res/interpolator/folder_interpolator.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 2017, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:controlX1="0.2"
-    android:controlY1="0"
-    android:controlX2="0"
-    android:controlY2="1"/>
diff --git a/res/interpolator/large_folder_preview_item_close_interpolator.xml b/res/interpolator/large_folder_preview_item_close_interpolator.xml
deleted file mode 100644
index d28af63..0000000
--- a/res/interpolator/large_folder_preview_item_close_interpolator.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 2017, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:controlX1="0.3"
-    android:controlY1="0"
-    android:controlX2="1"
-    android:controlY2="1"/>
diff --git a/res/interpolator/standard_accelerate.xml b/res/interpolator/standard_accelerate.xml
deleted file mode 100644
index 394393d..0000000
--- a/res/interpolator/standard_accelerate.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:controlX1="0.3"
-    android:controlY1="0"
-    android:controlX2="1"
-    android:controlY2="1"/>
\ No newline at end of file
diff --git a/res/layout/all_apps_prediction_row_icon.xml b/res/layout/all_apps_prediction_row_icon.xml
new file mode 100644
index 0000000..c9b3275
--- /dev/null
+++ b/res/layout/all_apps_prediction_row_icon.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.launcher3.BubbleTextView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto"
+    style="@style/BaseIcon.AllApps"
+    android:id="@+id/icon"
+    launcher:iconDisplay="prediction_row"
+    launcher:centerVertically="true" />
diff --git a/res/layout/app_pair_icon.xml b/res/layout/app_pair_icon.xml
new file mode 100644
index 0000000..2b9a98b
--- /dev/null
+++ b/res/layout/app_pair_icon.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<com.android.launcher3.apppairs.AppPairIcon
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:focusable="true" >
+    <com.android.launcher3.views.DoubleShadowBubbleTextView
+        style="@style/BaseIcon.Workspace"
+        android:id="@+id/app_pair_icon_name"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:focusable="false"
+        android:layout_gravity="top" />
+</com.android.launcher3.apppairs.AppPairIcon>
diff --git a/res/layout/arrow_toast.xml b/res/layout/arrow_toast.xml
index 88a92eb..004e778 100644
--- a/res/layout/arrow_toast.xml
+++ b/res/layout/arrow_toast.xml
@@ -28,7 +28,7 @@
         android:padding="16dp"
         android:background="@drawable/arrow_toast_rounded_background"
         android:elevation="@dimen/arrow_toast_elevation"
-        android:textColor="@color/arrow_tip_view_content"
+        android:textColor="?attr/arrowTipTextColor"
         android:textSize="14sp"/>
 
     <View
diff --git a/res/layout/widgets_table_container.xml b/res/layout/widgets_table_container.xml
index 4a32672..c41d0bb 100644
--- a/res/layout/widgets_table_container.xml
+++ b/res/layout/widgets_table_container.xml
@@ -16,6 +16,7 @@
 <com.android.launcher3.widget.picker.WidgetsListTableView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/widgets_table"
+    android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
     android:background="@drawable/bg_widgets_content"
     android:layout_width="match_parent"
     android:layout_height="wrap_content" />
diff --git a/res/layout/work_apps_paused.xml b/res/layout/work_apps_paused.xml
index 52c5a49..695270e 100644
--- a/res/layout/work_apps_paused.xml
+++ b/res/layout/work_apps_paused.xml
@@ -15,7 +15,7 @@
 <com.android.launcher3.allapps.WorkPausedCard xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:padding="@dimen/work_edu_card_margin"
+    android:padding="@dimen/all_apps_tabs_margin_top"
     android:orientation="vertical"
     android:gravity="center_horizontal">
 
@@ -25,7 +25,6 @@
         android:id="@+id/work_apps_paused_title"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginTop="40dp"
         android:text="@string/work_apps_paused_title"
         android:textAlignment="center"
         android:textSize="18sp" />
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index d9a67c4..aa96397 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Vouer hernoem na <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Vouer: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> items"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Vouer: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> of meer items"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Muurpapiere"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Muurpapier en styl"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Wysig tuisskerm"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Tuis-instellings"</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index dbe2acf..bcc626e 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"አቃፊ <xliff:g id="NAME">%1$s</xliff:g> ተብሎ ዳግም ተሰይሟል"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"አቃፊ፦ <xliff:g id="NAME">%1$s</xliff:g>፣ <xliff:g id="SIZE">%2$d</xliff:g> ንጥሎች"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"አቃፊ፦ <xliff:g id="NAME">%1$s</xliff:g>፣ <xliff:g id="SIZE">%2$d</xliff:g> ወይም ተጨማሪ ንጥሎች"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"የግድግዳ ወረቀቶች"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"ልጣፍ እና ቅጥ"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"መነሻ ማያ ገጽን አርትዕ"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"የመነሻ ቅንብሮች"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 90a1e87..69e96cb 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"تمت إعادة تسمية المجلد إلى <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"المجلد: <xliff:g id="NAME">%1$s</xliff:g>، <xliff:g id="SIZE">%2$d</xliff:g> عنصر"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"المجلد: <xliff:g id="NAME">%1$s</xliff:g>، <xliff:g id="SIZE">%2$d</xliff:g> عنصر أو أكثر"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"الخلفيات"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"الخلفية والأسلوب"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"تعديل الشاشة الرئيسية"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"إعدادات الشاشة الرئيسية"</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index 587b377..8c18e8d 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"ফ\'ল্ডাৰৰ নাম সলনি কৰি <xliff:g id="NAME">%1$s</xliff:g> কৰা হৈছে"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"ফ’ল্ডাৰ: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> টা বস্তু"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"ফ’ল্ডাৰ: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> টা অথবা তাতকৈ অধিক বস্তু"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"ৱালপেপাৰসমূহ"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"ৱালপেপাৰ আৰু শৈলী"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"গৃহ স্ক্ৰীন সম্পাদনা কৰক"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"গৃহ ছেটিং"</string>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index 1e1b634..bf12761 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Qovluq adı <xliff:g id="NAME">%1$s</xliff:g> ilə dəyişdirildi"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Qovluq: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> element"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Qovluq: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> və ya daha çox element"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Divar kağızları"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Divar kağızı və üslub"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Əsas ekranı redaktə edin"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Home ayarları"</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 2b3ed53..cfb6fbd 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Folder je preimenovan u <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> stavke"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ili više stavki"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Pozadine"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Pozadina i stil"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Izmeni početni ekran"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Podešavanja početnog ekrana"</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 1eee242..5950ec5 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Папка перайменавана ў <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Папка: <xliff:g id="NAME">%1$s</xliff:g>, элементы: <xliff:g id="SIZE">%2$d</xliff:g>"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Папка: <xliff:g id="NAME">%1$s</xliff:g>, элементы: <xliff:g id="SIZE">%2$d</xliff:g> ці больш"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Шпалеры"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Шпалеры і стыль"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Змяніць Галоўны экран"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Налады галоўнага экрана"</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index f59dcfb..b0f1c4c 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Папката е преименувана на „<xliff:g id="NAME">%1$s</xliff:g>“"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Папка: „<xliff:g id="NAME">%1$s</xliff:g>“ – <xliff:g id="SIZE">%2$d</xliff:g> елемента"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Папка: „<xliff:g id="NAME">%1$s</xliff:g>“ – <xliff:g id="SIZE">%2$d</xliff:g> или повече елементи"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Тапети"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Тапет и стил"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Редактиране на началния екран"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Настройки за началния екран"</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 326fc7b..e28638b 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"ফোল্ডারের নাম পাল্টে <xliff:g id="NAME">%1$s</xliff:g> করা হয়েছে"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"ফোল্ডার: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g>টি আইটেম"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"ফোল্ডার: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g>টি বা তার বেশি আইটেম"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"ওয়ালপেপারগুলি"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"ওয়ালপেপার এবং স্টাইল"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"হোম স্ক্রিন এডিট করুন"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"হোম সেটিংস"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 8c3dd08..8387f14 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Ime foldera je promijenjeno u <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, br. stavki: <xliff:g id="SIZE">%2$d</xliff:g>"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ili više stavki"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Pozadinske slike"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Pozadinska slika i stil"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Uredi Početni ekran"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Postavke početnog ekrana"</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 9fe81dd..dd2d03f 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"S\'ha canviat el nom de la carpeta a <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Carpeta: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> elements"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Carpeta: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> o més elements"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Fons de pantalla"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Estil i fons de pantalla"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Edita la pantalla d\'inici"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Config. pantalla d\'inici"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 40e1397..f9c7c77 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Složka přejmenována na <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Složka: <xliff:g id="NAME">%1$s</xliff:g>, počet položek: <xliff:g id="SIZE">%2$d</xliff:g>"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Složka: <xliff:g id="NAME">%1$s</xliff:g>, počet položek: <xliff:g id="SIZE">%2$d</xliff:g> nebo více"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Tapety"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Tapeta a styl"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Upravit plochu"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Nastavení plochy"</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 5867716..134cbb0 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Mappen er omdøbt til <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Mappe: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> elementer"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Mappe: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> eller flere elementer"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Baggrunde"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Baggrund og stil"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Rediger startskærm"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Indst. for startskærm"</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 93ea28e..2fa7f91 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Ordner umbenannt in <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Ordner: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> Elemente"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Ordner: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> oder mehr Elemente"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Hintergründe"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Hintergrund und Stil"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Startbildschirm bearbeiten"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Einstellungen"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 132b20f..35cac5b 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Ο φάκελος μετονομάστηκε σε <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Φάκελος: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> στοιχεία"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Φάκελος: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ή περισσότερα στοιχεία"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Ταπετσαρίες"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Ταπετσαρία και στιλ"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Επεξεργασία αρχικής οθόνης"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Ρυθμίσεις Αρχ. Οθ."</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index aa926df..47a67a8 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Folder renamed to <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> items"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> or more items"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Wallpapers"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Wallpaper and style"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Edit home screen"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Home settings"</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index 40534ca..cc86e87 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Folder renamed to <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> items"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> or more items"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Wallpapers"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Wallpaper and style"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Edit Home Screen"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Home settings"</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index aa926df..47a67a8 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Folder renamed to <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> items"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> or more items"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Wallpapers"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Wallpaper and style"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Edit home screen"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Home settings"</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index aa926df..47a67a8 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Folder renamed to <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> items"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> or more items"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Wallpapers"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Wallpaper and style"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Edit home screen"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Home settings"</string>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index d7a5d95..cca6551 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‎‎‎‏‏‏‎‎‏‎‏‏‏‏‎‎‎‎‏‏‏‎‏‏‏‎‎‎‎‏‎‎‎‏‎‏‎‎‎‏‏‎‎‎‎‎‎‏‏‏‏‎‎‎‏‏‎‎‎‎Folder renamed to ‎‏‎‎‏‏‎<xliff:g id="NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‏‎‏‏‎‏‏‎‏‎‎‏‎‎‏‎‎‎‎‎‏‎‏‏‏‏‏‎‏‎‎‎‎‏‏‏‎‏‎‎‎‏‏‏‎‎‏‎‎‎‎‎‎‎‏‏‎Folder: ‎‏‎‎‏‏‎<xliff:g id="NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎, ‎‏‎‎‏‏‎<xliff:g id="SIZE">%2$d</xliff:g>‎‏‎‎‏‏‏‎ items‎‏‎‎‏‎"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‏‎‏‎‎‎‎‏‎‎‏‏‏‏‎‎‏‎‏‏‎‏‎‎‏‎‏‏‏‎‏‎‎‏‎‎‏‎‎‏‏‎‏‏‎‎‏‎‎‏‏‏‏‏‏‏‏‎Folder: ‎‏‎‎‏‏‎<xliff:g id="NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎, ‎‏‎‎‏‏‎<xliff:g id="SIZE">%2$d</xliff:g>‎‏‎‎‏‏‏‎ or more items‎‏‎‎‏‎"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‎‎‎‏‎‏‎‏‏‏‏‏‎‎‏‏‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‎‎‎‏‎‏‏‎‎‏‏‎‏‏‏‏‎‏‏‎Wallpapers‎‏‎‎‏‎"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‏‎‎‎‎‎‏‎‎‎‏‎‎‎‎‎‏‎‏‏‎‏‎‎‎‏‎‎‏‏‏‎‏‏‎‎‏‎‎‎‎‎‏‎‎‏‎‏‎‏‎‎‏‏‏‎‏‎‎Wallpaper &amp; style‎‏‎‎‏‎"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‏‏‎‏‎‎‏‎‏‏‎‏‏‏‎‎‏‎‎‎‏‏‏‏‎‏‎‎‏‏‏‏‏‏‏‎‏‏‏‎‎‏‎‏‏‏‏‏‏‎‏‏‎Edit Home Screen‎‏‎‎‏‎"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‎‎‏‎‎‏‎‏‏‎‎‏‏‏‎‏‏‏‏‏‎‏‏‎‎‏‏‏‎‎‏‎‎‎‎‏‏‏‎‎‏‏‏‎‏‏‎‏‎‎‏‏‏‎‎‎‎Home settings‎‏‎‎‏‎"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index bffa3f8..c49fa0e 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"El nombre de la carpeta se cambió a <xliff:g id="NAME">%1$s</xliff:g>."</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Carpeta: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> elementos"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Carpeta: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> o más elementos"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Fondos de pantalla"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Fondo de pantalla y estilo"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Editar pantalla principal"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Configuración de pantalla principal"</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 2ebefc0..b89a717 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Se ha cambiado el nombre de la carpeta a <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Carpeta: <xliff:g id="NAME">%1$s</xliff:g> (<xliff:g id="SIZE">%2$d</xliff:g> elementos)"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Carpeta: <xliff:g id="NAME">%1$s</xliff:g> (<xliff:g id="SIZE">%2$d</xliff:g> o más elementos)"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Fondos de pantalla"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Fondo de pantalla y estilo"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Editar pantalla de inicio"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Ajustes de la pantalla de inicio"</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 1859b06..333fded 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Kausta uus nimi: <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Kaust: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> üksust"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Kaust: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> või rohkem üksust"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Taustapildid"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Taustapilt ja stiil"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Muuda avaekraani"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Avakuva seaded"</string>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index 4d617c5..cbf7e1c 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Karpetari <xliff:g id="NAME">%1$s</xliff:g> izena eman zaio"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"<xliff:g id="NAME">%1$s</xliff:g> karpeta (<xliff:g id="SIZE">%2$d</xliff:g> elementu)"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"<xliff:g id="NAME">%1$s</xliff:g> karpeta (<xliff:g id="SIZE">%2$d</xliff:g> elementu edo gehiago)"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Horma-paperak"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Horma-papera eta estiloa"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Editatu hasierako pantaila"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Hasierako pantailaren ezarpenak"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 091211a..4ec760e 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"نام پوشه به <xliff:g id="NAME">%1$s</xliff:g> تغییر کرد"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"پوشه: <xliff:g id="NAME">%1$s</xliff:g>، <xliff:g id="SIZE">%2$d</xliff:g> مورد"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"پوشه: <xliff:g id="NAME">%1$s</xliff:g>، <xliff:g id="SIZE">%2$d</xliff:g> مورد یا بیشتر"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"کاغذدیواری‌ها"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"کاغذدیواری و سبک"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"ویرایش «صفحه اصلی»"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"تنظیمات صفحه اصلی"</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index a1aa48c..16dabb7 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Kansion nimeksi vaihdettiin <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Kansio: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> kohdetta"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Kansio: <xliff:g id="NAME">%1$s</xliff:g>, ainakin <xliff:g id="SIZE">%2$d</xliff:g> kohdetta"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Taustakuvat"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Taustakuva ja tyyli"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Muokkaa aloitusnäyttöä"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Aloitusnäyttö"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 7478968..1267205 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Nouveau nom du dossier : <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Dossier : <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> élément(s)"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Dossier : <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> éléments ou plus"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Fonds d\'écran"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Fond d\'écran et style"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Modifier l\'écran d\'accueil"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Paramètres d\'accueil"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 63869f6..1addca7 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Nouveau nom du dossier : <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Dossier : <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> éléments"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Dossier : <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> éléments ou plus"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Fonds d\'écran"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Fond d\'écran et style"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Modifier l\'écran d\'accueil"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Paramètres de l\'accueil"</string>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index ab68e22..cf6ac1c 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"O cartafol cambiou o nome a <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Cartafol: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> elementos"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Cartafol: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> elementos ou máis"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Fondos de pantalla"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Estilo e fondo de pantalla"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Editar pantalla de inicio"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Axustes de Inicio"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 80aaba3..7f5c39f 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"ફોલ્ડરનું નામ બદલીને <xliff:g id="NAME">%1$s</xliff:g> કર્યું"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"ફોલ્ડર: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> આઇટમ"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"ફોલ્ડર: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> કે વધુ આઇટમ"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"વૉલપેપર"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"વૉલપેપર અને સ્ટાઇલ"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"હોમ સ્ક્રીનમાં ફેરફાર કરો"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"હોમ સેટિંગ"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 1834b1e..8301194 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"फ़ोल्डर का नाम बदलकर <xliff:g id="NAME">%1$s</xliff:g> किया गया"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"फ़ोल्डर: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> आइटम"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"फ़ोल्डर: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> या इससे ज़्यादा आइटम"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"वॉलपेपर"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"वॉलपेपर और स्टाइल"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"होम स्क्रीन में बदलाव करें"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"होम स्क्रीन की सेटिंग"</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 3e52b18..59630f4 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Mapa je preimenovana u <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Mapa: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> stavke"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Mapa: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ili više stavki"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Pozadine"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Pozadina i stil"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Uredi početni zaslon"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Postavke početnog zaslona"</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 979539d..3bcf350 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"A mappa új neve: <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Mappa: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> elem"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Mappa: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> vagy több elem"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Háttérképek"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Háttérkép és stílus"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Kezdőképernyő szerkesztése"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Kezdőképernyő beállításai"</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 442f4f6..ccc028f 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Պանակը վերանվանվեց <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Պանակ՝ <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> տարր"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Պանակ՝ <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> կամ ավելի տարրեր"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Պաստառներ"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Պաստառ և ոճ"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Փոփոխել հիմնական էկրանը"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Գլխավոր էկրանի կարգավորումներ"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index f16f69f..c1d62d4 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Folder diganti namanya menjadi <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> item"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> item atau lebih"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Wallpaper"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Wallpaper &amp; gaya"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Edit Layar Utama"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Setelan layar utama"</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index fc574cb..e336a4b 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Heiti möppu breytt í <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Mappa: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> atriði"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Mappa: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> eða fleiri atriði"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Veggfóður"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Veggfóður og stíll"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Breyta heimaskjá"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Heimastillingar"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 59c0cb7..4342ce6 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Nome della cartella sostituito con <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Cartella: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> elementi"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Cartella: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> o più elementi"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Sfondi"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Sfondo e stile"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Modifica la schermata Home"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Impostazioni schermata Home"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 92f5d1d..808d324 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"שם התיקייה שונה ל-<xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"תיקייה: <xliff:g id="NAME">%1$s</xliff:g>, מספר הפריטים: <xliff:g id="SIZE">%2$d</xliff:g>"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"תיקייה: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> פריטים או יותר"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"טפטים"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"טפט וסגנון"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"עריכה של מסך הבית"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"הגדרות של מסך הבית"</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 43cc1e3..a6b1a21 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"フォルダの名前を「<xliff:g id="NAME">%1$s</xliff:g>」に変更しました"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"フォルダ: <xliff:g id="NAME">%1$s</xliff:g>、<xliff:g id="SIZE">%2$d</xliff:g> 件のアイテム"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"フォルダ: <xliff:g id="NAME">%1$s</xliff:g>、<xliff:g id="SIZE">%2$d</xliff:g> 件以上のアイテム"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"壁紙"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"壁紙とスタイル"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"ホーム画面を編集"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"ホームの設定"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index 7553f6a..7e79faa 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"საქაღალდეს შეეცვალა სახელი „<xliff:g id="NAME">%1$s</xliff:g>“-ად"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"საქაღალდე: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ერთეული"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"საქაღალდე: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ან მეტი ერთეული"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"ფონები"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"ფონი და სტილი"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"მთავარი ეკრანის რედაქტირება"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"მთავარი გვერდის პარამეტრები"</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 00babfd..cb514d8 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Қалта атауы <xliff:g id="NAME">%1$s</xliff:g> болып өзгертілді"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Қалта: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> элемент бар"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Қалта: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> не одан көп элемент бар"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Тұсқағаздар"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Тұсқағаз және стиль"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Негізгі экранды өзгерту"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Негізгі экран параметрлері"</string>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index 42e0d4b..bdb9f0d 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"បាន​ប្ដូរ​ឈ្មោះ​ថត​ជា <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"ថត៖ <xliff:g id="NAME">%1$s</xliff:g>, ធាតុ <xliff:g id="SIZE">%2$d</xliff:g>"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"ថត៖ <xliff:g id="NAME">%1$s</xliff:g>, ធាតុ <xliff:g id="SIZE">%2$d</xliff:g> ឬច្រើនជាងនេះ"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"ផ្ទាំង​រូបភាព"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"ផ្ទាំងរូបភាព និងរចនាប័ទ្ម"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"កែអេក្រង់ដើម"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"ការកំណត់​ទំព័រដើម"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 481c426..a35a70b 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"ಫೋಲ್ಡರ್‌ ಅನ್ನು <xliff:g id="NAME">%1$s</xliff:g> ಗೆ ಮರುಹೆಸರಿಸಲಾಗಿದೆ"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"ಫೋಲ್ಡರ್: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ಐಟಂಗಳು"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"ಫೋಲ್ಡರ್: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ಅಥವಾ ಹೆಚ್ಚಿನ ಐಟಂಗಳು"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"ವಾಲ್‌ಪೇಪರ್‌ಗಳು"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"ವಾಲ್‌ಪೇಪರ್ ಮತ್ತು ಶೈಲಿ"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"ಹೋಮ್ ಸ್ಕ್ರೀನ್ ಅನ್ನು ಎಡಿಟ್ ಮಾಡಿ"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"ಮುಖಪುಟ ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index ab682a4..955af0b 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"폴더 이름 변경: <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"폴더: <xliff:g id="NAME">%1$s</xliff:g>, 항목 <xliff:g id="SIZE">%2$d</xliff:g>개"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"폴더: <xliff:g id="NAME">%1$s</xliff:g>, 항목 <xliff:g id="SIZE">%2$d</xliff:g>개 이상"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"배경화면"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"배경화면 및 스타일"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"홈 화면 수정"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"홈 설정"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 2047691..7d77440 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Фолдердин аты <xliff:g id="NAME">%1$s</xliff:g> деп өзгөртүлдү"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"<xliff:g id="NAME">%1$s</xliff:g> папкасындагы объекттер: <xliff:g id="SIZE">%2$d</xliff:g>"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"<xliff:g id="NAME">%1$s</xliff:g> папкасындагы объекттер: <xliff:g id="SIZE">%2$d</xliff:g> же андан көбүрөөк"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Тушкагаздар"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Тушкагаз жана стиль"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Башкы экранды түзөтүү"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Башкы бет параметрлери"</string>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index 166c370..9c26cfa 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"ປ່ຽນຊື່ໂຟນເດີເປັນ <xliff:g id="NAME">%1$s</xliff:g> ແລ້ວ"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"ໂຟນເດີ: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ລາຍການ"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"ໂຟນເດີ: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ຫຼື ລາຍການເພີ່ມເຕີມ"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"ພາບພື້ນຫຼັງ"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"ຮູບພື້ນຫຼັງ ແລະ ຮູບແບບ"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"ແກ້ໄຂໂຮມສະກຣີນ"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"ການຕັ້ງຄ່າໜ້າຫຼັກ"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 4c67498..7d69c57 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Aplankas pervardytas kaip „<xliff:g id="NAME">%1$s</xliff:g>“"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Aplankas: „<xliff:g id="NAME">%1$s</xliff:g>“, elementų: <xliff:g id="SIZE">%2$d</xliff:g>"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Aplankas: „<xliff:g id="NAME">%1$s</xliff:g>“, elementų: <xliff:g id="SIZE">%2$d</xliff:g> ar daugiau"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Ekrano fonai"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Ekrano fonas ir stilius"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Redaguoti pagrindinį ekraną"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"„Home“ nustatymai"</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index ad7af54..6dc9564 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Mape pārdēvēta par: <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Mape <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> vienumi"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Mape <xliff:g id="NAME">%1$s</xliff:g>, vienumu skaits mapē: vismaz <xliff:g id="SIZE">%2$d</xliff:g>"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Fona tapetes"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Fona tapete un stils"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Rediģēt sākuma ekrānu"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Sākumlapas iestatījumi"</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index c2ee822..eff0e15 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Папката е преименувана во <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Папка: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ставки"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Папка: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> или повеќе ставки"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Тапети"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Тапет и стил"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Изменете го почетниот екран"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Поставки за почетен екран"</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index 3600edd..d7feb3c 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"ഫോൾഡറിന്റെ പേര് <xliff:g id="NAME">%1$s</xliff:g> എന്നായി മാറ്റി"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"ഫോൾഡർ: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ഇനങ്ങൾ"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"ഫോൾഡർ: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> അല്ലെങ്കിൽ അതിലധികം ഇനങ്ങൾ"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"വാൾപേപ്പർ"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"വാൾപേപ്പറും സ്‌റ്റൈലും"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"ഹോം സ്‌ക്രീൻ എഡിറ്റ് ചെയ്യുക"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"ഹോം ക്രമീകരണം"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 430e80d..716bd85 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Фолдерын нэр <xliff:g id="NAME">%1$s</xliff:g> болов"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Фолдер: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> зүйл"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Фолдер: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> эсвэл үүнээс олон зүйл"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Дэлгэцийн зураг"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Дэлгэцийн зураг, загвар"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Үндсэн нүүрийг засах"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Нүүр хуудасны тохиргоо"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index b2c023e..c73295d 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"फोल्डरचे नाव बदलून <xliff:g id="NAME">%1$s</xliff:g> असे ठेवले"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"फोल्डर: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> आयटम"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"फोल्डर: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> किंवा त्याहून अधिक आयटम"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"वॉलपेपर"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"वॉलपेपर आणि शैली"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"होम स्क्रीन संपादित करा"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"होम सेटिंग्‍ज"</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 5dcc913..e4a2e57 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Folder dinamakan semula kepada <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> item"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> atau lebih banyak item"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Hiasan latar"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Hiasan latar &amp; gaya"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Edit Skrin Utama"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Tetapan skrin utama"</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index e0e98d5..1ec09f5 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"ပြောင်းလဲလိုက်သော အကန့်အမည် <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"ဖိုင်တွဲ - <xliff:g id="NAME">%1$s</xliff:g>၊ <xliff:g id="SIZE">%2$d</xliff:g> ဖိုင်များ"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"ဖိုင်တွဲ - <xliff:g id="NAME">%1$s</xliff:g>၊ <xliff:g id="SIZE">%2$d</xliff:g> သို့မဟုတ် နောက်ထပ်ဖိုင်များ"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"နောက်ခံများ"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"နောက်ခံနှင့် ပုံစံ"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"ပင်မစာမျက်နှာ တည်းဖြတ်ရန်"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"ပင်မဆက်တင်များ"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 485f845..7266a81 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Mappen heter nå <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Mappe: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> elementer"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Mappe: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> eller flere elementer"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Bakgrunner"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Bakgrunn og stil"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Endre startsiden"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Startsideinnstillinger"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 4219dce..a73f2a3 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"फोल्डर <xliff:g id="NAME">%1$s</xliff:g> मा पुनःनामाकरण गरियो।"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"फोल्डर: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> वस्तुहरू"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"फोल्डर: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> वा सोभन्दा बढी वस्तुहरू"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"वालपेपरहरु"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"वालपेपर तथा शैली"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"होम स्क्रिन बदल्नुहोस्"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"होम पेजका सेटिङहरू"</string>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 416d82d..8d2aa21 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"De naam van de map is gewijzigd in <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Map: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> items"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Map: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> of meer items"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Achtergrond"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Achtergrond en stijl"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Startscherm bewerken"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Instellingen start"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index c3345a0..555913e 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"ଫୋଲ୍ଡରର ନାମ <xliff:g id="NAME">%1$s</xliff:g>କୁ ବଦଳାଗଲା"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"ଫୋଲ୍ଡର୍: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ଆଇଟମଗୁଡ଼ିକ"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"ଫୋଲ୍ଡର୍: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> କିମ୍ବା ଅଧିକ ଆଇଟମ୍"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"ୱାଲପେପର୍‌"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"ୱାଲପେପର ଏବଂ ଷ୍ଟାଇଲ"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"ହୋମ ସ୍କ୍ରିନକୁ ଏଡିଟ କରନ୍ତୁ"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"ହୋମ ସେଟିଂସ"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index f9a66df..e6e3f07 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"ਫੋਲਡਰ ਨੂੰ <xliff:g id="NAME">%1$s</xliff:g> ਮੁੜ ਨਾਮ ਦਿੱਤਾ ਗਿਆ"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"ਫੋਲਡਰ: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ਆਈਟਮਾਂ"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"ਫੋਲਡਰ: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ਜਾਂ ਹੋਰ ਆਈਟਮਾਂ"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"ਵਾਲਪੇਪਰ"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"ਵਾਲਪੇਪਰ ਅਤੇ ਸਟਾਈਲ"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"ਹੋਮ ਸਕ੍ਰੀਨ ਦਾ ਸੰਪਾਦਨ ਕਰੋ"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"ਹੋਮ ਸੈਟਿੰਗਾਂ"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 4f3abe3..739ef6c 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Nazwa folderu zmieniona na <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> elementy"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, liczba elementów: <xliff:g id="SIZE">%2$d</xliff:g> lub więcej"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Tapety"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Tapeta i styl"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Edytuj ekran główny"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Ustawienia ekranu głównego"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 0acf63e..76c65a4 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Nome de pasta alterado para <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Pasta: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> itens"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Pasta: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ou mais itens"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Imagens de fundo"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Imagem fundo/estilo"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Editar ecrã principal"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Definições de início"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index e0c9b84..e23f459 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Pasta renomeada para <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Pasta: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> itens"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Pasta: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ou mais itens"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Planos de fundo"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Plano de fundo e estilo"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Editar tela inicial"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Configurações da tela inicial"</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index d6c5e5e..fc9c5d1 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Dosar redenumit <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Dosar: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> elemente"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Dosar: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> sau mai multe elemente"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Imagini de fundal"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Imagine de fundal și stil"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Editează ecranul de pornire"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Setări ecran de pornire"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 6315ee5..b3ff865 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Папка переименована в \"<xliff:g id="NAME">%1$s</xliff:g>\""</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Папка \"<xliff:g id="NAME">%1$s</xliff:g>\" (объектов: <xliff:g id="SIZE">%2$d</xliff:g>)"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Папка \"<xliff:g id="NAME">%1$s</xliff:g>\" (объектов: <xliff:g id="SIZE">%2$d</xliff:g> или больше)"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Обои"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Обои и стиль"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Изменить главный экран"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Главный экран"</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 15bc4a7..6f93989 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"<xliff:g id="NAME">%1$s</xliff:g> වෙත ෆෝල්ඩරය නැවත නම් කෙරිණි"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"ෆෝල්ඩරය: <xliff:g id="NAME">%1$s</xliff:g>, අයිතම <xliff:g id="SIZE">%2$d</xliff:g>"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"ෆෝල්ඩර: <xliff:g id="NAME">%1$s</xliff:g>, අයිතම <xliff:g id="SIZE">%2$d</xliff:g>ක් හෝ වැඩි ගණනක්"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"වෝල්පේපර"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"වෝල්පේපරය සහ මෝස්තරය"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"මුල් තිරය සංස්කරණය කරන්න"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"නිවසේ සැකසීම්"</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 2028c35..742d726 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Priečinok bol premenovaný na <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Priečinok: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> položky"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Priečinok: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> alebo viac položiek"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Tapety"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Tapeta a štýl"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Upraviť plochu"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Nastavenia plochy"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index e6197c7..57289d3 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Mapa je preimenovana v <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Mapa: <xliff:g id="NAME">%1$s</xliff:g>, št. elementov: <xliff:g id="SIZE">%2$d</xliff:g>"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Mapa: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ali več elementov"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Ozadja"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Zaslonsko ozadje in slog"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Urejanje začetnega zaslona"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Domače nastavitve"</string>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index c6b8e7c..7dee8aa 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Dosja u riemërtua në <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Dosja: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> artikuj"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Dosja: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ose më shumë artikuj"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Imazhet e sfondit"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Imazhi i sfondit dhe stili"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Modifiko ekranin bazë"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Cilësimet e ekranit bazë"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index a4d7795..7121c53 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Фолдер је преименован у <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Фолдер: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ставке"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Фолдер: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> или више ставки"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Позадине"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Позадина и стил"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Измени почетни екран"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Подешавања почетног екрана"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index cd946e7..aeaf708 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Mappen har bytt namn till <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Mapp: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> objekt"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Mapp: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> eller fler objekt"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Bakgrunder"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Bakgrund och utseende"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Redigera startskärm"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Startinställningar"</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 2686f8a..5be3bf3 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Folda imebadilishwa jina kuwa <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Folda: <xliff:g id="NAME">%1$s</xliff:g>, vipengee <xliff:g id="SIZE">%2$d</xliff:g>"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Folda: <xliff:g id="NAME">%1$s</xliff:g>, vipengee <xliff:g id="SIZE">%2$d</xliff:g> au zaidi"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Mandhari"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Mandhari na mtindo"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Badilisha Skrini ya Kwanza"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Mipangilio ya mwanzo"</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 8d9b2cc..ec287f5 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"ஃபோல்டர் <xliff:g id="NAME">%1$s</xliff:g> என மறுபெயரிடப்பட்டது"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"ஃபோல்டர்: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ஃபைல்கள்"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"ஃபோல்டர்: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> அல்லது அதற்கு அதிகமான ஃபைல்கள்"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"வால்பேப்பர்கள்"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"வால்பேப்பர் &amp; ஸ்டைல்"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"முகப்புத் திரையில் மாற்று"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"முகப்பு அமைப்புகள்"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 08e633d..032a241 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"ఫోల్డర్ పేరు <xliff:g id="NAME">%1$s</xliff:g>గా మార్చబడింది"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"ఫోల్డర్: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> ఐటెమ్‌లు"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"ఫోల్డర్: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> లేదా అంతకంటే ఎక్కువ ఐటెమ్‌లు"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"వాల్‌పేపర్‌లు"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"వాల్‌పేపర్ &amp; స్టయిల్"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"మొదటి స్క్రీన్‌ను ఎడిట్ చేయండి"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"మొదటి స్క్రీన్ సెట్టింగ్‌లు"</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 4b64993..fe79b12 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"เปลี่ยนชื่อโฟลเดอร์เป็น <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"โฟลเดอร์: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> รายการ"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"โฟลเดอร์: <xliff:g id="NAME">%1$s</xliff:g>, อย่างน้อย <xliff:g id="SIZE">%2$d</xliff:g> รายการ"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"วอลเปเปอร์"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"วอลเปเปอร์และรูปแบบ"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"แก้ไขหน้าจอหลัก"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"การตั้งค่าหน้าจอหลัก"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 61f9227..fe4aac1 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Pinalitan ang pangalan ng folder ng <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> (na) item"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Folder: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> o higit pang item"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Mga Wallpaper"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Wallpaper &amp; istilo"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"I-edit ang Home Screen"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Mga setting ng Home"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index ef28f18..8a836e6 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Klasörün adı <xliff:g id="NAME">%1$s</xliff:g> olarak değiştirildi"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Klasör: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> öğe"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Klasör: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> veya daha fazla öğe"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Duvar Kağıtları"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Duvar kağıdı ve stil"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Ana ekranı düzenleyin"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Ana ekran ayarları"</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 822936b..5dc5d8e 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Папку перейменовано на <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Папка \"<xliff:g id="NAME">%1$s</xliff:g>\", елементів: <xliff:g id="SIZE">%2$d</xliff:g>"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Папка \"<xliff:g id="NAME">%1$s</xliff:g>\", елементів: <xliff:g id="SIZE">%2$d</xliff:g> або більше"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Фонові малюнки"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Оформлення і стиль"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Редагувати головний екран"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Налаштування головного екрана"</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 1fffcfd..3994810 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"فولڈر کا نام تبدیل کر کے <xliff:g id="NAME">%1$s</xliff:g> کر دیا گیا"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"فولڈر: <xliff:g id="NAME">%1$s</xliff:g>، <xliff:g id="SIZE">%2$d</xliff:g> آئٹمز"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"فولڈر: <xliff:g id="NAME">%1$s</xliff:g>، <xliff:g id="SIZE">%2$d</xliff:g> یا مزید آئٹمز"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"وال پیپرز"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"وال پیپر اور طرز"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"ہوم اسکرین میں ترمیم کریں"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"ہوم ترتیبات"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index db381ca..405c4ef 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Jild nomi <xliff:g id="NAME">%1$s</xliff:g>ga o‘zgartirildi"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Jild: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> fayllar"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Jild: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> va undan ortiq fayllar"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Fon rasmlari"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Fon rasmi va uslubi"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Bosh ekranni tahrirlash"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Bosh ekran sozlamalari"</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index b73bc61..075a7b0 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Đã đổi tên thư mục thành <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Thư mục: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> mục"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Thư mục: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> mục trở lên"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Hình nền"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Hình nền và phong cách"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Chỉnh sửa Màn hình chính"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Cài đặt màn hình chính"</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 20c8ea9..e525c02 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"已将文件夹重命名为“<xliff:g id="NAME">%1$s</xliff:g>”"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"文件夹:<xliff:g id="NAME">%1$s</xliff:g>,<xliff:g id="SIZE">%2$d</xliff:g> 个项目"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"文件夹:<xliff:g id="NAME">%1$s</xliff:g>,<xliff:g id="SIZE">%2$d</xliff:g> 个或更多项目"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"壁纸"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"壁纸与个性化"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"修改主屏幕"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"主屏幕设置"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index 75f191e..0f0960e 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"資料夾已重新命名為「<xliff:g id="NAME">%1$s</xliff:g>」"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"資料夾:<xliff:g id="NAME">%1$s</xliff:g>,<xliff:g id="SIZE">%2$d</xliff:g> 個項目"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"資料夾:<xliff:g id="NAME">%1$s</xliff:g>,<xliff:g id="SIZE">%2$d</xliff:g> 個或以上的項目"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"桌布"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"桌布和樣式"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"編輯主畫面"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"主畫面設定"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 69d1d90..cf40155 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"已將資料夾重新命名為「<xliff:g id="NAME">%1$s</xliff:g>」"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"資料夾:<xliff:g id="NAME">%1$s</xliff:g>,<xliff:g id="SIZE">%2$d</xliff:g> 個項目"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"資料夾:<xliff:g id="NAME">%1$s</xliff:g>,<xliff:g id="SIZE">%2$d</xliff:g> 個以上的項目"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"桌布"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"桌布和樣式"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"編輯主畫面"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"主畫面設定"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index d9ed3f0..6ccc4be 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -99,7 +99,6 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Ifolda iqanjwe kabusha ngo-<xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Ifolda: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> izinto"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Ifolda: <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> noma izinto eziningi"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Izithombe zangemuva"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Isithombe sangemuva nesitayela"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Hlela Isikrini Sasekhaya"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Amasethingi asekhaya"</string>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index e4650b2..f046eca 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -36,7 +36,6 @@
     <attr name="workspaceShadowColor" format="color" />
     <attr name="workspaceAmbientShadowColor" format="color" />
     <attr name="workspaceKeyShadowColor" format="color" />
-    <attr name="workspaceStatusBarScrim" format="reference" />
     <attr name="widgetsTheme" format="reference" />
     <attr name="iconOnlyShortcutColor" format="color" />
     <attr name="eduHalfSheetBGColor" format="color" />
@@ -86,6 +85,8 @@
             <enum name="taskbar" value="5" />
             <enum name="search_result_tall" value="6" />
             <enum name="search_result_small" value="7" />
+            <enum name="prediction_row" value="8" />
+            <enum name="search_result_app_row" value="9" />
         </attr>
         <attr name="centerVertically" format="boolean" />
     </declare-styleable>
@@ -202,9 +203,24 @@
         <attr name="demoModeLayoutId" format="reference" />
         <attr name="isScalable" format="boolean" />
         <attr name="devicePaddingId" format="reference" />
+
         <!-- File that contains the specs for the workspace.
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="workspaceSpecsId" format="reference" />
+        <attr name="workspaceSpecsTwoPanelId" format="reference" />
+        <!-- File that contains the specs for all apps.
+        Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
+        <attr name="allAppsSpecsId" format="reference" />
+        <attr name="allAppsSpecsTwoPanelId" format="reference" />
+        <!-- File that contains the specs for the workspace.
+        Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
+        <attr name="folderSpecsId" format="reference" />
+        <attr name="folderSpecsTwoPanelId" format="reference" />
+        <!-- File that contains the specs for hotseat bar.
+        Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
+        <attr name="hotseatSpecsId" format="reference" />
+        <attr name="hotseatSpecsTwoPanelId" format="reference" />
+
         <!-- By default all categories are enabled -->
         <attr name="deviceCategory" format="integer">
             <!-- Enable on phone only -->
@@ -244,7 +260,7 @@
     </declare-styleable>
 
     <!--  Responsive grids attributes  -->
-    <declare-styleable name="WorkspaceSpec">
+    <declare-styleable name="ResponsiveSpec">
         <attr name="specType" format="integer">
             <enum name="height" value="0" />
             <enum name="width" value="1" />
@@ -252,10 +268,32 @@
         <attr name="maxAvailableSize" format="dimension" />
     </declare-styleable>
 
-    <declare-styleable name="SpecSize">
+    <declare-styleable name="WorkspaceSpec">
+        <attr name="specType" />
+        <attr name="maxAvailableSize" />
+    </declare-styleable>
+
+    <declare-styleable name="FolderSpec">
+        <attr name="specType" />
+        <attr name="maxAvailableSize" />
+    </declare-styleable>
+
+    <declare-styleable name="AllAppsSpec">
+        <attr name="specType" />
+        <attr name="maxAvailableSize" />
+    </declare-styleable>
+
+    <declare-styleable name="HotseatSpec">
+        <attr name="specType" />
+        <attr name="maxAvailableSize" />
+    </declare-styleable>
+
+    <declare-styleable name="SizeSpec">
         <attr name="fixedSize" format="dimension" />
         <attr name="ofAvailableSpace" format="float" />
         <attr name="ofRemainderSpace" format="float" />
+        <attr name="matchWorkspace" format="boolean" />
+        <attr name="maxSize" format="dimension" />
     </declare-styleable>
 
     <declare-styleable name="ProfileDisplayOption">
@@ -564,4 +602,9 @@
         <!-- The icon drawable of a widget category. -->
         <attr name="sectionDrawable" format="reference" />
     </declare-styleable>
+
+    <declare-styleable name="ArrowTipView">
+        <attr name="arrowTipBackground" format="color" />
+        <attr name="arrowTipTextColor" format="color" />
+    </declare-styleable>
 </resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index 5a6698b..8f9731c 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -84,6 +84,8 @@
     <string name="window_manager_proxy_class" translatable="false"></string>
     <string name="secondary_display_predictions_class" translatable="false"></string>
     <string name="widget_holder_factory_class" translatable="false"></string>
+    <string name="taskbar_search_session_controller_class" translatable="false"></string>
+    <string name="taskbar_model_callbacks_factory_class" translatable="false"></string>
 
     <!-- View ID to use for QSB widget -->
     <item type="id" name="qsb_widget" />
@@ -102,8 +104,6 @@
 
     <!-- Default packages -->
     <string name="wallpaper_picker_package" translatable="false"></string>
-    <string name="custom_activity_picker" translatable="false">
-        com.android.customization.picker.CustomizationPickerActivity</string>
     <string name="local_colors_extraction_class" translatable="false"></string>
     <string name="search_session_manager_class" translatable="false"></string>
 
@@ -217,4 +217,28 @@
     <!-- Whether the floating rotation button should be on the left/right in the device's natural
          orientation -->
     <bool name="floating_rotation_button_position_left">true</bool>
+
+    <!--  Mapping of visual icon size to XML value http://b/235886078  -->
+    <dimen name="iconSize48dp">52dp</dimen>
+    <dimen name="iconSize50dp">55dp</dimen>
+    <dimen name="iconSize52dp">57dp</dimen>
+    <dimen name="iconSize54dp">59dp</dimen>
+    <dimen name="iconSize56dp">61dp</dimen>
+    <dimen name="iconSize58dp">63dp</dimen>
+    <dimen name="iconSize60dp">66dp</dimen>
+    <dimen name="iconSize66dp">72dp</dimen>
+    <dimen name="iconSize72dp">79dp</dimen>
+
+    <!--  Icon size steps in dp  -->
+    <integer-array name="icon_size_steps">
+        <item>@dimen/iconSize48dp</item>
+        <item>@dimen/iconSize50dp</item>
+        <item>@dimen/iconSize52dp</item>
+        <item>@dimen/iconSize54dp</item>
+        <item>@dimen/iconSize56dp</item>
+        <item>@dimen/iconSize58dp</item>
+        <item>@dimen/iconSize60dp</item>
+        <item>@dimen/iconSize66dp</item>
+        <item>@dimen/iconSize72dp</item>
+    </integer-array>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 786088e..1079e00 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -411,6 +411,7 @@
     <dimen name="split_instructions_elevation">1dp</dimen>
     <dimen name="split_instructions_horizontal_padding">24dp</dimen>
     <dimen name="split_instructions_vertical_padding">12dp</dimen>
+    <dimen name="split_instructions_drawable_padding">10dp</dimen>
     <dimen name="split_instructions_bottom_margin_phone_landscape">24dp</dimen>
     <dimen name="split_instructions_bottom_margin_phone_portrait">60dp</dimen>
     
@@ -438,4 +439,8 @@
     <!--  Folder spaces  -->
     <dimen name="folder_top_padding_default">24dp</dimen>
     <dimen name="folder_footer_horiz_padding">20dp</dimen>
+
+    <!-- Default Ime height. Used only for logging purposes.
+    Assume this is default keyboard height in EN locale in case the keyboard height is not known when queried.-->
+    <dimen name="default_ime_height">300dp</dimen>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c2eb373..1b46b4d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -250,8 +250,6 @@
 
     <!-- Strings for the customization mode -->
     <!-- Text for wallpaper change button [CHAR LIMIT=30]-->
-    <string name="wallpaper_button_text">Wallpapers</string>
-    <!-- Text for wallpaper change button [CHAR LIMIT=30]-->
     <string name="styles_wallpaper_button_text">Wallpaper &amp; style</string>
     <!-- Text for edit home screen button [CHAR LIMIT=30]-->
     <string name="edit_home_screen">Edit Home Screen</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index c41f0e8..4cb6414 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -32,7 +32,8 @@
     <style name="LauncherTheme" parent="@style/BaseLauncherTheme">
         <item name="android:textColorSecondary">#DE000000</item>
         <item name="allAppsScrimColor">?attr/materialColorSurfaceDim</item>
-        <item name="allappsHeaderProtectionColor">@color/popup_color_tertiary_light</item>
+        <item name="allappsHeaderProtectionColor">
+            @color/material_color_surface_container_highest</item>
         <item name="allAppsNavBarScrimColor">#66FFFFFF</item>
         <item name="popupColorPrimary">@color/popup_color_primary_light</item>
         <item name="popupColorSecondary">@color/popup_color_secondary_light</item>
@@ -50,7 +51,6 @@
         <item name="workspaceShadowColor">#B0000000</item>
         <item name="workspaceAmbientShadowColor">#40000000</item>
         <item name="workspaceKeyShadowColor">#89000000</item>
-        <item name="workspaceStatusBarScrim">@drawable/workspace_bg</item>
         <item name="widgetsTheme">@style/WidgetContainerTheme</item>
         <item name="folderPaginationColor">@color/folder_pagination_color_light</item>
         <item name="folderPreviewColor">@color/folder_preview_light</item>
@@ -137,7 +137,6 @@
         <item name="workspaceAmbientShadowColor">@android:color/transparent</item>
         <item name="workspaceKeyShadowColor">@android:color/transparent</item>
         <item name="isWorkspaceDarkText">true</item>
-        <item name="workspaceStatusBarScrim">@null</item>
         <item name="workspaceAccentColor">@color/workspace_accent_color_dark</item>
         <item name="dropTargetHoverTextColor">@color/drop_target_hover_text_color_dark</item>
         <item name="dropTargetHoverButtonColor">@color/drop_target_hover_button_color_dark</item>
@@ -151,6 +150,7 @@
         <item name="android:colorControlHighlight">#19FFFFFF</item>
         <item name="android:colorPrimary">#FF212121</item>
         <item name="allAppsScrimColor">?attr/materialColorSurfaceDim</item>
+        <item name="allappsHeaderProtectionColor">@color/material_color_surface_container_low</item>
         <item name="allAppsNavBarScrimColor">#80000000</item>
         <item name="popupColorPrimary">@color/popup_color_primary_dark</item>
         <item name="popupColorSecondary">@color/popup_color_secondary_dark</item>
@@ -188,7 +188,6 @@
         <item name="workspaceAmbientShadowColor">@android:color/transparent</item>
         <item name="workspaceKeyShadowColor">@android:color/transparent</item>
         <item name="isWorkspaceDarkText">true</item>
-        <item name="workspaceStatusBarScrim">@null</item>
         <item name="workspaceAccentColor">@color/workspace_accent_color_dark</item>
         <item name="dropTargetHoverTextColor">@color/drop_target_hover_text_color_dark</item>
         <item name="dropTargetHoverButtonColor">@color/drop_target_hover_button_color_dark</item>
@@ -426,4 +425,8 @@
         <item name="horizontalPadding">16dp</item>
     </style>
 
+    <style name="ArrowTipStyle">
+        <item name="arrowTipBackground">@color/arrow_tip_view_bg</item>
+        <item name="arrowTipTextColor">@color/arrow_tip_view_content</item>
+    </style>
 </resources>
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 31f9bfe..b845c88 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -133,6 +133,8 @@
     public static final int TYPE_TASKBAR_OVERLAYS =
             TYPE_TASKBAR_ALL_APPS | TYPE_TASKBAR_EDUCATION_DIALOG;
 
+    public static final int TYPE_ALL_EXCEPT_ON_BOARD_POPUP = TYPE_ALL & ~TYPE_ON_BOARD_POPUP;
+
     protected boolean mIsOpen;
 
     public AbstractFloatingView(Context context, AttributeSet attrs) {
diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java
index ede7e2f..c7cdfa8 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -17,6 +17,8 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
+import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch;
 
 import android.content.ComponentName;
 import android.content.ContentValues;
@@ -30,7 +32,6 @@
 import android.content.res.Resources.NotFoundException;
 import android.content.res.XmlResourceParser;
 import android.database.sqlite.SQLiteDatabase;
-import android.net.Uri;
 import android.os.Bundle;
 import android.os.Process;
 import android.text.TextUtils;
@@ -44,7 +45,6 @@
 import androidx.annotation.WorkerThread;
 import androidx.annotation.XmlRes;
 
-import com.android.launcher3.LauncherProvider.SqlArguments;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -619,9 +619,7 @@
             // failed to add, and less than 2 were actually added
             if (folderItems.size() < 2) {
                 // Delete the folder
-                Uri uri = Favorites.getContentUri(folderId);
-                SqlArguments args = new SqlArguments(uri, null, null);
-                mDb.delete(args.table, args.where, args.args);
+                mDb.delete(TABLE_NAME, itemIdMatch(folderId), null);
                 addedId = -1;
 
                 // If we have a single item, promote it to where the folder
@@ -634,7 +632,7 @@
                     copyInteger(myValues, childValues, Favorites.CELLY);
 
                     addedId = folderItems.get(0);
-                    mDb.update(Favorites.TABLE_NAME, childValues,
+                    mDb.update(TABLE_NAME, childValues,
                             Favorites._ID + "=" + addedId, null);
                 }
             }
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 8876a1b..808cf70 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -17,12 +17,7 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR;
 
-import android.app.WallpaperColors;
-import android.app.WallpaperManager;
-import android.app.WallpaperManager.OnColorsChangedListener;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Point;
@@ -32,6 +27,7 @@
 import android.view.Display;
 import android.view.View;
 
+import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
@@ -41,9 +37,11 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
 import com.android.launcher3.util.DisplayController.Info;
+import com.android.launcher3.util.OnColorHintListener;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.util.WallpaperColorHints;
 import com.android.launcher3.util.WindowBounds;
 
 /**
@@ -51,7 +49,7 @@
  */
 @SuppressWarnings("NewApi")
 public abstract class BaseDraggingActivity extends BaseActivity
-        implements OnColorsChangedListener, DisplayInfoChangeListener {
+        implements OnColorHintListener, DisplayInfoChangeListener {
 
     private static final String TAG = "BaseDraggingActivity";
 
@@ -63,8 +61,7 @@
     protected boolean mIsSafeModeEnabled;
 
     private Runnable mOnStartCallback;
-    private RunnableList mOnResumeCallbacks = new RunnableList();
-
+    private final RunnableList mOnResumeCallbacks = new RunnableList();
     private int mThemeRes = R.style.AppTheme;
 
     @Override
@@ -76,10 +73,7 @@
         DisplayController.INSTANCE.get(this).addChangeListener(this);
 
         // Update theme
-        if (Utilities.ATLEAST_P) {
-            THREAD_POOL_EXECUTOR.execute(() -> getSystemService(WallpaperManager.class)
-                    .addOnColorsChangedListener(this, MAIN_EXECUTOR.getHandler()));
-        }
+        WallpaperColorHints.get(this).registerOnColorHintsChangedListener(this);
         int themeRes = Themes.getActivityThemeRes(this);
         if (themeRes != mThemeRes) {
             mThemeRes = themeRes;
@@ -97,8 +91,9 @@
         mOnResumeCallbacks.add(callback);
     }
 
+    @MainThread
     @Override
-    public void onColorsChanged(WallpaperColors wallpaperColors, int which) {
+    public void onColorHintsChanged(int colorHints) {
         updateTheme();
     }
 
@@ -175,10 +170,8 @@
     @Override
     protected void onDestroy() {
         super.onDestroy();
-        if (Utilities.ATLEAST_P) {
-            getSystemService(WallpaperManager.class).removeOnColorsChangedListener(this);
-        }
         DisplayController.INSTANCE.get(this).removeChangeListener(this);
+        WallpaperColorHints.get(this).unregisterOnColorsChangedListener(this);
     }
 
     public void runOnceOnStart(Runnable action) {
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index f920d75..a539148 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V2;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_ICON_LABEL_AUTO_SCALING;
 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
@@ -94,11 +95,13 @@
         IconLabelDotView, DraggableView, Reorderable {
 
     private static final int DISPLAY_WORKSPACE = 0;
-    private static final int DISPLAY_ALL_APPS = 1;
+    public static final int DISPLAY_ALL_APPS = 1;
     private static final int DISPLAY_FOLDER = 2;
     protected static final int DISPLAY_TASKBAR = 5;
-    private static final int DISPLAY_SEARCH_RESULT = 6;
-    private static final int DISPLAY_SEARCH_RESULT_SMALL = 7;
+    public static final int DISPLAY_SEARCH_RESULT = 6;
+    public static final int DISPLAY_SEARCH_RESULT_SMALL = 7;
+    public static final int DISPLAY_PREDICTION_ROW = 8;
+    public static final int DISPLAY_SEARCH_RESULT_APP_ROW = 9;
 
     private static final float MIN_LETTER_SPACING = -0.05f;
     private static final int MAX_SEARCH_LOOP_COUNT = 20;
@@ -151,7 +154,7 @@
 
     private final CheckLongPressHelper mLongPressHelper;
 
-    private final boolean mLayoutHorizontal;
+    private boolean mLayoutHorizontal;
     private final boolean mIsRtl;
     private final int mIconSize;
 
@@ -196,6 +199,7 @@
     public BubbleTextView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         mActivity = ActivityContext.lookupContext(context);
+        FastBitmapDrawable.setFlagHoverEnabled(ENABLE_CURSOR_HOVER_STATES.get());
 
         TypedArray a = context.obtainStyledAttributes(attrs,
                 R.styleable.BubbleTextView, defStyle, 0);
@@ -210,8 +214,9 @@
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx);
             setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
             defaultIconSize = grid.iconSizePx;
-            setCenterVertically(grid.isScalableGrid);
-        } else if (mDisplay == DISPLAY_ALL_APPS) {
+            setCenterVertically(grid.iconCenterVertically);
+        } else if (mDisplay == DISPLAY_ALL_APPS || mDisplay == DISPLAY_PREDICTION_ROW
+                || mDisplay == DISPLAY_SEARCH_RESULT_APP_ROW) {
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx);
             setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx);
             defaultIconSize = grid.allAppsIconSizePx;
@@ -402,7 +407,7 @@
      *  Only if actual text can be displayed in two line, the {@code true} value will be effective.
      */
     protected boolean shouldUseTwoLine() {
-        return (FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get() && mDisplay == DISPLAY_ALL_APPS)
+        return  (FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get() && mDisplay == DISPLAY_ALL_APPS)
                 || (FeatureFlags.ENABLE_TWOLINE_DEVICESEARCH.get()
                 && mDisplay == DISPLAY_SEARCH_RESULT);
     }
@@ -424,10 +429,10 @@
         }
     }
 
-    /** This is used for testing to forcefully set the display to ALL_APPS */
+    /** This is used for testing to forcefully set the display. */
     @VisibleForTesting
-    public void setDisplayAllApps() {
-        mDisplay = DISPLAY_ALL_APPS;
+    public void setDisplay(int display) {
+        mDisplay = display;
     }
 
     /**
@@ -631,6 +636,11 @@
         }
     }
 
+    @VisibleForTesting
+    public boolean getForceHideDot() {
+        return mForceHideDot;
+    }
+
     private boolean hasDot() {
         return mDotInfo != null;
     }
@@ -660,6 +670,18 @@
     }
 
     /**
+     * Sets whether the layout is horizontal.
+     */
+    public void setLayoutHorizontal(boolean layoutHorizontal) {
+        if (mLayoutHorizontal == layoutHorizontal) {
+            return;
+        }
+
+        mLayoutHorizontal = layoutHorizontal;
+        applyCompoundDrawables(getIconOrTransparentColor());
+    }
+
+    /**
      * Sets whether to vertically center the content.
      */
     public void setCenterVertically(boolean centerVertically) {
@@ -985,10 +1007,14 @@
         if (!mIsIconVisible) {
             resetIconScale();
         }
-        Drawable icon = visible ? mIcon : new ColorDrawable(Color.TRANSPARENT);
+        Drawable icon = getIconOrTransparentColor();
         applyCompoundDrawables(icon);
     }
 
+    private Drawable getIconOrTransparentColor() {
+        return mIsIconVisible ? mIcon : new ColorDrawable(Color.TRANSPARENT);
+    }
+
     /** Sets the icon visual state to disabled or not. */
     public void setIconDisabled(boolean isDisabled) {
         if (mIcon != null) {
@@ -1080,8 +1106,13 @@
     }
 
     public boolean isDisplaySearchResult() {
-        return mDisplay == DISPLAY_SEARCH_RESULT ||
-                mDisplay == DISPLAY_SEARCH_RESULT_SMALL;
+        return mDisplay == DISPLAY_SEARCH_RESULT
+                || mDisplay == DISPLAY_SEARCH_RESULT_SMALL
+                || mDisplay == DISPLAY_SEARCH_RESULT_APP_ROW;
+    }
+
+    public int getIconDisplay() {
+        return mDisplay;
     }
 
     @Override
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index 108e557..3d715e5 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -35,7 +35,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.launcher3.anim.Interpolators;
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
@@ -258,7 +258,7 @@
 
         dragLayer.animateView(d.dragView, to, scale, 0.1f, 0.1f,
                 DRAG_VIEW_DROP_DURATION,
-                Interpolators.DEACCEL_2, onAnimationEndRunnable,
+                Interpolators.DECELERATE_2, onAnimationEndRunnable,
                 DragLayer.ANIMATION_END_DISAPPEAR, null);
     }
 
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 61b2748..08e5def 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -18,8 +18,8 @@
 
 import static android.animation.ValueAnimator.areAnimatorsEnabled;
 
+import static com.android.app.animation.Interpolators.DECELERATE_1_5;
 import static com.android.launcher3.LauncherState.EDIT_MODE;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
 import static com.android.launcher3.dragndrop.DraggableView.DRAGGABLE_ICON;
 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_BOUNCE_OFFSET;
@@ -60,9 +60,9 @@
 import androidx.core.graphics.ColorUtils;
 import androidx.core.view.ViewCompat;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
 import com.android.launcher3.celllayout.CellPosMapper.CellPos;
 import com.android.launcher3.celllayout.ReorderAlgorithm;
@@ -245,6 +245,7 @@
         // the user where a dragged item will land when dropped.
         setWillNotDraw(false);
         setClipToPadding(false);
+        setClipChildren(false);
         mActivity = ActivityContext.lookupContext(context);
         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
 
@@ -272,7 +273,7 @@
         mReorderPreviewAnimationMagnitude = (REORDER_PREVIEW_MAGNITUDE * deviceProfile.iconSizePx);
 
         // Initialize the data structures used for the drag visualization.
-        mEaseOutInterpolator = Interpolators.DEACCEL_2_5; // Quint ease out
+        mEaseOutInterpolator = Interpolators.DECELERATE_QUINT; // Quint ease out
         mDragCell[0] = mDragCell[1] = -1;
         mDragCellSpan[0] = mDragCellSpan[1] = -1;
         for (int i = 0; i < mDragOutlines.length; i++) {
@@ -382,8 +383,7 @@
     private void resetCellSizeInternal(DeviceProfile deviceProfile) {
         switch (mContainerType) {
             case FOLDER:
-                mBorderSpace = new Point(deviceProfile.folderCellLayoutBorderSpacePx,
-                        deviceProfile.folderCellLayoutBorderSpacePx);
+                mBorderSpace = new Point(deviceProfile.folderCellLayoutBorderSpacePx);
                 break;
             case HOTSEAT:
                 mBorderSpace = new Point(deviceProfile.hotseatBorderSpace,
@@ -1606,7 +1606,7 @@
             ValueAnimator va = ObjectAnimator.ofFloat(this, ANIMATION_PROGRESS,
                     animationProgress, 0);
             a = va;
-            a.setInterpolator(DEACCEL_1_5);
+            a.setInterpolator(DECELERATE_1_5);
             a.setDuration(REORDER_ANIMATION_DURATION);
             a.start();
         }
@@ -2736,11 +2736,13 @@
     }
 
     public boolean isOccupied(int x, int y) {
-        if (x < mCountX && y < mCountY) {
+        if (x >= 0 && x < mCountX && y >= 0 && y < mCountY) {
             return mOccupied.cells[x][y];
-        } else {
+        }
+        if (BuildConfig.IS_STUDIO_BUILD) {
             throw new RuntimeException("Position exceeds the bound of this CellLayout");
         }
+        return true;
     }
 
     @Override
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index fa1cbd6..936229a 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -16,13 +16,13 @@
 
 package com.android.launcher3;
 
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.InvariantDeviceProfile.INDEX_DEFAULT;
 import static com.android.launcher3.InvariantDeviceProfile.INDEX_LANDSCAPE;
 import static com.android.launcher3.InvariantDeviceProfile.INDEX_TWO_PANEL_LANDSCAPE;
 import static com.android.launcher3.InvariantDeviceProfile.INDEX_TWO_PANEL_PORTRAIT;
 import static com.android.launcher3.Utilities.dpiFromPx;
 import static com.android.launcher3.Utilities.pxFromSp;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_MULTI_DISPLAY_PARTIAL_DEPTH;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
 import static com.android.launcher3.icons.GraphicsUtils.getShapePath;
@@ -53,13 +53,20 @@
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconNormalizer;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.responsive.AllAppsSpecs;
+import com.android.launcher3.responsive.CalculatedAllAppsSpec;
+import com.android.launcher3.responsive.CalculatedFolderSpec;
+import com.android.launcher3.responsive.CalculatedHotseatSpec;
+import com.android.launcher3.responsive.CalculatedWorkspaceSpec;
+import com.android.launcher3.responsive.FolderSpecs;
+import com.android.launcher3.responsive.HotseatSpecs;
+import com.android.launcher3.responsive.WorkspaceSpecs;
 import com.android.launcher3.uioverrides.ApiWrapper;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
+import com.android.launcher3.util.IconSizeSteps;
 import com.android.launcher3.util.ResourceHelper;
 import com.android.launcher3.util.WindowBounds;
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec;
-import com.android.launcher3.workspace.WorkspaceSpecs;
 
 import java.io.PrintWriter;
 import java.util.Locale;
@@ -80,6 +87,7 @@
     public final InvariantDeviceProfile inv;
     private final Info mInfo;
     private final DisplayMetrics mMetrics;
+    private final IconSizeSteps mIconSizeSteps;
 
     // Device properties
     public final boolean isTablet;
@@ -104,14 +112,18 @@
 
     public final float aspectRatio;
 
-    public final boolean isScalableGrid;
+    private final boolean mIsScalableGrid;
     private final int mTypeIndex;
 
     // Responsive grid
     private final boolean mIsResponsiveGrid;
-    private WorkspaceSpecs mWorkspaceSpecs;
     private CalculatedWorkspaceSpec mResponsiveWidthSpec;
     private CalculatedWorkspaceSpec mResponsiveHeightSpec;
+    private CalculatedAllAppsSpec mAllAppsResponsiveWidthSpec;
+    private CalculatedAllAppsSpec mAllAppsResponsiveHeightSpec;
+    private CalculatedFolderSpec mResponsiveFolderWidthSpec;
+    private CalculatedFolderSpec mResponsiveFolderHeightSpec;
+    private CalculatedHotseatSpec mResponsiveHotseatSpec;
 
     /**
      * The maximum amount of left/right workspace padding as a percentage of the screen width.
@@ -152,14 +164,15 @@
     public int iconSizePx;
     public int iconTextSizePx;
     public int iconDrawablePaddingPx;
-    public int iconDrawablePaddingOriginalPx;
+    private final int mIconDrawablePaddingOriginalPx;
+    public boolean iconCenterVertically;
 
     public float cellScaleToFit;
     public int cellWidthPx;
     public int cellHeightPx;
     public int workspaceCellPaddingXPx;
 
-    public int cellYPaddingPx;
+    public int cellYPaddingPx = -1;
 
     // Folder
     public float folderLabelTextScale;
@@ -169,7 +182,7 @@
     public int folderIconOffsetYPx;
 
     // Folder content
-    public int folderCellLayoutBorderSpacePx;
+    public Point folderCellLayoutBorderSpacePx;
     public int folderContentPaddingLeftRight;
     public int folderContentPaddingTop;
 
@@ -281,6 +294,7 @@
     public final int taskbarIconSize;
     // If true, used to layout taskbar in 3 button navigation mode.
     public final boolean startAlignTaskbar;
+    public final boolean isTransientTaskbar;
 
     // DragController
     public int flingToDeleteThresholdVelocity;
@@ -304,9 +318,12 @@
         mInsets.set(windowBounds.insets);
 
         // TODO(b/241386436): shouldn't change any launcher behaviour
-        mIsResponsiveGrid = inv.workspaceSpecsId != INVALID_RESOURCE_HANDLE;
+        mIsResponsiveGrid = inv.workspaceSpecsId != INVALID_RESOURCE_HANDLE
+                && inv.allAppsSpecsId != INVALID_RESOURCE_HANDLE
+                && inv.folderSpecsId != INVALID_RESOURCE_HANDLE
+                && inv.hotseatSpecsId != INVALID_RESOURCE_HANDLE;
 
-        isScalableGrid = inv.isScalable && !isVerticalBarLayout() && !isMultiWindowMode;
+        mIsScalableGrid = inv.isScalable && !isVerticalBarLayout() && !isMultiWindowMode;
         // Determine device posture.
         mInfo = info;
         isTablet = info.isTablet(windowBounds);
@@ -322,6 +339,8 @@
         final Resources res = context.getResources();
         mMetrics = res.getDisplayMetrics();
 
+        mIconSizeSteps = mIsResponsiveGrid ? new IconSizeSteps(res) : null;
+
         // Determine sizes.
         widthPx = windowBounds.bounds.width();
         heightPx = windowBounds.bounds.height();
@@ -343,15 +362,11 @@
             }
         }
 
-        if (mIsResponsiveGrid) {
-            mWorkspaceSpecs = new WorkspaceSpecs(new ResourceHelper(context, inv.workspaceSpecsId));
-            mResponsiveWidthSpec = mWorkspaceSpecs.getCalculatedWidthSpec(inv.numColumns,
-                    availableWidthPx);
-            mResponsiveHeightSpec = mWorkspaceSpecs.getCalculatedHeightSpec(inv.numRows,
-                    availableHeightPx);
-        }
-
-        if (DisplayController.isTransientTaskbar(context)) {
+        isTransientTaskbar = DisplayController.isTransientTaskbar(context);
+        if (!isTaskbarPresent) {
+            taskbarIconSize = taskbarHeight = stashedTaskbarHeight = taskbarBottomMargin = 0;
+            startAlignTaskbar = false;
+        } else if (isTransientTaskbar) {
             float invTransientIconSizeDp = inv.transientTaskbarIconSize[mTypeIndex];
             taskbarIconSize = pxFromDp(invTransientIconSizeDp, mMetrics);
             taskbarHeight = Math.round((taskbarIconSize * ICON_VISIBLE_AREA_FACTOR)
@@ -373,8 +388,6 @@
         edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
         workspaceContentScale = res.getFloat(R.dimen.workspace_content_scale);
 
-        desiredWorkspaceHorizontalMarginPx = getHorizontalMarginPx(inv, res);
-        desiredWorkspaceHorizontalMarginOriginalPx = desiredWorkspaceHorizontalMarginPx;
         gridVisualizationPaddingX = res.getDimensionPixelSize(
                 R.dimen.grid_visualization_horizontal_cell_spacing);
         gridVisualizationPaddingY = res.getDimensionPixelSize(
@@ -407,7 +420,7 @@
 
         folderLabelTextScale = res.getFloat(R.dimen.folder_label_text_scale);
 
-        if (isScalableGrid && inv.folderStyle != INVALID_RESOURCE_HANDLE) {
+        if (mIsScalableGrid && inv.folderStyle != INVALID_RESOURCE_HANDLE) {
             TypedArray folderStyle = context.obtainStyledAttributes(inv.folderStyle,
                     R.styleable.FolderStyle);
             // These are re-set in #updateFolderCellSize if the grid is not scalable
@@ -418,19 +431,19 @@
 
             folderContentPaddingTop = folderStyle.getDimensionPixelSize(
                     R.styleable.FolderStyle_folderTopPadding, 0);
-            folderCellLayoutBorderSpacePx = folderStyle.getDimensionPixelSize(
+
+            int gutter = folderStyle.getDimensionPixelSize(
                     R.styleable.FolderStyle_folderBorderSpace, 0);
+            folderCellLayoutBorderSpacePx = new Point(gutter, gutter);
             folderFooterHeightPx = folderStyle.getDimensionPixelSize(
                     R.styleable.FolderStyle_folderFooterHeight, 0);
             folderStyle.recycle();
-        } else {
-            folderCellLayoutBorderSpacePx = 0;
+        } else if (!mIsResponsiveGrid) {
+            folderCellLayoutBorderSpacePx = new Point(0, 0);
             folderFooterHeightPx = res.getDimensionPixelSize(R.dimen.folder_footer_height_default);
             folderContentPaddingTop = res.getDimensionPixelSize(R.dimen.folder_top_padding_default);
         }
 
-        cellLayoutBorderSpacePx = getCellLayoutBorderSpace(inv);
-        cellLayoutBorderSpaceOriginalPx = new Point(cellLayoutBorderSpacePx);
         allAppsBorderSpacePx = new Point(
                 pxFromDp(inv.allAppsBorderSpaces[mTypeIndex].x, mMetrics),
                 pxFromDp(inv.allAppsBorderSpaces[mTypeIndex].y, mMetrics));
@@ -449,7 +462,7 @@
             cellStyle = context.obtainStyledAttributes(R.style.CellStyleDefault,
                     R.styleable.CellStyle);
         }
-        iconDrawablePaddingOriginalPx = cellStyle.getDimensionPixelSize(
+        mIconDrawablePaddingOriginalPx = cellStyle.getDimensionPixelSize(
                 R.styleable.CellStyle_iconDrawablePadding, 0);
         cellStyle.recycle();
 
@@ -480,7 +493,7 @@
                 || inv.inlineQsb[INDEX_TWO_PANEL_LANDSCAPE]
                 : inv.inlineQsb[INDEX_DEFAULT] || inv.inlineQsb[INDEX_LANDSCAPE])
                 && hotseatQsbHeight > 0;
-        isQsbInline = isScalableGrid && inv.inlineQsb[mTypeIndex] && canQsbInline;
+        isQsbInline = mIsScalableGrid && inv.inlineQsb[mTypeIndex] && canQsbInline;
 
         areNavButtonsInline = isTaskbarPresent && !isGestureMode;
         numShownHotseatIcons =
@@ -491,7 +504,17 @@
 
         int hotseatBarBottomSpace = pxFromDp(inv.hotseatBarBottomSpace[mTypeIndex], mMetrics);
         int minQsbMargin = res.getDimensionPixelSize(R.dimen.min_qsb_margin);
-        hotseatQsbSpace = pxFromDp(inv.hotseatQsbSpace[mTypeIndex], mMetrics);
+
+        if (mIsResponsiveGrid) {
+            HotseatSpecs hotseatSpecs =
+                    HotseatSpecs.create(new ResourceHelper(context,
+                            isTwoPanels ? inv.hotseatSpecsTwoPanelId : inv.hotseatSpecsId));
+            mResponsiveHotseatSpec = hotseatSpecs.getCalculatedHeightSpec(heightPx);
+            hotseatQsbSpace = mResponsiveHotseatSpec.getHotseatQsbSpace();
+        } else {
+            hotseatQsbSpace = pxFromDp(inv.hotseatQsbSpace[mTypeIndex], mMetrics);
+        }
+
         // Have a little space between the inset and the QSB
         if (mInsets.bottom + minQsbMargin > hotseatBarBottomSpace) {
             int availableSpace = hotseatQsbSpace - (mInsets.bottom - hotseatBarBottomSpace);
@@ -535,6 +558,44 @@
             hotseatBarEndOffset = 0;
         }
 
+        // Needs to be calculated after hotseatBarSizePx is correct,
+        // for the available height to be correct
+        if (mIsResponsiveGrid) {
+            WorkspaceSpecs workspaceSpecs = WorkspaceSpecs.create(
+                    new ResourceHelper(context,
+                            isTwoPanels ? inv.workspaceSpecsTwoPanelId : inv.workspaceSpecsId));
+            int availableResponsiveWidth =
+                    availableWidthPx - (isVerticalBarLayout() ? hotseatBarSizePx : 0);
+            int numColumns = getPanelCount() * inv.numColumns;
+            // don't use availableHeightPx because it subtracts bottom padding,
+            // but the workspace go behind it
+            int availableResponsiveHeight =
+                    heightPx - mInsets.top - (isVerticalBarLayout() ? 0 : hotseatBarSizePx);
+            mResponsiveWidthSpec = workspaceSpecs.getCalculatedWidthSpec(numColumns,
+                    availableResponsiveWidth);
+            mResponsiveHeightSpec = workspaceSpecs.getCalculatedHeightSpec(inv.numRows,
+                    availableResponsiveHeight);
+
+            AllAppsSpecs allAppsSpecs = AllAppsSpecs.create(
+                    new ResourceHelper(context,
+                            isTwoPanels ? inv.allAppsSpecsTwoPanelId : inv.allAppsSpecsId));
+            mAllAppsResponsiveWidthSpec = allAppsSpecs.getCalculatedWidthSpec(numColumns,
+                    mResponsiveWidthSpec.getAvailableSpace(), mResponsiveWidthSpec);
+            mAllAppsResponsiveHeightSpec = allAppsSpecs.getCalculatedHeightSpec(inv.numRows,
+                    mResponsiveHeightSpec.getAvailableSpace(), mResponsiveHeightSpec);
+
+            FolderSpecs folderSpecs = FolderSpecs.create(
+                    new ResourceHelper(context,
+                            isTwoPanels ? inv.folderSpecsTwoPanelId : inv.folderSpecsId));
+            mResponsiveFolderWidthSpec = folderSpecs.getCalculatedWidthSpec(inv.numFolderColumns,
+                    mResponsiveWidthSpec.getAvailableSpace(), mResponsiveWidthSpec);
+            mResponsiveFolderHeightSpec = folderSpecs.getCalculatedHeightSpec(inv.numFolderRows,
+                    mResponsiveHeightSpec.getAvailableSpace(), mResponsiveHeightSpec);
+        }
+
+        desiredWorkspaceHorizontalMarginPx = getHorizontalMarginPx(inv, res);
+        desiredWorkspaceHorizontalMarginOriginalPx = desiredWorkspaceHorizontalMarginPx;
+
         overviewTaskMarginPx = res.getDimensionPixelSize(R.dimen.overview_task_margin);
         overviewTaskIconSizePx = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_size);
         overviewTaskIconDrawableSizePx =
@@ -542,7 +603,9 @@
         overviewTaskIconDrawableSizeGridPx =
                 res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_drawable_size_grid);
         overviewTaskThumbnailTopMarginPx = overviewTaskIconSizePx + overviewTaskMarginPx;
-        overviewActionsTopMarginPx = res.getDimensionPixelSize(R.dimen.overview_actions_top_margin);
+        // Don't add margin with floating search bar to minimize risk of overlapping.
+        overviewActionsTopMarginPx = FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get() ? 0
+                : res.getDimensionPixelSize(R.dimen.overview_actions_top_margin);
         overviewPageSpacing = res.getDimensionPixelSize(R.dimen.overview_page_spacing);
         overviewActionsButtonSpacing = res.getDimensionPixelSize(
                 R.dimen.overview_actions_button_spacing);
@@ -555,21 +618,7 @@
         // Calculate all of the remaining variables.
         extraSpace = updateAvailableDimensions(res);
 
-        // Now that we have all of the variables calculated, we can tune certain sizes.
-        if (isScalableGrid && inv.devicePaddingId != INVALID_RESOURCE_HANDLE) {
-            // Paddings were created assuming no scaling, so we first unscale the extra space.
-            int unscaledExtraSpace = (int) (extraSpace / cellScaleToFit);
-            DevicePaddings devicePaddings = new DevicePaddings(context, inv.devicePaddingId);
-            DevicePadding padding = devicePaddings.getDevicePadding(unscaledExtraSpace);
-            maxEmptySpace = padding.getMaxEmptySpacePx();
-
-            int paddingWorkspaceTop = padding.getWorkspaceTopPadding(unscaledExtraSpace);
-            int paddingWorkspaceBottom = padding.getWorkspaceBottomPadding(unscaledExtraSpace);
-            int paddingHotseatBottom = padding.getHotseatBottomPadding(unscaledExtraSpace);
-
-            workspaceTopPadding = Math.round(paddingWorkspaceTop * cellScaleToFit);
-            workspaceBottomPadding = Math.round(paddingWorkspaceBottom * cellScaleToFit);
-        }
+        calculateAndSetWorkspaceVerticalPadding(context, inv, extraSpace);
 
         int cellLayoutPadding =
                 isTwoPanels ? cellLayoutBorderSpacePx.x / 2 : res.getDimensionPixelSize(
@@ -588,6 +637,10 @@
         // Hotseat and QSB width depends on updated cellSize and workspace padding
         recalculateHotseatWidthAndBorderSpace();
 
+        if (mIsResponsiveGrid && isVerticalBarLayout()) {
+            hotseatBorderSpace = cellLayoutBorderSpacePx.y;
+        }
+
         // AllApps height calculation depends on updated cellSize
         if (isTablet) {
             int collapseHandleHeight =
@@ -650,19 +703,44 @@
     }
 
     private int getHorizontalMarginPx(InvariantDeviceProfile idp, Resources res) {
+        if (mIsResponsiveGrid) {
+            return mResponsiveWidthSpec.getStartPaddingPx();
+        }
+
         if (isVerticalBarLayout()) {
             return 0;
         }
 
-        return isScalableGrid
+        return mIsScalableGrid
                 ? pxFromDp(idp.horizontalMargin[mTypeIndex], mMetrics)
                 : res.getDimensionPixelSize(R.dimen.dynamic_grid_left_right_margin);
     }
 
+    private void calculateAndSetWorkspaceVerticalPadding(Context context,
+            InvariantDeviceProfile inv,
+            int extraSpace) {
+        if (mIsResponsiveGrid) {
+            workspaceTopPadding = mResponsiveHeightSpec.getStartPaddingPx();
+            workspaceBottomPadding = mResponsiveHeightSpec.getEndPaddingPx();
+        } else if (mIsScalableGrid && inv.devicePaddingId != INVALID_RESOURCE_HANDLE) {
+            // Paddings were created assuming no scaling, so we first unscale the extra space.
+            int unscaledExtraSpace = (int) (extraSpace / cellScaleToFit);
+            DevicePaddings devicePaddings = new DevicePaddings(context, inv.devicePaddingId);
+            DevicePadding padding = devicePaddings.getDevicePadding(unscaledExtraSpace);
+            maxEmptySpace = padding.getMaxEmptySpacePx();
+
+            int paddingWorkspaceTop = padding.getWorkspaceTopPadding(unscaledExtraSpace);
+            int paddingWorkspaceBottom = padding.getWorkspaceBottomPadding(unscaledExtraSpace);
+
+            workspaceTopPadding = Math.round(paddingWorkspaceTop * cellScaleToFit);
+            workspaceBottomPadding = Math.round(paddingWorkspaceBottom * cellScaleToFit);
+        }
+    }
+
     /** Updates hotseatCellHeightPx and hotseatBarSizePx */
     private void updateHotseatSizes(int hotseatIconSizePx) {
         // Ensure there is enough space for folder icons, which have a slightly larger radius.
-        hotseatCellHeightPx = (int) Math.ceil(hotseatIconSizePx * ICON_OVERLAP_FACTOR);
+        hotseatCellHeightPx = getIconSizeWithOverlap(hotseatIconSizePx);
 
         if (isVerticalBarLayout()) {
             hotseatBarSizePx = hotseatIconSizePx + hotseatBarSidePaddingStartPx
@@ -683,7 +761,7 @@
      * necessary.
      */
     public void recalculateHotseatWidthAndBorderSpace() {
-        if (!isScalableGrid) return;
+        if (!mIsScalableGrid) return;
 
         int columns = inv.hotseatColumnSpan[mTypeIndex];
         float hotseatWidthPx = getIconToIconWidthForColumns(columns);
@@ -728,7 +806,6 @@
             hotseatBorderSpace = calculateHotseatBorderSpace(maxHotseatIconsWidthPx,
                     (isQsbInline ? 1 : 0) + /* border between nav buttons and first icon */ 1);
         } while (hotseatBorderSpace < mMinHotseatIconSpacePx && numShownHotseatIcons > 1);
-
     }
 
     private Point getCellLayoutBorderSpace(InvariantDeviceProfile idp) {
@@ -736,12 +813,16 @@
     }
 
     private Point getCellLayoutBorderSpace(InvariantDeviceProfile idp, float scale) {
-        if (!isScalableGrid) {
-            return new Point(0, 0);
-        }
+        int horizontalSpacePx = 0;
+        int verticalSpacePx = 0;
 
-        int horizontalSpacePx = pxFromDp(idp.borderSpaces[mTypeIndex].x, mMetrics, scale);
-        int verticalSpacePx = pxFromDp(idp.borderSpaces[mTypeIndex].y, mMetrics, scale);
+        if (mIsResponsiveGrid) {
+            horizontalSpacePx = mResponsiveWidthSpec.getGutterPx();
+            verticalSpacePx = mResponsiveHeightSpec.getGutterPx();
+        } else if (mIsScalableGrid) {
+            horizontalSpacePx = pxFromDp(idp.borderSpaces[mTypeIndex].x, mMetrics, scale);
+            verticalSpacePx = pxFromDp(idp.borderSpaces[mTypeIndex].y, mMetrics, scale);
+        }
 
         return new Point(horizontalSpacePx, verticalSpacePx);
     }
@@ -807,54 +888,29 @@
         float workspaceCellPaddingY = getCellSize().y - iconSizePx - iconDrawablePaddingPx
                 - iconTextHeight;
 
+        if (mIsResponsiveGrid) {
+            // Hide text only if doesn't fit inside the cell for responsive grid
+            if (workspaceCellPaddingY < 0) {
+                iconTextSizePx = 0;
+                iconDrawablePaddingPx = 0;
+                int iconSizeWithOverlap = getIconSizeWithOverlap(iconSizePx);
+                cellYPaddingPx = Math.max(0, getCellSize().y - iconSizeWithOverlap) / 2;
+                autoResizeAllAppsCells();
+            }
+
+            return;
+        }
+
         // We want enough space so that the text is closer to its corresponding icon.
         if (workspaceCellPaddingY < iconTextHeight) {
             iconTextSizePx = 0;
             iconDrawablePaddingPx = 0;
-            cellHeightPx = (int) Math.ceil(iconSizePx * ICON_OVERLAP_FACTOR);
+            cellHeightPx = getIconSizeWithOverlap(iconSizePx);
             autoResizeAllAppsCells();
         }
     }
 
     /**
-     * Re-computes the all-apps cell size to be independent of workspace
-     */
-    public void autoResizeAllAppsCells() {
-        int textHeight = Utilities.calculateTextHeight(allAppsIconTextSizePx);
-        int topBottomPadding = textHeight;
-        allAppsCellHeightPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx
-                + textHeight + (topBottomPadding * 2);
-    }
-
-    private void updateAllAppsContainerWidth(Resources res) {
-        int cellLayoutHorizontalPadding =
-                (cellLayoutPaddingPx.left + cellLayoutPaddingPx.right) / 2;
-        if (isTablet) {
-            int usedWidth = (allAppsCellWidthPx * numShownAllAppsColumns)
-                    + (allAppsBorderSpacePx.x * (numShownAllAppsColumns - 1))
-                    + allAppsLeftRightPadding * 2;
-            allAppsLeftRightMargin = Math.max(1, (availableWidthPx - usedWidth) / 2);
-        } else {
-            allAppsLeftRightPadding =
-                    desiredWorkspaceHorizontalMarginPx + cellLayoutHorizontalPadding;
-        }
-    }
-
-    private void setupAllAppsStyle(Context context) {
-        TypedArray allAppsStyle;
-        if (inv.allAppsStyle != INVALID_RESOURCE_HANDLE) {
-            allAppsStyle = context.obtainStyledAttributes(inv.allAppsStyle,
-                    R.styleable.AllAppsStyle);
-        } else {
-            allAppsStyle = context.obtainStyledAttributes(R.style.AllAppsStyleDefault,
-                    R.styleable.AllAppsStyle);
-        }
-        allAppsLeftRightPadding = allAppsStyle.getDimensionPixelSize(
-                R.styleable.AllAppsStyle_horizontalPadding, 0);
-        allAppsStyle.recycle();
-    }
-
-    /**
      * Returns the amount of extra (or unused) vertical space.
      */
     private int updateAvailableDimensions(Resources res) {
@@ -862,11 +918,15 @@
         float invIconTextSizeSp = inv.iconTextSize[mTypeIndex];
         iconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mMetrics));
         iconTextSizePx = pxFromSp(invIconTextSizeSp, mMetrics);
+        iconCenterVertically = mIsScalableGrid || mIsResponsiveGrid;
 
         updateIconSize(1f, res);
-
         updateWorkspacePadding();
 
+        if (mIsResponsiveGrid) {
+            return 0;
+        }
+
         // Check to see if the icons fit within the available height.
         float usedHeight = getCellLayoutHeightSpecification();
         final int maxHeight = getCellLayoutHeight();
@@ -875,7 +935,7 @@
         boolean shouldScale = scaleY < 1f;
 
         float scaleX = 1f;
-        if (isScalableGrid) {
+        if (mIsScalableGrid) {
             // We scale to fit the cellWidth and cellHeight in the available space.
             // The benefit of scalable grids is that we can get consistent aspect ratios between
             // devices.
@@ -906,6 +966,29 @@
                 + cellLayoutPaddingPx.left + cellLayoutPaddingPx.right;
     }
 
+    private int getNormalizedIconDrawablePadding() {
+        // TODO(b/235886078): workaround needed because of this bug
+        // Icons are 10% larger on XML than their visual size,
+        // so remove that extra space to get labels closer to the correct padding
+        int iconVisibleSizePx = (int) Math.round(ICON_VISIBLE_AREA_FACTOR * iconSizePx);
+        return Math.max(0, mIconDrawablePaddingOriginalPx - ((iconSizePx - iconVisibleSizePx) / 2));
+    }
+
+    private int getNormalizedFolderChildDrawablePaddingPx(int textHeight) {
+        // TODO(b/235886078): workaround needed because of this bug
+        // Icons are 10% larger on XML than their visual size,
+        // so remove that extra space to get labels closer to the correct padding
+        int drawablePadding = (folderCellHeightPx - folderChildIconSizePx - textHeight) / 3;
+
+        int iconVisibleSizePx = Math.round(ICON_VISIBLE_AREA_FACTOR * folderChildIconSizePx);
+        int iconSizeDiff = folderChildIconSizePx - iconVisibleSizePx;
+        return Math.max(0, drawablePadding - iconSizeDiff / 2);
+    }
+
+    private int getIconSizeWithOverlap(int iconSize) {
+        return (int) Math.ceil(iconSize * ICON_OVERLAP_FACTOR);
+    }
+
     /**
      * Updating the iconSize affects many aspects of the launcher layout, such as: iconSizePx,
      * iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
@@ -918,10 +1001,36 @@
 
         // Workspace
         final boolean isVerticalLayout = isVerticalBarLayout();
-        iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * iconScale);
         cellLayoutBorderSpacePx = getCellLayoutBorderSpace(inv, scale);
 
-        if (isScalableGrid) {
+        if (mIsResponsiveGrid) {
+            cellWidthPx = mResponsiveWidthSpec.getCellSizePx();
+            cellHeightPx = mResponsiveHeightSpec.getCellSizePx();
+
+            if (cellWidthPx < iconSizePx) {
+                // get a smaller icon size
+                iconSizePx = mIconSizeSteps.getIconSmallerThan(cellWidthPx);
+            }
+
+            iconDrawablePaddingPx = getNormalizedIconDrawablePadding();
+            int iconTextHeight = Utilities.calculateTextHeight(iconTextSizePx);
+            int cellContentHeight = iconSizePx + iconDrawablePaddingPx + iconTextHeight;
+
+            while (iconSizePx > mIconSizeSteps.minimumIconSize()
+                    && cellContentHeight > cellHeightPx) {
+                iconDrawablePaddingPx -= cellContentHeight - cellHeightPx;
+                if (iconDrawablePaddingPx < 0) {
+                    // get a smaller icon size
+                    iconSizePx = mIconSizeSteps.getNextLowerIconSize(iconSizePx);
+                    iconDrawablePaddingPx = getNormalizedIconDrawablePadding();
+                }
+                // calculate new cellContentHeight
+                cellContentHeight = iconSizePx + iconDrawablePaddingPx + iconTextHeight;
+            }
+
+            cellYPaddingPx = Math.max(0, cellHeightPx - cellContentHeight) / 2;
+        } else if (mIsScalableGrid) {
+            iconDrawablePaddingPx = (int) (getNormalizedIconDrawablePadding() * iconScale);
             cellWidthPx = pxFromDp(inv.minCellSize[mTypeIndex].x, mMetrics, scale);
             cellHeightPx = pxFromDp(inv.minCellSize[mTypeIndex].y, mMetrics, scale);
 
@@ -980,8 +1089,9 @@
             desiredWorkspaceHorizontalMarginPx =
                     (int) (desiredWorkspaceHorizontalMarginOriginalPx * scale);
         } else {
+            iconDrawablePaddingPx = (int) (getNormalizedIconDrawablePadding() * iconScale);
             cellWidthPx = iconSizePx + iconDrawablePaddingPx;
-            cellHeightPx = (int) Math.ceil(iconSizePx * ICON_OVERLAP_FACTOR)
+            cellHeightPx = getIconSizeWithOverlap(iconSizePx)
                     + iconDrawablePaddingPx
                     + Utilities.calculateTextHeight(iconTextSizePx);
             int cellPaddingY = (getCellSize().y - cellHeightPx) / 2;
@@ -996,7 +1106,15 @@
         }
 
         // All apps
-        updateAllAppsIconSize(scale, res);
+        if (mIsResponsiveGrid) {
+            updateAllAppsWithResponsiveMeasures();
+        } else {
+            updateAllAppsIconSize(scale, res);
+        }
+        updateAllAppsContainerWidth();
+        if (isVerticalBarLayout()) {
+            hideWorkspaceLabelsIfNotEnoughSpace();
+        }
 
         updateHotseatSizes(iconSizePx);
 
@@ -1021,14 +1139,14 @@
      * This method calculates the space between the icons to achieve a certain width.
      */
     private int calculateHotseatBorderSpace(float hotseatWidthPx, int numExtraBorder) {
+        int numBorders = (numShownHotseatIcons - 1 + numExtraBorder);
+        if (numBorders <= 0) return 0;
+
         float hotseatIconsTotalPx = iconSizePx * numShownHotseatIcons;
-        int hotseatBorderSpacePx =
-                (int) (hotseatWidthPx - hotseatIconsTotalPx)
-                        / (numShownHotseatIcons - 1 + numExtraBorder);
+        int hotseatBorderSpacePx = (int) (hotseatWidthPx - hotseatIconsTotalPx) / numBorders;
         return Math.min(hotseatBorderSpacePx, mMaxHotseatIconSpacePx);
     }
 
-
     /**
      * Updates the iconSize for allApps* variants.
      */
@@ -1042,10 +1160,10 @@
                 + allAppsBorderSpacePx.y;
         // but width is just the cell,
         // the border is added in #updateAllAppsContainerWidth
-        if (isScalableGrid) {
+        if (mIsScalableGrid) {
             allAppsIconSizePx = pxFromDp(inv.allAppsIconSize[mTypeIndex], mMetrics);
             allAppsIconTextSizePx = pxFromSp(inv.allAppsIconTextSize[mTypeIndex], mMetrics);
-            allAppsIconDrawablePaddingPx = iconDrawablePaddingOriginalPx;
+            allAppsIconDrawablePaddingPx = getNormalizedIconDrawablePadding();
             allAppsCellWidthPx = pxFromDp(inv.allAppsCellSize[mTypeIndex].x, mMetrics, scale);
 
             if (allAppsCellWidthPx < allAppsIconSizePx) {
@@ -1082,22 +1200,70 @@
                     res.getDimensionPixelSize(R.dimen.all_apps_icon_drawable_padding);
             allAppsCellWidthPx = allAppsIconSizePx + (2 * allAppsIconDrawablePaddingPx);
         }
+    }
 
-        updateAllAppsContainerWidth(res);
-        if (isVerticalBarLayout()) {
-            hideWorkspaceLabelsIfNotEnoughSpace();
+    private void updateAllAppsWithResponsiveMeasures() {
+        allAppsIconSizePx = iconSizePx;
+        allAppsIconTextSizePx = iconTextSizePx;
+        allAppsIconDrawablePaddingPx = iconDrawablePaddingPx;
+
+        allAppsBorderSpacePx = new Point(
+                mAllAppsResponsiveWidthSpec.getGutterPx(),
+                mAllAppsResponsiveHeightSpec.getGutterPx()
+        );
+        allAppsCellHeightPx = mAllAppsResponsiveHeightSpec.getCellSizePx()
+                + mAllAppsResponsiveHeightSpec.getGutterPx();
+        allAppsCellWidthPx = mAllAppsResponsiveWidthSpec.getCellSizePx();
+        allAppsLeftRightPadding = mAllAppsResponsiveWidthSpec.getStartPaddingPx();
+    }
+
+    /**
+     * Re-computes the all-apps cell size to be independent of workspace
+     */
+    public void autoResizeAllAppsCells() {
+        int textHeight = Utilities.calculateTextHeight(allAppsIconTextSizePx);
+        int topBottomPadding = textHeight;
+        allAppsCellHeightPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx
+                + textHeight + (topBottomPadding * 2);
+    }
+
+    private void updateAllAppsContainerWidth() {
+        int cellLayoutHorizontalPadding =
+                (cellLayoutPaddingPx.left + cellLayoutPaddingPx.right) / 2;
+        if (isTablet) {
+            int usedWidth = (allAppsCellWidthPx * numShownAllAppsColumns)
+                    + (allAppsBorderSpacePx.x * (numShownAllAppsColumns - 1))
+                    + allAppsLeftRightPadding * 2;
+            allAppsLeftRightMargin = Math.max(1, (availableWidthPx - usedWidth) / 2);
+        } else {
+            allAppsLeftRightPadding =
+                    Math.max(0, desiredWorkspaceHorizontalMarginPx + cellLayoutHorizontalPadding
+                            - (allAppsBorderSpacePx.x / 2));
         }
     }
 
+    private void setupAllAppsStyle(Context context) {
+        TypedArray allAppsStyle = context.obtainStyledAttributes(
+                inv.allAppsStyle != INVALID_RESOURCE_HANDLE ? inv.allAppsStyle
+                        : R.style.AllAppsStyleDefault, R.styleable.AllAppsStyle);
+
+        allAppsLeftRightPadding = allAppsStyle.getDimensionPixelSize(
+                R.styleable.AllAppsStyle_horizontalPadding, 0);
+        allAppsStyle.recycle();
+    }
+
     private void updateAvailableFolderCellDimensions(Resources res) {
         updateFolderCellSize(1f, res);
 
+        // Responsive grid doesn't need to scale the folder
+        if (mIsResponsiveGrid) return;
+
         // For usability we can't have the folder use the whole width of the screen
         Point totalWorkspacePadding = getTotalWorkspacePadding();
 
         // Check if the folder fit within the available height.
         float contentUsedHeight = folderCellHeightPx * inv.numFolderRows
-                + ((inv.numFolderRows - 1) * folderCellLayoutBorderSpacePx)
+                + ((inv.numFolderRows - 1) * folderCellLayoutBorderSpacePx.y)
                 + folderFooterHeightPx
                 + folderContentPaddingTop;
         int contentMaxHeight = availableHeightPx - totalWorkspacePadding.y;
@@ -1105,7 +1271,7 @@
 
         // Check if the folder fit within the available width.
         float contentUsedWidth = folderCellWidthPx * inv.numFolderColumns
-                + ((inv.numFolderColumns - 1) * folderCellLayoutBorderSpacePx)
+                + ((inv.numFolderColumns - 1) * folderCellLayoutBorderSpacePx.x)
                 + folderContentPaddingLeftRight * 2;
         int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x;
         float scaleX = contentMaxWidth / contentUsedWidth;
@@ -1125,7 +1291,45 @@
 
         int textHeight = Utilities.calculateTextHeight(folderChildTextSizePx);
 
-        if (isScalableGrid) {
+        if (mIsResponsiveGrid) {
+            folderCellWidthPx = mResponsiveFolderWidthSpec.getCellSizePx();
+
+            // Height
+            folderCellHeightPx = mResponsiveFolderHeightSpec.getCellSizePx();
+            folderContentPaddingTop = mResponsiveFolderHeightSpec.getStartPaddingPx();
+            folderFooterHeightPx = mResponsiveFolderHeightSpec.getEndPaddingPx();
+
+            folderCellLayoutBorderSpacePx = new Point(mResponsiveFolderWidthSpec.getGutterPx(),
+                    mResponsiveFolderHeightSpec.getGutterPx());
+
+            folderContentPaddingLeftRight = mResponsiveFolderWidthSpec.getStartPaddingPx();
+
+            // Reduce icon width if it's wider than the expected folder cell width
+            if (folderCellWidthPx < folderChildIconSizePx) {
+                folderChildIconSizePx = mIconSizeSteps.getIconSmallerThan(folderCellWidthPx);
+            }
+
+            // Recalculating padding and cell height
+            folderChildDrawablePaddingPx = getNormalizedFolderChildDrawablePaddingPx(textHeight);
+            int folderCellContentHeight = folderChildIconSizePx + folderChildDrawablePaddingPx
+                    + textHeight;
+
+            // Reduce the icon in height when it's taller than the expected cell height
+            while (folderChildIconSizePx > mIconSizeSteps.minimumIconSize()
+                    && folderCellContentHeight > folderCellHeightPx) {
+                folderChildDrawablePaddingPx -= folderCellContentHeight - folderCellHeightPx;
+                if (folderChildDrawablePaddingPx < 0) {
+                    // get a smaller icon size
+                    folderChildIconSizePx = mIconSizeSteps.getNextLowerIconSize(
+                            folderChildIconSizePx);
+                    folderChildDrawablePaddingPx =
+                            getNormalizedFolderChildDrawablePaddingPx(textHeight);
+                }
+                // calculate new cellContentHeight
+                folderCellContentHeight = folderChildIconSizePx + folderChildDrawablePaddingPx
+                        + textHeight;
+            }
+        } else if (mIsScalableGrid) {
             if (inv.folderStyle == INVALID_RESOURCE_HANDLE) {
                 folderCellWidthPx = roundPxValueFromFloat(getCellSize().x * scale);
                 folderCellHeightPx = roundPxValueFromFloat(getCellSize().y * scale);
@@ -1135,11 +1339,15 @@
             }
 
             folderContentPaddingTop = roundPxValueFromFloat(folderContentPaddingTop * scale);
-            folderCellLayoutBorderSpacePx = roundPxValueFromFloat(
-                    folderCellLayoutBorderSpacePx * scale);
+            folderCellLayoutBorderSpacePx = new Point(
+                    roundPxValueFromFloat(folderCellLayoutBorderSpacePx.x * scale),
+                    roundPxValueFromFloat(folderCellLayoutBorderSpacePx.y * scale)
+            );
             folderFooterHeightPx = roundPxValueFromFloat(folderFooterHeightPx * scale);
 
-            folderContentPaddingLeftRight = folderCellLayoutBorderSpacePx;
+            folderContentPaddingLeftRight = folderCellLayoutBorderSpacePx.x;
+
+            folderChildDrawablePaddingPx = getNormalizedFolderChildDrawablePaddingPx(textHeight);
         } else {
             int cellPaddingX = (int) (res.getDimensionPixelSize(R.dimen.folder_cell_x_padding)
                     * scale);
@@ -1156,10 +1364,8 @@
                             res.getDimensionPixelSize(R.dimen.folder_footer_height_default)
                                     * scale);
 
+            folderChildDrawablePaddingPx = getNormalizedFolderChildDrawablePaddingPx(textHeight);
         }
-
-        folderChildDrawablePaddingPx = Math.max(0,
-                (folderCellHeightPx - folderChildIconSizePx - textHeight) / 3);
     }
 
     public void updateInsets(Rect insets) {
@@ -1288,22 +1494,36 @@
     private void updateWorkspacePadding() {
         Rect padding = workspacePadding;
         if (isVerticalBarLayout()) {
-            padding.top = 0;
-            padding.bottom = edgeMarginPx;
-            if (isSeascape()) {
-                padding.left = hotseatBarSizePx;
-                padding.right = hotseatBarSidePaddingStartPx;
+            if (mIsResponsiveGrid) {
+                padding.top = mResponsiveHeightSpec.getStartPaddingPx();
+                padding.bottom = mResponsiveHeightSpec.getEndPaddingPx();
+                if (isSeascape()) {
+                    padding.left = hotseatBarSizePx + mResponsiveWidthSpec.getEndPaddingPx();
+                    padding.right = mResponsiveWidthSpec.getStartPaddingPx();
+                } else {
+                    padding.left = mResponsiveWidthSpec.getStartPaddingPx();
+                    padding.right = hotseatBarSizePx + mResponsiveWidthSpec.getEndPaddingPx();
+                }
             } else {
-                padding.left = hotseatBarSidePaddingStartPx;
-                padding.right = hotseatBarSizePx;
+                padding.top = 0;
+                padding.bottom = edgeMarginPx;
+                if (isSeascape()) {
+                    padding.left = hotseatBarSizePx;
+                    padding.right = hotseatBarSidePaddingStartPx;
+                } else {
+                    padding.left = hotseatBarSidePaddingStartPx;
+                    padding.right = hotseatBarSizePx;
+                }
             }
         } else {
             // Pad the bottom of the workspace with hotseat bar
             // and leave a bit of space in case a widget go all the way down
-            int paddingBottom = hotseatBarSizePx + workspaceBottomPadding
-                    + workspacePageIndicatorHeight - mWorkspacePageIndicatorOverlapWorkspace
-                    - mInsets.bottom;
-            int paddingTop = workspaceTopPadding + (isScalableGrid ? 0 : edgeMarginPx);
+            int paddingBottom = hotseatBarSizePx + workspaceBottomPadding - mInsets.bottom;
+            if (!mIsResponsiveGrid) {
+                paddingBottom +=
+                        workspacePageIndicatorHeight - mWorkspacePageIndicatorOverlapWorkspace;
+            }
+            int paddingTop = workspaceTopPadding + (mIsScalableGrid ? 0 : edgeMarginPx);
             int paddingSide = desiredWorkspaceHorizontalMarginPx;
 
             padding.set(paddingSide, paddingTop, paddingSide, paddingBottom);
@@ -1336,7 +1556,9 @@
             // in vertical bar layout.
             // Workspace icons are moved up by a small factor. The variable diffOverlapFactor
             // is set to account for that difference.
-            float diffOverlapFactor = iconSizePx * (ICON_OVERLAP_FACTOR - 1) / 2;
+            float diffOverlapFactor = mIsResponsiveGrid ? 0
+                    : iconSizePx * (ICON_OVERLAP_FACTOR - 1) / 2;
+
             int paddingTop = Math.max((int) (mInsets.top + cellLayoutPaddingPx.top
                     - diffOverlapFactor), 0);
             int paddingBottom = Math.max((int) (mInsets.bottom + cellLayoutPaddingPx.bottom
@@ -1379,7 +1601,7 @@
                 hotseatBarPadding.right = endSpacing;
             }
 
-        } else if (isScalableGrid) {
+        } else if (mIsScalableGrid) {
             int sideSpacing = (availableWidthPx - hotseatQsbWidth) / 2;
             hotseatBarPadding.set(sideSpacing,
                     0,
@@ -1404,6 +1626,25 @@
         return hotseatBarPadding;
     }
 
+    /** The margin between the edge of all apps and the edge of the first icon. */
+    public int getAllAppsIconStartMargin() {
+        int allAppsSpacing;
+        if (isVerticalBarLayout()) {
+            // On phones, the landscape layout uses a different setup.
+            allAppsSpacing = workspacePadding.left + workspacePadding.right;
+        } else {
+            allAppsSpacing = allAppsLeftRightPadding * 2 + allAppsLeftRightMargin * 2;
+        }
+
+        int cellWidth = DeviceProfile.calculateCellWidth(
+                availableWidthPx - allAppsSpacing,
+                0 /* borderSpace */,
+                numShownAllAppsColumns);
+        int iconVisibleSize = Math.round(ICON_VISIBLE_AREA_FACTOR * allAppsIconSizePx);
+        int iconAlignmentMargin = (cellWidth - iconVisibleSize) / 2;
+        return allAppsLeftRightPadding + iconAlignmentMargin;
+    }
+
     private int getAdditionalQsbSpace() {
         return isQsbInline ? hotseatQsbWidth + hotseatBorderSpace : 0;
     }
@@ -1599,7 +1840,7 @@
         writer.println(prefix + "\taspectRatio:" + aspectRatio);
 
         writer.println(prefix + "\tisResponsiveGrid:" + mIsResponsiveGrid);
-        writer.println(prefix + "\tisScalableGrid:" + isScalableGrid);
+        writer.println(prefix + "\tisScalableGrid:" + mIsScalableGrid);
 
         writer.println(prefix + "\tinv.numRows: " + inv.numRows);
         writer.println(prefix + "\tinv.numColumns: " + inv.numColumns);
@@ -1639,8 +1880,10 @@
         writer.println(prefix + pxToDpStr("folderChildTextSizePx", folderChildTextSizePx));
         writer.println(prefix + pxToDpStr("folderChildDrawablePaddingPx",
                 folderChildDrawablePaddingPx));
-        writer.println(prefix + pxToDpStr("folderCellLayoutBorderSpacePx",
-                folderCellLayoutBorderSpacePx));
+        writer.println(prefix + pxToDpStr("folderCellLayoutBorderSpacePx.x",
+                folderCellLayoutBorderSpacePx.x));
+        writer.println(prefix + pxToDpStr("folderCellLayoutBorderSpacePx.y",
+                folderCellLayoutBorderSpacePx.y));
         writer.println(prefix + pxToDpStr("folderContentPaddingLeftRight",
                 folderContentPaddingLeftRight));
         writer.println(prefix + pxToDpStr("folderTopPadding", folderContentPaddingTop));
@@ -1753,6 +1996,17 @@
                 getWorkspaceSpringLoadScale(context)));
         writer.println(prefix + pxToDpStr("getCellLayoutHeight()", getCellLayoutHeight()));
         writer.println(prefix + pxToDpStr("getCellLayoutWidth()", getCellLayoutWidth()));
+        if (mIsResponsiveGrid) {
+            writer.println(prefix + "\tmResponsiveHeightSpec:" + mResponsiveHeightSpec.toString());
+            writer.println(prefix + "\tmResponsiveWidthSpec:" + mResponsiveWidthSpec.toString());
+            writer.println(prefix + "\tmAllAppsResponsiveHeightSpec:"
+                    + mAllAppsResponsiveHeightSpec.toString());
+            writer.println(prefix + "\tmAllAppsResponsiveWidthSpec:"
+                    + mAllAppsResponsiveWidthSpec.toString());
+            writer.println(prefix + "\tmResponsiveFolderHeightSpec:" + mResponsiveFolderHeightSpec);
+            writer.println(prefix + "\tmResponsiveFolderWidthSpec:" + mResponsiveFolderWidthSpec);
+            writer.println(prefix + "\tmResponsiveHotseatSpec:" + mResponsiveHotseatSpec);
+        }
     }
 
     /** Returns a reduced representation of this DeviceProfile. */
diff --git a/src/com/android/launcher3/DropTargetBar.java b/src/com/android/launcher3/DropTargetBar.java
index addcac9..8bdf61a 100644
--- a/src/com/android/launcher3/DropTargetBar.java
+++ b/src/com/android/launcher3/DropTargetBar.java
@@ -30,7 +30,7 @@
 import android.view.ViewPropertyAnimator;
 import android.widget.FrameLayout;
 
-import com.android.launcher3.anim.Interpolators;
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragController.DragListener;
 import com.android.launcher3.dragndrop.DragOptions;
@@ -42,7 +42,7 @@
         implements DragListener, Insettable {
 
     protected static final int DEFAULT_DRAG_FADE_DURATION = 175;
-    protected static final TimeInterpolator DEFAULT_INTERPOLATOR = Interpolators.ACCEL;
+    protected static final TimeInterpolator DEFAULT_INTERPOLATOR = Interpolators.ACCELERATE;
 
     private final Runnable mFadeAnimationEndRunnable =
             () -> updateVisibility(DropTargetBar.this);
diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java
index f94a3c5..8ec5c18 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -21,6 +21,7 @@
 import android.graphics.Rect;
 import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.DragEvent;
 import android.view.KeyEvent;
 import android.view.inputmethod.InputMethodManager;
@@ -37,6 +38,8 @@
  * Note: AppCompatEditText doesn't fully support #displayCompletions and #onCommitCompletion
  */
 public class ExtendedEditText extends EditText {
+    private static final String TAG = "ExtendedEditText";
+
     private final Set<OnFocusChangeListener> mOnFocusChangeListeners = new HashSet<>();
 
     private boolean mForceDisableSuggestions = false;
@@ -71,14 +74,9 @@
     @Override
     public boolean onKeyPreIme(int keyCode, KeyEvent event) {
         // If this is a back key, propagate the key back to the listener
-        if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
-            if (TextUtils.isEmpty(getText())) {
-                hideKeyboard();
-            }
-            if (mBackKeyListener != null) {
-                return mBackKeyListener.onBackKey();
-            }
-            return false;
+        if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP
+                && mBackKeyListener != null) {
+            return mBackKeyListener.onBackKey();
         }
         return super.onKeyPreIme(keyCode, event);
     }
@@ -89,9 +87,17 @@
         return false;
     }
 
-    public void showKeyboard() {
+    /**
+     * Synchronously shows the soft input method.
+     *
+     * @param shouldFocus whether this EditText should also request focus.
+     * @return true if the keyboard is shown correctly and focus is given to this view (if
+     *     applicable).
+     */
+    public boolean showKeyboard(boolean shouldFocus) {
         onKeyboardShown();
-        showSoftInput();
+        boolean focusResult = !shouldFocus || requestFocus();
+        return focusResult && showSoftInputInternal();
     }
 
     public void hideKeyboard() {
@@ -104,10 +110,15 @@
                 .keyboardStateManager().setKeyboardState(SHOW);
     }
 
-    private boolean showSoftInput() {
-        return requestFocus() &&
-                getContext().getSystemService(InputMethodManager.class)
-                    .showSoftInput(this, InputMethodManager.SHOW_IMPLICIT);
+    private boolean showSoftInputInternal() {
+        boolean result = false;
+        InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
+        if (imm != null) {
+            result = imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT);
+        } else {
+            Log.w(TAG, "Failed to retrieve InputMethodManager from the system.");
+        }
+        return result;
     }
 
     public void dispatchBackKey() {
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 376f54d..c619bc5 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -18,7 +18,6 @@
 
 import static com.android.launcher3.LauncherPrefs.GRID_NAME;
 import static com.android.launcher3.Utilities.dpiFromPx;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_TWO_PANEL_HOME;
 import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
@@ -50,6 +49,7 @@
 
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.DotRenderer;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.DeviceGridState;
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.testing.shared.ResourceUtils;
@@ -180,6 +180,18 @@
     public int devicePaddingId = INVALID_RESOURCE_HANDLE;
     @XmlRes
     public int workspaceSpecsId = INVALID_RESOURCE_HANDLE;
+    @XmlRes
+    public int workspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+    @XmlRes
+    public int allAppsSpecsId = INVALID_RESOURCE_HANDLE;
+    @XmlRes
+    public int allAppsSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+    @XmlRes
+    public int folderSpecsId = INVALID_RESOURCE_HANDLE;
+    @XmlRes
+    public int folderSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+    public int hotseatSpecsId = INVALID_RESOURCE_HANDLE;
+    public int hotseatSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
 
     public String dbFile;
     public int defaultLayoutId;
@@ -275,12 +287,13 @@
      * Reinitialize the current grid after a restore, where some grids might now be disabled.
      */
     public void reinitializeAfterRestore(Context context) {
+        FileLog.d(TAG, "Reinitializing grid after restore");
         String currentGridName = getCurrentGridName(context);
         String currentDbFile = dbFile;
         String newGridName = initGrid(context, currentGridName);
         String newDbFile = dbFile;
         if (!newDbFile.equals(currentDbFile)) {
-            Log.d(TAG, "Restored grid is disabled : " + currentGridName
+            FileLog.d(TAG, "Restored grid is disabled : " + currentGridName
                     + ", migrating to: " + newGridName
                     + ", removing all other grid db files");
             for (String gridDbFile : LauncherFiles.GRID_DB_FILES) {
@@ -288,7 +301,7 @@
                     continue;
                 }
                 if (context.getDatabasePath(gridDbFile).delete()) {
-                    Log.d(TAG, "Removed old grid db file: " + gridDbFile);
+                    FileLog.d(TAG, "Removed old grid db file: " + gridDbFile);
                 }
             }
             setCurrentGrid(context, newGridName);
@@ -302,7 +315,7 @@
         int type = displayInfo.supportedBounds.stream()
                 .mapToInt(bounds -> displayInfo.isTablet(bounds) ? flagTablet : flagPhone)
                 .reduce(0, (a, b) -> a | b);
-        if ((type == (flagPhone | flagTablet)) && ENABLE_TWO_PANEL_HOME.get()) {
+        if (type == (flagPhone | flagTablet)) {
             // device has profiles supporting both phone and table modes
             return TYPE_MULTI_DISPLAY;
         } else if (type == flagTablet) {
@@ -354,6 +367,13 @@
         isScalable = closestProfile.isScalable;
         devicePaddingId = closestProfile.devicePaddingId;
         workspaceSpecsId = closestProfile.mWorkspaceSpecsId;
+        workspaceSpecsTwoPanelId = closestProfile.mWorkspaceSpecsTwoPanelId;
+        allAppsSpecsId = closestProfile.mAllAppsSpecsId;
+        allAppsSpecsTwoPanelId = closestProfile.mAllAppsSpecsTwoPanelId;
+        folderSpecsId = closestProfile.mFolderSpecsId;
+        folderSpecsTwoPanelId = closestProfile.mFolderSpecsTwoPanelId;
+        hotseatSpecsId = closestProfile.mHotseatSpecsId;
+        hotseatSpecsTwoPanelId = closestProfile.mHotseatSpecsTwoPanelId;
         this.deviceType = deviceType;
 
         inlineNavButtonsEndSpacing = closestProfile.inlineNavButtonsEndSpacing;
@@ -800,6 +820,13 @@
         private final boolean isScalable;
         private final int devicePaddingId;
         private final int mWorkspaceSpecsId;
+        private final int mWorkspaceSpecsTwoPanelId;
+        private final int mAllAppsSpecsId;
+        private final int mAllAppsSpecsTwoPanelId;
+        private final int mFolderSpecsId;
+        private final int mFolderSpecsTwoPanelId;
+        private final int mHotseatSpecsId;
+        private final int mHotseatSpecsTwoPanelId;
 
         public GridOption(Context context, AttributeSet attrs) {
             TypedArray a = context.obtainStyledAttributes(
@@ -864,8 +891,33 @@
             if (FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE.get()) {
                 mWorkspaceSpecsId = a.getResourceId(
                         R.styleable.GridDisplayOption_workspaceSpecsId, INVALID_RESOURCE_HANDLE);
+                mWorkspaceSpecsTwoPanelId = a.getResourceId(
+                        R.styleable.GridDisplayOption_workspaceSpecsTwoPanelId,
+                        INVALID_RESOURCE_HANDLE);
+                mAllAppsSpecsId = a.getResourceId(
+                        R.styleable.GridDisplayOption_allAppsSpecsId, INVALID_RESOURCE_HANDLE);
+                mAllAppsSpecsTwoPanelId = a.getResourceId(
+                        R.styleable.GridDisplayOption_allAppsSpecsTwoPanelId,
+                        INVALID_RESOURCE_HANDLE);
+                mFolderSpecsId = a.getResourceId(
+                        R.styleable.GridDisplayOption_folderSpecsId, INVALID_RESOURCE_HANDLE);
+                mFolderSpecsTwoPanelId = a.getResourceId(
+                        R.styleable.GridDisplayOption_folderSpecsTwoPanelId,
+                        INVALID_RESOURCE_HANDLE);
+                mHotseatSpecsId = a.getResourceId(
+                        R.styleable.GridDisplayOption_hotseatSpecsId, INVALID_RESOURCE_HANDLE);
+                mHotseatSpecsTwoPanelId = a.getResourceId(
+                        R.styleable.GridDisplayOption_hotseatSpecsTwoPanelId,
+                        INVALID_RESOURCE_HANDLE);
             } else {
                 mWorkspaceSpecsId = INVALID_RESOURCE_HANDLE;
+                mWorkspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+                mAllAppsSpecsId = INVALID_RESOURCE_HANDLE;
+                mAllAppsSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+                mFolderSpecsId = INVALID_RESOURCE_HANDLE;
+                mFolderSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+                mHotseatSpecsId = INVALID_RESOURCE_HANDLE;
+                mHotseatSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
             }
 
             int inlineForRotation = a.getInt(R.styleable.GridDisplayOption_inlineQsb,
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 8fb68f1..038967c 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -21,6 +21,7 @@
 import static android.content.pm.ActivityInfo.CONFIG_UI_MODE;
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
 
+import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
 import static com.android.launcher3.AbstractFloatingView.TYPE_FOLDER;
 import static com.android.launcher3.AbstractFloatingView.TYPE_ICON_SURFACE;
@@ -43,7 +44,6 @@
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.getSupportedActions;
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED;
 import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
 import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
 import static com.android.launcher3.config.FeatureFlags.SHOW_DOT_PAGINATION;
@@ -100,7 +100,6 @@
 import android.graphics.RectF;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.CancellationSignal;
 import android.os.Parcelable;
 import android.os.StrictMode;
 import android.os.SystemClock;
@@ -145,7 +144,9 @@
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.allapps.BaseSearchConfig;
 import com.android.launcher3.allapps.DiscoveryBounce;
+import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.PropertyListBuilder;
+import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.celllayout.CellPosMapper.CellPos;
 import com.android.launcher3.celllayout.CellPosMapper.TwoPanelCellPosMapper;
@@ -154,7 +155,6 @@
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.dragndrop.LauncherDragController;
 import com.android.launcher3.folder.Folder;
@@ -183,7 +183,6 @@
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.pageindicators.WorkspacePageIndicator;
 import com.android.launcher3.pm.PinRequestHelper;
-import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.popup.ArrowPopup;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.PopupDataProvider;
@@ -196,9 +195,11 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.touch.AllAppsSwipeController;
+import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.ActivityResultInfo;
 import com.android.launcher3.util.ActivityTracker;
+import com.android.launcher3.util.CannedAnimationCoordinator;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
@@ -208,7 +209,6 @@
 import com.android.launcher3.util.PendingRequestArgs;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
-import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.ScreenOnTracker;
 import com.android.launcher3.util.ScreenOnTracker.ScreenOnListener;
 import com.android.launcher3.util.SystemUiController;
@@ -411,8 +411,6 @@
     protected long mLastTouchUpTime = -1;
     private boolean mTouchInProgress;
 
-    private SafeCloseable mUserChangedCallbackCloseable;
-
     // New InstanceId is assigned to mAllAppsSessionLogId for each AllApps sessions.
     // When Launcher is not in AllApps state mAllAppsSessionLogId will be null.
     // User actions within AllApps state are logged with this InstanceId, to recreate AllApps
@@ -425,6 +423,9 @@
     private StartupLatencyLogger mStartupLatencyLogger;
     private CellPosMapper mCellPosMapper = CellPosMapper.DEFAULT;
 
+    private final CannedAnimationCoordinator mAnimationCoordinator =
+            new CannedAnimationCoordinator(this);
+
     @Override
     @TargetApi(Build.VERSION_CODES.S)
     protected void onCreate(Bundle savedInstanceState) {
@@ -445,8 +446,7 @@
             Trace.beginAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME,
                     DISPLAY_ALL_APPS_TRACE_COOKIE);
         }
-        Object traceToken = TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT,
-                TraceHelper.FLAG_UI_EVENT);
+        TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT);
         if (DEBUG_STRICT_MODE) {
             StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                     .detectDiskReads()
@@ -580,10 +580,7 @@
                 LauncherOverlayPlugin.class, false /* allowedMultiple */);
 
         mRotationHelper.initialize();
-        TraceHelper.INSTANCE.endSection(traceToken);
-
-        mUserChangedCallbackCloseable = UserCache.INSTANCE.get(this).addUserChangeListener(
-                () -> getStateManager().goToState(NORMAL));
+        TraceHelper.INSTANCE.endSection();
 
         if (Utilities.ATLEAST_R) {
             getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
@@ -733,18 +730,23 @@
 
     @Override
     protected void onHandleConfigurationChanged() {
-        if (!initDeviceProfile(mDeviceProfile.inv)) {
-            return;
+        Trace.beginSection("Launcher#onHandleconfigurationChanged");
+        try {
+            if (!initDeviceProfile(mDeviceProfile.inv)) {
+                return;
+            }
+
+            dispatchDeviceProfileChanged();
+            reapplyUi();
+            mDragLayer.recreateControllers();
+
+            // Calling onSaveInstanceState ensures that static cache used by listWidgets is
+            // initialized properly.
+            onSaveInstanceState(new Bundle());
+            mModel.rebindCallbacks();
+        } finally {
+            Trace.endSection();
         }
-
-        dispatchDeviceProfileChanged();
-        reapplyUi();
-        mDragLayer.recreateControllers();
-
-        // Calling onSaveInstanceState ensures that static cache used by listWidgets is
-        // initialized properly.
-        onSaveInstanceState(new Bundle());
-        mModel.rebindCallbacks();
     }
 
     public void onAssistantVisibilityChanged(float visibility) {
@@ -1079,15 +1081,14 @@
 
     @Override
     protected void onStart() {
-        Object traceToken = TraceHelper.INSTANCE.beginSection(ON_START_EVT,
-                TraceHelper.FLAG_UI_EVENT);
+        TraceHelper.INSTANCE.beginSection(ON_START_EVT);
         super.onStart();
         if (!mDeferOverlayCallbacks) {
             mOverlayManager.onActivityStarted(this);
         }
 
         mAppWidgetHolder.setActivityStarted(true);
-        TraceHelper.INSTANCE.endSection(traceToken);
+        TraceHelper.INSTANCE.endSection();
     }
 
     @Override
@@ -1258,8 +1259,7 @@
 
     @Override
     protected void onResume() {
-        Object traceToken = TraceHelper.INSTANCE.beginSection(ON_RESUME_EVT,
-                TraceHelper.FLAG_UI_EVENT);
+        TraceHelper.INSTANCE.beginSection(ON_RESUME_EVT);
         super.onResume();
 
         if (mDeferOverlayCallbacks) {
@@ -1269,7 +1269,7 @@
         }
 
         DragView.removeAllViews(this);
-        TraceHelper.INSTANCE.endSection(traceToken);
+        TraceHelper.INSTANCE.endSection();
     }
 
     @Override
@@ -1402,9 +1402,10 @@
      * @param info   The data structure describing the shortcut.
      * @return A View inflated from layoutResId.
      */
-    public View createShortcut(ViewGroup parent, WorkspaceItemInfo info) {
-        BubbleTextView favorite = (BubbleTextView) LayoutInflater.from(parent.getContext())
-                .inflate(R.layout.app_icon, parent, false);
+    public View createShortcut(@Nullable ViewGroup parent, WorkspaceItemInfo info) {
+        BubbleTextView favorite =
+                (BubbleTextView) LayoutInflater.from(parent != null ? parent.getContext() : this)
+                        .inflate(R.layout.app_icon, parent, false);
         favorite.applyFromWorkspaceItem(info);
         favorite.setOnClickListener(getItemOnClickListener());
         favorite.setOnFocusChangeListener(mFocusHandler);
@@ -1656,7 +1657,7 @@
         if (Utilities.isRunningInTestHarness()) {
             Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Launcher.onNewIntent: " + intent);
         }
-        Object traceToken = TraceHelper.INSTANCE.beginSection(ON_NEW_INTENT_EVT);
+        TraceHelper.INSTANCE.beginSection(ON_NEW_INTENT_EVT);
         super.onNewIntent(intent);
 
         boolean alreadyOnHome = hasWindowFocus() && ((intent.getFlags() &
@@ -1672,7 +1673,8 @@
         if (isActionMain) {
             if (!internalStateHandled) {
                 // In all these cases, only animate if we're already on home
-                closeOpenViews(isStarted());
+                AbstractFloatingView.closeAllOpenViewsExcept(
+                        this, isStarted(), AbstractFloatingView.TYPE_LISTENER);
 
                 if (!isInState(NORMAL)) {
                     // Only change state, if not already the same. This prevents cancelling any
@@ -1693,6 +1695,9 @@
             if (mLauncherCallbacks != null) {
                 mLauncherCallbacks.onHomeIntent(internalStateHandled);
             }
+            if (FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+                handleSplitAnimationGoingToHome();
+            }
             mOverlayManager.hideOverlay(isStarted() && !isForceInvisible());
             handleGestureContract(intent);
         } else if (Intent.ACTION_ALL_APPS.equals(intent.getAction())) {
@@ -1703,14 +1708,28 @@
             showAllAppsWorkTabFromIntent(alreadyOnHome);
         }
 
-        TraceHelper.INSTANCE.endSection(traceToken);
+        TraceHelper.INSTANCE.endSection();
+    }
+
+    /** Handle animating away split placeholder view when user taps on home button */
+    protected void handleSplitAnimationGoingToHome() {
+        // Overridden
     }
 
     protected void toggleAllAppsFromIntent(boolean alreadyOnHome) {
         if (getStateManager().isInStableState(ALL_APPS)) {
             getStateManager().goToState(NORMAL, alreadyOnHome);
         } else {
-            showAllAppsFromIntent(alreadyOnHome);
+            AbstractFloatingView.closeAllOpenViews(this);
+            getStateManager().goToState(ALL_APPS, true /* animated */,
+                    new AnimationSuccessListener() {
+                        @Override
+                        public void onAnimationSuccess(Animator animator) {
+                            if (mAppsView.getSearchUiManager().getEditText() != null) {
+                                mAppsView.getSearchUiManager().getEditText().requestFocus();
+                            }
+                        }
+                    });
         }
     }
 
@@ -1807,7 +1826,6 @@
         LauncherAppState.getIDP(this).removeOnChangeListener(this);
 
         mOverlayManager.onActivityDestroyed(this);
-        mUserChangedCallbackCloseable.close();
     }
 
     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
@@ -2298,7 +2316,7 @@
      * Implementation of the method from LauncherModel.Callbacks.
      */
     public void startBinding() {
-        Object traceToken = TraceHelper.INSTANCE.beginSection("startBinding");
+        TraceHelper.INSTANCE.beginSection("startBinding");
         // Floating panels (except the full widget sheet) are associated with individual icons. If
         // we are starting a fresh bind, close all such panels as all the icons are about
         // to go away.
@@ -2316,11 +2334,12 @@
         if (mHotseat != null) {
             mHotseat.resetLayout(getDeviceProfile().isVerticalBarLayout());
         }
-        TraceHelper.INSTANCE.endSection(traceToken);
+        TraceHelper.INSTANCE.endSection();
     }
 
     @Override
     public void bindScreens(IntArray orderedScreenIds) {
+        mWorkspace.mPageIndicator.setAreScreensBinding(true);
         int firstScreenPosition = 0;
         if (FeatureFlags.QSB_ON_FIRST_SCREEN &&
                 orderedScreenIds.indexOf(Workspace.FIRST_SCREEN_ID) != firstScreenPosition) {
@@ -2338,13 +2357,37 @@
         mWorkspace.unlockWallpaperFromDefaultPageOnNextLayout();
     }
 
+    /**
+     * Remove odd number because they are already included when isTwoPanels and add the pair screen
+     * if not present.
+     */
+    private IntArray filterTwoPanelScreenIds(IntArray orderedScreenIds) {
+        IntSet screenIds = IntSet.wrap(orderedScreenIds);
+        orderedScreenIds.forEach(screenId -> {
+            if (screenId % 2 == 1) {
+                screenIds.remove(screenId);
+                // In case the pair is not added, add it
+                if (!mWorkspace.containsScreenId(screenId - 1)) {
+                    screenIds.add(screenId - 1);
+                }
+            }
+        });
+        return screenIds.getArray();
+    }
+
     private void bindAddScreens(IntArray orderedScreenIds) {
+
         if (mDeviceProfile.isTwoPanels) {
-            // Some empty pages might have been removed while the phone was in a single panel
-            // mode, so we want to add those empty pages back.
-            IntSet screenIds = IntSet.wrap(orderedScreenIds);
-            orderedScreenIds.forEach(screenId -> screenIds.add(mWorkspace.getScreenPair(screenId)));
-            orderedScreenIds = screenIds.getArray();
+            if (FOLDABLE_SINGLE_PAGE.get()) {
+                orderedScreenIds = filterTwoPanelScreenIds(orderedScreenIds);
+            } else {
+                // Some empty pages might have been removed while the phone was in a single panel
+                // mode, so we want to add those empty pages back.
+                IntSet screenIds = IntSet.wrap(orderedScreenIds);
+                orderedScreenIds.forEach(
+                        screenId -> screenIds.add(mWorkspace.getScreenPair(screenId)));
+                orderedScreenIds = screenIds.getArray();
+            }
         }
 
         int count = orderedScreenIds.size();
@@ -2435,7 +2478,6 @@
             final View view;
             switch (item.itemType) {
                 case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
-                case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                 case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
                     WorkspaceItemInfo info = (WorkspaceItemInfo) item;
                     view = createShortcut(info);
@@ -2448,9 +2490,9 @@
                     break;
                 }
                 case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: {
-                    FolderInfo info = (FolderInfo) item;
-                    // TODO (jeremysim b/274189428): Create app pair icon
-                    view = null;
+                    view = AppPairIcon.inflateIcon(R.layout.app_pair_icon, this,
+                            (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
+                            (FolderInfo) item);
                     break;
                 }
                 case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
@@ -2570,7 +2612,7 @@
             return view;
         }
 
-        Object traceToken = TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId);
+        TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId);
 
         try {
             final LauncherAppWidgetProviderInfo appWidgetInfo;
@@ -2700,7 +2742,7 @@
             }
             prepareAppWidget(view, item);
         } finally {
-            TraceHelper.INSTANCE.endSection(traceToken);
+            TraceHelper.INSTANCE.endSection();
         }
 
         return view;
@@ -2793,7 +2835,7 @@
      * Implementation of the method from LauncherModel.Callbacks.
      */
     public void finishBindingItems(IntSet pagesBoundFirst) {
-        Object traceToken = TraceHelper.INSTANCE.beginSection("finishBindingItems");
+        TraceHelper.INSTANCE.beginSection("finishBindingItems");
         mWorkspace.restoreInstanceStateForRemainingPages();
 
         setWorkspaceLoading(false);
@@ -2818,9 +2860,9 @@
                 mDeviceProfile.inv.numFolderColumns * mDeviceProfile.inv.numFolderRows);
         getViewCache().setCacheSize(R.layout.folder_page, 2);
 
-        TraceHelper.INSTANCE.endSection(traceToken);
-
-        mWorkspace.removeExtraEmptyScreen(true);
+        TraceHelper.INSTANCE.endSection();
+        mWorkspace.removeExtraEmptyScreen(/* stripEmptyScreens= */ true);
+        mWorkspace.mPageIndicator.setAreScreensBinding(false);
     }
 
     private boolean canAnimatePageChange() {
@@ -2974,9 +3016,14 @@
     public void bindAllApplications(AppInfo[] apps, int flags,
             Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
         Preconditions.assertUIThread();
-        AllAppsStore appsStore = mAppsView.getAppsStore();
+        boolean hadWorkApps = mAppsView.shouldShowTabs();
+        AllAppsStore<Launcher> appsStore = mAppsView.getAppsStore();
         appsStore.setApps(apps, flags, packageUserKeytoUidMap);
         PopupContainerWithArrow.dismissInvalidPopup(this);
+        if (hadWorkApps != mAppsView.shouldShowTabs()) {
+            getStateManager().goToState(NORMAL);
+        }
+
         if (Utilities.ATLEAST_S) {
             Trace.endAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME,
                     DISPLAY_ALL_APPS_TRACE_COOKIE);
@@ -3240,8 +3287,6 @@
         return new TouchController[] {getDragController(), new AllAppsSwipeController(this)};
     }
 
-    public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) { }
-
     public void onDragLayerHierarchyChanged() {
         updateDisallowBack();
     }
@@ -3307,10 +3352,6 @@
         return false;
     }
 
-    public DragOptions getDefaultWorkspaceDragOptions() {
-        return new DragOptions();
-    }
-
     /**
      * Animates Launcher elements during a transition to the All Apps page.
      *
@@ -3382,4 +3423,24 @@
     public boolean areFreeformTasksVisible() {
         return false; // Base launcher does not track freeform tasks
     }
+
+    @Override
+    public View.OnLongClickListener getAllAppsItemLongClickListener() {
+        return ItemLongClickListener.INSTANCE_ALL_APPS;
+    }
+
+    /**
+     * Handles an app pair launch; overridden in
+     * {@link com.android.launcher3.uioverrides.QuickstepLauncher}
+     */
+    public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
+        // Overridden
+    }
+
+    /**
+     * Returns the animation coordinator for playing one-off animations
+     */
+    public CannedAnimationCoordinator getAnimationCoordinator() {
+        return mAnimationCoordinator;
+    }
 }
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 4d15ac7..9db8c82 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -17,6 +17,7 @@
 package com.android.launcher3;
 
 import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
+import static android.content.Context.RECEIVER_EXPORTED;
 
 import static com.android.launcher3.LauncherPrefs.ICON_STATE;
 import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
@@ -26,6 +27,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.content.pm.LauncherApps;
@@ -38,7 +40,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.IconProvider;
@@ -62,6 +63,7 @@
 
     public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher";
     public static final String KEY_ICON_STATE = "pref_icon_shape_path";
+    public static final String KEY_ALL_APPS_OVERVIEW_THRESHOLD = "pref_all_apps_overview_threshold";
 
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<LauncherAppState> INSTANCE =
@@ -104,27 +106,27 @@
         });
 
         mContext.getSystemService(LauncherApps.class).registerCallback(mModel);
+        mOnTerminateCallback.add(() ->
+                mContext.getSystemService(LauncherApps.class).unregisterCallback(mModel));
 
         SimpleBroadcastReceiver modelChangeReceiver =
                 new SimpleBroadcastReceiver(mModel::onBroadcastIntent);
         modelChangeReceiver.register(mContext, Intent.ACTION_LOCALE_CHANGED,
-                Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
-                Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
-                Intent.ACTION_MANAGED_PROFILE_UNLOCKED,
-                Intent.ACTION_PROFILE_INACCESSIBLE,
                 ACTION_DEVICE_POLICY_RESOURCE_UPDATED);
-        if (FeatureFlags.IS_STUDIO_BUILD) {
-            modelChangeReceiver.register(mContext, ACTION_FORCE_ROLOAD);
+        if (BuildConfig.IS_STUDIO_BUILD) {
+            mContext.registerReceiver(modelChangeReceiver, new IntentFilter(ACTION_FORCE_ROLOAD),
+                    RECEIVER_EXPORTED);
         }
         mOnTerminateCallback.add(() -> mContext.unregisterReceiver(modelChangeReceiver));
 
         SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext)
-                .addUserChangeListener(mModel::forceReload);
+                .addUserEventListener(mModel::onUserEvent);
         mOnTerminateCallback.add(userChangeListener::close);
 
         LockedUserState.get(context).runOnUserUnlocked(() -> {
-            CustomWidgetManager.INSTANCE.get(mContext)
-                    .setWidgetRefreshCallback(mModel::refreshAndBindWidgetsAndShortcuts);
+            CustomWidgetManager cwm = CustomWidgetManager.INSTANCE.get(mContext);
+            cwm.setWidgetRefreshCallback(mModel::refreshAndBindWidgetsAndShortcuts);
+            mOnTerminateCallback.add(() -> cwm.setWidgetRefreshCallback(null));
 
             IconObserver observer = new IconObserver();
             SafeCloseable iconChangeTracker = mIconProvider.registerIconChangeListener(
@@ -159,6 +161,7 @@
         mModel = new LauncherModel(context, this, mIconCache, new AppFilter(mContext),
                 iconCacheFileName != null);
         mOnTerminateCallback.add(mIconCache::close);
+        mOnTerminateCallback.add(mModel::destroy);
     }
 
     private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
@@ -180,9 +183,6 @@
      */
     @Override
     public void close() {
-        mModel.destroy();
-        mContext.getSystemService(LauncherApps.class).unregisterCallback(mModel);
-        CustomWidgetManager.INSTANCE.get(mContext).setWidgetRefreshCallback(null);
         mOnTerminateCallback.executeAllAndDestroy();
     }
 
diff --git a/src/com/android/launcher3/LauncherBackupAgent.java b/src/com/android/launcher3/LauncherBackupAgent.java
index 3d2700d..2617b93 100644
--- a/src/com/android/launcher3/LauncherBackupAgent.java
+++ b/src/com/android/launcher3/LauncherBackupAgent.java
@@ -34,7 +34,7 @@
         // Remove old files which might contain obsolete attributes like idp_grid_name in shared
         // preference that will obstruct backup's attribute from writing to shared preferences.
         if (destination.delete()) {
-            FileLog.d("LauncherBackupAgent", "Removed obsolete file: " + destination);
+            FileLog.d(TAG, "onRestoreFile: Removed obsolete file " + destination);
         }
         super.onRestoreFile(data, size, destination, type, mode, mtime);
     }
@@ -47,6 +47,7 @@
 
     @Override
     public void onRestoreFinished() {
+        FileLog.d(TAG, "onRestoreFinished: set pending for RestoreDbTask");
         RestoreDbTask.setPending(this);
     }
 }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 4e066b0..fd8f668 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -20,9 +20,7 @@
 
 import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
 import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
-import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
 import static com.android.launcher3.testing.shared.TestProtocol.sDebugTracing;
-import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
@@ -69,7 +67,6 @@
 import com.android.launcher3.pm.PackageInstallInfo;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.shortcuts.ShortcutRequest;
-import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.PackageUserKey;
@@ -95,15 +92,6 @@
 
     static final String TAG = "Launcher.Model";
 
-    // Broadcast intent to track when the profile gets locked:
-    // ACTION_MANAGED_PROFILE_UNAVAILABLE can be used until Android U where profile no longer gets
-    // locked when paused.
-    // ACTION_PROFILE_INACCESSIBLE always means that the profile is getting locked but it only
-    // appeared in Android S.
-    private static final String ACTION_PROFILE_LOCKED = Utilities.ATLEAST_U
-            ? Intent.ACTION_PROFILE_INACCESSIBLE
-            : Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE;
-
     @NonNull
     private final LauncherAppState mApp;
     @NonNull
@@ -114,6 +102,9 @@
     private LoaderTask mLoaderTask;
     private boolean mIsLoaderTaskRunning;
 
+    // only allow this once per reboot to reload work apps
+    private boolean mShouldReloadWorkProfile = true;
+
     // Indicates whether the current model data is valid or not.
     // We start off with everything not loaded. After that, we assume that
     // our monitoring of the package manager provides all updates and we never
@@ -143,6 +134,8 @@
     @NonNull
     private final ModelDelegate mModelDelegate;
 
+    private int mLastLoadId = -1;
+
     // Runnable to check if the shortcuts permission has changed.
     @NonNull
     private final Runnable mDataValidationCheck = new Runnable() {
@@ -301,28 +294,6 @@
         if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
             // If we have changed locale we need to clear out the labels in all apps/workspace.
             forceReload();
-        } else if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action)
-                || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)
-                || Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)
-                || Intent.ACTION_PROFILE_INACCESSIBLE.equals(action)) {
-            UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.WORK_TAB_MISSING, "onBroadcastIntent intentAction: " + action +
-                        " user: " + user);
-            }
-            if (user != null) {
-                if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) ||
-                        Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) {
-                    enqueueModelUpdateTask(new PackageUpdatedTask(
-                            PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user));
-                }
-
-                if (ACTION_PROFILE_LOCKED.equals(action)
-                        || Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)) {
-                    enqueueModelUpdateTask(new UserLockStateChangedTask(
-                            user, Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)));
-                }
-            }
         } else if (ACTION_DEVICE_POLICY_RESOURCE_UPDATED.equals(action)) {
             enqueueModelUpdateTask(new ReloadStringCacheTask(mModelDelegate));
         } else if (IS_STUDIO_BUILD && ACTION_FORCE_ROLOAD.equals(action)) {
@@ -335,6 +306,30 @@
     }
 
     /**
+     * Called then there use a user event
+     * @see UserCache#addUserEventListener
+     */
+    public void onUserEvent(UserHandle user, String action) {
+        if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action)
+                && mShouldReloadWorkProfile) {
+            mShouldReloadWorkProfile = false;
+            forceReload();
+        } else if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action)
+                || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) {
+            mShouldReloadWorkProfile = false;
+            enqueueModelUpdateTask(new PackageUpdatedTask(
+                    PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user));
+        } else if (UserCache.ACTION_PROFILE_LOCKED.equals(action)
+                || UserCache.ACTION_PROFILE_UNLOCKED.equals(action)) {
+            enqueueModelUpdateTask(new UserLockStateChangedTask(
+                    user, UserCache.ACTION_PROFILE_UNLOCKED.equals(action)));
+        } else if (UserCache.ACTION_PROFILE_ADDED.equals(action)
+                || UserCache.ACTION_PROFILE_REMOVED.equals(action)) {
+            forceReload();
+        }
+    }
+
+    /**
      * Reloads the workspace items from the DB and re-binds the workspace. This should generally
      * not be called as DB updates are automatically followed by UI update
      */
@@ -553,6 +548,7 @@
                 if (mLoaderTask != task) {
                     throw new CancellationException("Loader already stopped");
                 }
+                mLastLoadId++;
                 mTask = task;
                 mIsLoaderTaskRunning = true;
                 mModelLoaded = false;
@@ -563,7 +559,6 @@
             synchronized (mLock) {
                 // Everything loaded bind the data.
                 mModelLoaded = true;
-                testLogD(WORK_TAB_MISSING, "launcher model loaded");
             }
         }
 
@@ -722,4 +717,12 @@
             return mCallbacksList.toArray(new Callbacks[mCallbacksList.size()]);
         }
     }
+
+    /**
+     * Returns the ID for the last model load. If the load ID doesn't match for a transaction, the
+     * transaction should be ignored.
+     */
+    public int getLastLoadId() {
+        return mLastLoadId;
+    }
 }
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index c98df1b..427eaa3 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -275,6 +275,9 @@
 
         const val TASKBAR_PINNING_KEY = "TASKBAR_PINNING_KEY"
         @JvmField val ICON_STATE = nonRestorableItem(LauncherAppState.KEY_ICON_STATE, "", true)
+        @JvmField
+        val ALL_APPS_OVERVIEW_THRESHOLD =
+            nonRestorableItem(LauncherAppState.KEY_ALL_APPS_OVERVIEW_THRESHOLD, 180, true)
         @JvmField val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false, true)
         @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "")
         @JvmField val WORK_EDU_STEP = backedUpItem(WorkProfileManager.KEY_WORK_EDU_STEP, 0)
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 9abec50..440e146 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -16,21 +16,18 @@
 
 package com.android.launcher3;
 
-import android.annotation.TargetApi;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
 import android.appwidget.AppWidgetManager;
 import android.content.ComponentName;
 import android.content.ContentProvider;
-import android.content.ContentProviderOperation;
-import android.content.ContentProviderResult;
 import android.content.ContentUris;
 import android.content.ContentValues;
-import android.content.OperationApplicationException;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.os.Binder;
-import android.os.Build;
-import android.os.Bundle;
 import android.os.Process;
 import android.text.TextUtils;
 import android.util.Log;
@@ -38,12 +35,11 @@
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.ModelDbController;
-import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.widget.LauncherWidgetHolder;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.ArrayList;
+import java.util.function.ToIntFunction;
 
 public class LauncherProvider extends ContentProvider {
     private static final String TAG = "LauncherProvider";
@@ -74,10 +70,6 @@
         return true;
     }
 
-    public ModelDbController getModelDbController() {
-        return LauncherAppState.getInstance(getContext()).getModel().getModelDbController();
-    }
-
     @Override
     public String getType(Uri uri) {
         SqlArguments args = new SqlArguments(uri, null, null);
@@ -91,180 +83,91 @@
     @Override
     public Cursor query(Uri uri, String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
-
         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
         qb.setTables(args.table);
 
-        Cursor result = getModelDbController().query(
-                args.table, projection, args.where, args.args, sortOrder);
-        result.setNotificationUri(getContext().getContentResolver(), uri);
-        return result;
-    }
-
-    private void reloadLauncherIfExternal() {
-        if (Binder.getCallingPid() != Process.myPid()) {
-            LauncherAppState app = LauncherAppState.getInstanceNoCreate();
-            if (app != null) {
-                app.getModel().forceReload();
-            }
-        }
+        Cursor[] result = new Cursor[1];
+        executeControllerTask(controller -> {
+            result[0] = controller.query(args.table, projection, args.where, args.args, sortOrder);
+            return 0;
+        });
+        return result[0];
     }
 
     @Override
-    public Uri insert(Uri uri, ContentValues initialValues) {
-        // In very limited cases, we support system|signature permission apps to modify the db.
-        if (Binder.getCallingPid() != Process.myPid()) {
-            if (!initializeExternalAdd(initialValues)) {
-                return null;
-            }
-        }
+    public Uri insert(Uri uri, ContentValues values) {
+        int rowId = executeControllerTask(controller -> {
+            // 1. Ensure that externally added items have a valid item id
+            int id = controller.generateNewItemId();
+            values.put(LauncherSettings.Favorites._ID, id);
 
-        SqlArguments args = new SqlArguments(uri);
-        int rowId = getModelDbController().insert(args.table, initialValues);
-        if (rowId < 0) return null;
+            // 2. In the case of an app widget, and if no app widget id is specified, we
+            // attempt allocate and bind the widget.
+            Integer itemType = values.getAsInteger(Favorites.ITEM_TYPE);
+            if (itemType != null
+                    && itemType.intValue() == Favorites.ITEM_TYPE_APPWIDGET
+                    && !values.containsKey(Favorites.APPWIDGET_ID)) {
 
-        uri = ContentUris.withAppendedId(uri, rowId);
-        reloadLauncherIfExternal();
-        return uri;
-    }
+                ComponentName cn = ComponentName.unflattenFromString(
+                        values.getAsString(Favorites.APPWIDGET_PROVIDER));
+                if (cn == null) {
+                    return 0;
+                }
 
-    private boolean initializeExternalAdd(ContentValues values) {
-        // 1. Ensure that externally added items have a valid item id
-        int id = getModelDbController().generateNewItemId();
-        values.put(LauncherSettings.Favorites._ID, id);
-
-        // 2. In the case of an app widget, and if no app widget id is specified, we
-        // attempt allocate and bind the widget.
-        Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
-        if (itemType != null &&
-                itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
-                !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
-
-            final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getContext());
-            ComponentName cn = ComponentName.unflattenFromString(
-                    values.getAsString(Favorites.APPWIDGET_PROVIDER));
-
-            if (cn != null) {
                 LauncherWidgetHolder widgetHolder = LauncherWidgetHolder.newInstance(getContext());
                 try {
                     int appWidgetId = widgetHolder.allocateAppWidgetId();
                     values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
-                    if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
+                    if (!AppWidgetManager.getInstance(getContext())
+                            .bindAppWidgetIdIfAllowed(appWidgetId, cn)) {
                         widgetHolder.deleteAppWidgetId(appWidgetId);
-                        return false;
+                        return 0;
                     }
                 } catch (RuntimeException e) {
                     Log.e(TAG, "Failed to initialize external widget", e);
-                    return false;
+                    return 0;
                 } finally {
                     // Necessary to destroy the holder to free up possible activity context
                     widgetHolder.destroy();
                 }
-            } else {
-                return false;
             }
-        }
 
-        return true;
-    }
+            SqlArguments args = new SqlArguments(uri);
+            return controller.insert(args.table, values);
+        });
 
-    @Override
-    public int bulkInsert(Uri uri, ContentValues[] values) {
-        SqlArguments args = new SqlArguments(uri);
-        getModelDbController().bulkInsert(args.table, values);
-        reloadLauncherIfExternal();
-        return values.length;
-    }
-
-    @TargetApi(Build.VERSION_CODES.M)
-    @Override
-    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
-            throws OperationApplicationException {
-        try (SQLiteTransaction t = getModelDbController().newTransaction()) {
-            final int numOperations = operations.size();
-            final ContentProviderResult[] results = new ContentProviderResult[numOperations];
-            for (int i = 0; i < numOperations; i++) {
-                ContentProviderOperation op = operations.get(i);
-                results[i] = op.apply(this, results, i);
-            }
-            t.commit();
-            reloadLauncherIfExternal();
-            return results;
-        }
+        return rowId < 0 ? null : ContentUris.withAppendedId(uri, rowId);
     }
 
     @Override
     public int delete(Uri uri, String selection, String[] selectionArgs) {
         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
-        int count = getModelDbController().delete(args.table, args.where, args.args);
-        if (count > 0) {
-            reloadLauncherIfExternal();
-        }
-        return count;
+        return executeControllerTask(c -> c.delete(args.table, args.where, args.args));
     }
 
     @Override
     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
-        int count = getModelDbController().update(args.table, values, args.where, args.args);
-        reloadLauncherIfExternal();
-        return count;
+        return executeControllerTask(c -> c.update(args.table, values, args.where, args.args));
     }
 
-    @Override
-    public Bundle call(String method, final String arg, final Bundle extras) {
-        if (Binder.getCallingUid() != Process.myUid()) {
-            return null;
+    private int executeControllerTask(ToIntFunction<ModelDbController> task) {
+        if (Binder.getCallingPid() == Process.myPid()) {
+            throw new IllegalArgumentException("Same process should call model directly");
         }
-
-        switch (method) {
-            case LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG: {
-                getModelDbController().clearEmptyDbFlag();
-                return null;
-            }
-            case LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS: {
-                Bundle result = new Bundle();
-                result.putIntArray(LauncherSettings.Settings.EXTRA_VALUE,
-                        getModelDbController().deleteEmptyFolders().toArray());
-                return result;
-            }
-            case LauncherSettings.Settings.METHOD_NEW_ITEM_ID: {
-                Bundle result = new Bundle();
-                result.putInt(LauncherSettings.Settings.EXTRA_VALUE,
-                        getModelDbController().generateNewItemId());
-                return result;
-            }
-            case LauncherSettings.Settings.METHOD_NEW_SCREEN_ID: {
-                Bundle result = new Bundle();
-                result.putInt(LauncherSettings.Settings.EXTRA_VALUE,
-                        getModelDbController().getNewScreenId());
-                return result;
-            }
-            case LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB: {
-                getModelDbController().createEmptyDB();
-                return null;
-            }
-            case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: {
-                getModelDbController().loadDefaultFavoritesIfNecessary();
-                return null;
-            }
-            case LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS: {
-                getModelDbController().removeGhostWidgets();
-                return null;
-            }
-            case LauncherSettings.Settings.METHOD_NEW_TRANSACTION: {
-                Bundle result = new Bundle();
-                result.putBinder(LauncherSettings.Settings.EXTRA_VALUE,
-                        getModelDbController().newTransaction());
-                return result;
-            }
-            case LauncherSettings.Settings.METHOD_REFRESH_HOTSEAT_RESTORE_TABLE: {
-                getModelDbController().refreshHotseatRestoreTable();
-                return null;
-            }
+        try {
+            return MODEL_EXECUTOR.submit(() -> {
+                LauncherModel model = LauncherAppState.getInstance(getContext()).getModel();
+                int count = task.applyAsInt(model.getModelDbController());
+                if (count > 0) {
+                    MAIN_EXECUTOR.submit(model::forceReload);
+                }
+                return count;
+            }).get();
+        } catch (Exception e) {
+            throw new IllegalStateException(e);
         }
-        return null;
     }
 
     static class SqlArguments {
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 7fda326..105d5f3 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -16,10 +16,7 @@
 
 package com.android.launcher3;
 
-import android.content.ContentResolver;
 import android.database.sqlite.SQLiteDatabase;
-import android.net.Uri;
-import android.os.Bundle;
 import android.provider.BaseColumns;
 
 import com.android.launcher3.model.data.ItemInfo;
@@ -89,7 +86,9 @@
 
         /**
          * The gesture is an application created shortcut
+         * @deprecated This is no longer supported. Use {@link #ITEM_TYPE_DEEP_SHORTCUT} instead
          */
+        @Deprecated
         public static final int ITEM_TYPE_SHORTCUT = 1;
 
         /**
@@ -153,24 +152,6 @@
         public static final String TMP_TABLE = "favorites_tmp";
 
         /**
-         * The content:// style URL for "favorites" table
-         */
-        public static final Uri CONTENT_URI = Uri.parse("content://"
-                + LauncherProvider.AUTHORITY + "/" + TABLE_NAME);
-
-        /**
-         * The content:// style URL for a given row, identified by its id.
-         *
-         * @param id The row id.
-         *
-         * @return The unique content URL for the specified row.
-         */
-        public static Uri getContentUri(int id) {
-            return Uri.parse("content://" + LauncherProvider.AUTHORITY
-                    + "/" + TABLE_NAME + "/" + id);
-        }
-
-        /**
          * The container holding the favorite
          * <P>Type: INTEGER</P>
          */
@@ -213,7 +194,6 @@
         public static final String itemTypeToString(int type) {
             switch(type) {
                 case ITEM_TYPE_APPLICATION: return "APP";
-                case ITEM_TYPE_SHORTCUT: return "SHORTCUT";
                 case ITEM_TYPE_FOLDER: return "FOLDER";
                 case ITEM_TYPE_APPWIDGET: return "WIDGET";
                 case ITEM_TYPE_CUSTOM_APPWIDGET: return "CUSTOMWIDGET";
@@ -338,42 +318,8 @@
      * Launcher settings
      */
     public static final class Settings {
-
-        public static final Uri CONTENT_URI = Uri.parse("content://" +
-                LauncherProvider.AUTHORITY + "/settings");
-
-        public static final String METHOD_CLEAR_EMPTY_DB_FLAG = "clear_empty_db_flag";
-
-        public static final String METHOD_DELETE_EMPTY_FOLDERS = "delete_empty_folders";
-
-        public static final String METHOD_NEW_ITEM_ID = "generate_new_item_id";
-        public static final String METHOD_NEW_SCREEN_ID = "generate_new_screen_id";
-
-        public static final String METHOD_CREATE_EMPTY_DB = "create_empty_db";
-
-        public static final String METHOD_LOAD_DEFAULT_FAVORITES = "load_default_favorites";
-
-        public static final String METHOD_REMOVE_GHOST_WIDGETS = "remove_ghost_widgets";
-
-        public static final String METHOD_NEW_TRANSACTION = "new_db_transaction";
-
-        public static final String METHOD_REFRESH_HOTSEAT_RESTORE_TABLE = "restore_hotseat_table";
-
-        public static final String EXTRA_VALUE = "value";
-
-        public static final String EXTRA_DB_NAME = "db_name";
-
         public static final String LAYOUT_DIGEST_KEY = "launcher3.layout.provider.blob";
         public static final String LAYOUT_DIGEST_LABEL = "launcher-layout";
         public static final String LAYOUT_DIGEST_TAG = "ignore";
-
-        public static Bundle call(ContentResolver cr, String method) {
-            return call(cr, method, null /* arg */);
-        }
-
-        public static Bundle call(ContentResolver cr, String method, String arg) {
-            return cr.call(CONTENT_URI, method, arg, null);
-        }
-
     }
 }
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 6097fb1..d2ea7cc 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -15,8 +15,8 @@
  */
 package com.android.launcher3;
 
-import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.app.animation.Interpolators.ACCELERATE_2;
+import static com.android.app.animation.Interpolators.DECELERATE_2;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
 import static com.android.launcher3.testing.shared.TestProtocol.ALL_APPS_STATE_ORDINAL;
@@ -33,6 +33,7 @@
 
 import android.content.Context;
 import android.graphics.Color;
+import android.view.View;
 import android.view.animation.Interpolator;
 
 import androidx.annotation.FloatRange;
@@ -66,6 +67,7 @@
     public static final int CLEAR_ALL_BUTTON = 1 << 4;
     public static final int WORKSPACE_PAGE_INDICATOR = 1 << 5;
     public static final int SPLIT_PLACHOLDER_VIEW = 1 << 6;
+    public static final int FLOATING_SEARCH_BAR = 1 << 7;
 
     // Flag indicating workspace has multiple pages visible.
     public static final int FLAG_MULTI_PAGE = BaseState.getFlag(0);
@@ -90,7 +92,7 @@
     public static final float NO_SCALE = 1;
 
     protected static final PageAlphaProvider DEFAULT_ALPHA_PROVIDER =
-            new PageAlphaProvider(ACCEL_2) {
+            new PageAlphaProvider(ACCELERATE_2) {
                 @Override
                 public float getPageAlpha(int pageIndex) {
                     return 1;
@@ -98,7 +100,7 @@
             };
 
     protected static final PageTranslationProvider DEFAULT_PAGE_TRANSLATION_PROVIDER =
-            new PageTranslationProvider(DEACCEL_2) {
+            new PageTranslationProvider(DECELERATE_2) {
                 @Override
                 public float getPageTranslation(int pageIndex) {
                     return 0;
@@ -202,8 +204,61 @@
         return 0;
     }
 
+    /**
+     * How far from the bottom of the screen the <em>floating</em> search bar should rest in this
+     * state when the IME is not present.
+     * <p>
+     * To hide offscreen, use a negative value.
+     * <p>
+     * Note: if the provided value is non-negative but less than the current bottom insets, the
+     * insets will be applied. As such, you can use 0 to default to this.
+     */
+    public int getFloatingSearchBarRestingMarginBottom(Launcher launcher) {
+        DeviceProfile dp = launcher.getDeviceProfile();
+        return areElementsVisible(launcher, FLOATING_SEARCH_BAR) ? dp.getQsbOffsetY()
+                : -dp.hotseatQsbHeight;
+    }
+
+    /**
+     * How far from the start of the screen the <em>floating</em> search bar should rest.
+     * <p>
+     * To use original margin, return a negative value.
+     */
+    public int getFloatingSearchBarRestingMarginStart(Launcher launcher) {
+        boolean isRtl = Utilities.isRtl(launcher.getResources());
+        View qsb = launcher.getHotseat().getQsb();
+        return isRtl ? launcher.getHotseat().getRight() - qsb.getRight() : qsb.getLeft();
+    }
+
+    /**
+     * How far from the end of the screen the <em>floating</em> search bar should rest.
+     * <p>
+     * To use original margin, return a negative value.
+     */
+    public int getFloatingSearchBarRestingMarginEnd(Launcher launcher) {
+        DeviceProfile dp = launcher.getDeviceProfile();
+        if (dp.isQsbInline) {
+            int marginStart = getFloatingSearchBarRestingMarginStart(launcher);
+            return dp.widthPx - marginStart - dp.hotseatQsbWidth;
+        }
+
+        boolean isRtl = Utilities.isRtl(launcher.getResources());
+        View qsb = launcher.getHotseat().getQsb();
+        return isRtl ? qsb.getLeft() : launcher.getHotseat().getRight() - qsb.getRight();
+    }
+
+    /** Whether the <em>floating</em> search bar should use the pill UI when not focused. */
+    public boolean shouldFloatingSearchBarUsePillWhenUnfocused(Launcher launcher) {
+        return false;
+    }
+
     public int getVisibleElements(Launcher launcher) {
-        return HOTSEAT_ICONS | WORKSPACE_PAGE_INDICATOR | VERTICAL_SWIPE_INDICATOR;
+        int elements = HOTSEAT_ICONS | WORKSPACE_PAGE_INDICATOR | VERTICAL_SWIPE_INDICATOR;
+        // Floating search bar is visible in normal state except in landscape on phones.
+        if (!(launcher.getDeviceProfile().isPhone && launcher.getDeviceProfile().isLandscape)) {
+            elements |= FLOATING_SEARCH_BAR;
+        }
+        return elements;
     }
 
     /**
@@ -319,7 +374,7 @@
             return DEFAULT_ALPHA_PROVIDER;
         }
         final int centerPage = launcher.getWorkspace().getNextPage();
-        return new PageAlphaProvider(ACCEL_2) {
+        return new PageAlphaProvider(ACCELERATE_2) {
             @Override
             public float getPageAlpha(int pageIndex) {
                 return pageIndex != centerPage ? 0 : 1f;
@@ -336,7 +391,7 @@
             return DEFAULT_PAGE_TRANSLATION_PROVIDER;
         }
         final float quarterPageSpacing = launcher.getWorkspace().getPageSpacing() / 4f;
-        return new PageTranslationProvider(DEACCEL_2) {
+        return new PageTranslationProvider(DECELERATE_2) {
             @Override
             public float getPageTranslation(int pageIndex) {
                 boolean isRtl = launcher.getWorkspace().mIsRtl;
diff --git a/src/com/android/launcher3/MotionEventsUtils.java b/src/com/android/launcher3/MotionEventsUtils.java
index 40de003..3228ec6 100644
--- a/src/com/android/launcher3/MotionEventsUtils.java
+++ b/src/com/android/launcher3/MotionEventsUtils.java
@@ -30,6 +30,9 @@
     /** {@link MotionEvent#CLASSIFICATION_MULTI_FINGER_SWIPE} is hidden. */
     public static final int CLASSIFICATION_MULTI_FINGER_SWIPE = 4;
 
+    /** {@link MotionEvent#AXIS_GESTURE_SWIPE_FINGER_COUNT} is hidden. */
+    private static final int AXIS_GESTURE_SWIPE_FINGER_COUNT = 53;
+
     @TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public static boolean isTrackpadScroll(MotionEvent event) {
         return ENABLE_TRACKPAD_GESTURE.get()
@@ -43,11 +46,13 @@
     }
 
     public static boolean isTrackpadThreeFingerSwipe(MotionEvent event) {
-        return isTrackpadMultiFingerSwipe(event) && event.getPointerCount() == 3;
+        return isTrackpadMultiFingerSwipe(event) && event.getAxisValue(
+                AXIS_GESTURE_SWIPE_FINGER_COUNT) == 3;
     }
 
     public static boolean isTrackpadFourFingerSwipe(MotionEvent event) {
-        return isTrackpadMultiFingerSwipe(event) && event.getPointerCount() == 4;
+        return isTrackpadMultiFingerSwipe(event) && event.getAxisValue(
+                AXIS_GESTURE_SWIPE_FINGER_COUNT) == 4;
     }
 
     public static boolean isTrackpadMotionEvent(MotionEvent event) {
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index af64b3b..4b4a4a5 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -16,7 +16,7 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.anim.Interpolators.SCROLL;
+import static com.android.app.animation.Interpolators.SCROLL;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType;
 import static com.android.launcher3.touch.OverScroll.OVERSCROLL_DAMP_FACTOR;
diff --git a/src/com/android/launcher3/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java
index aaccb7d..d460ba8 100644
--- a/src/com/android/launcher3/SessionCommitReceiver.java
+++ b/src/com/android/launcher3/SessionCommitReceiver.java
@@ -32,6 +32,8 @@
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.util.Executors;
 
+import java.util.Locale;
+
 /**
  * BroadcastReceiver to handle session commit intent.
  */
@@ -63,9 +65,20 @@
         }
 
         InstallSessionHelper packageInstallerCompat = InstallSessionHelper.INSTANCE.get(context);
+        boolean alreadyAddedPromiseIcon =
+                packageInstallerCompat.promiseIconAddedForId(info.getSessionId());
         if (TextUtils.isEmpty(info.getAppPackageName())
                 || info.getInstallReason() != PackageManager.INSTALL_REASON_USER
-                || packageInstallerCompat.promiseIconAddedForId(info.getSessionId())) {
+                || alreadyAddedPromiseIcon) {
+            FileLog.d(LOG,
+                    String.format(Locale.ENGLISH,
+                            "Removing PromiseIcon for package: %s, install reason: %d,"
+                            + " alreadyAddedPromiseIcon: %s",
+                    info.getAppPackageName(),
+                    info.getInstallReason(),
+                    alreadyAddedPromiseIcon
+                )
+            );
             packageInstallerCompat.removePromiseIconId(info.getSessionId());
             return;
         }
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index a0ceefb..f0fea61 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -28,6 +28,7 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.os.Trace;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -65,6 +66,7 @@
         mActivity = ActivityContext.lookupContext(context);
         mWallpaperManager = WallpaperManager.getInstance(context);
         mContainerType = containerType;
+        setClipChildren(false);
     }
 
     public void setCellDimensions(int cellWidth, int cellHeight, int countX, int countY,
@@ -154,14 +156,15 @@
                     mBorderSpace);
             // Center the icon/folder
             int cHeight = getCellContentHeight();
-            int cellPaddingY = dp.isScalableGrid && mContainerType == WORKSPACE
-                    ? dp.cellYPaddingPx
-                    : (int) Math.max(0, ((lp.height - cHeight) / 2f));
+            int cellPaddingY =
+                    dp.cellYPaddingPx >= 0 && mContainerType == WORKSPACE
+                            ? dp.cellYPaddingPx
+                            : (int) Math.max(0, ((lp.height - cHeight) / 2f));
 
             // No need to add padding when cell layout border spacing is present.
             boolean noPaddingX =
                     (dp.cellLayoutBorderSpacePx.x > 0 && mContainerType == WORKSPACE)
-                            || (dp.folderCellLayoutBorderSpacePx > 0 && mContainerType == FOLDER)
+                            || (dp.folderCellLayoutBorderSpacePx.x > 0 && mContainerType == FOLDER)
                             || (dp.hotseatBorderSpace > 0 && mContainerType == HOTSEAT);
             int cellPaddingX = noPaddingX
                     ? 0
@@ -181,6 +184,7 @@
 
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        Trace.beginSection("ShortcutAndWidgetConteiner#onLayout");
         int count = getChildCount();
         for (int i = 0; i < count; i++) {
             final View child = getChildAt(i);
@@ -188,6 +192,7 @@
                 layoutChild(child);
             }
         }
+        Trace.endSection();
     }
 
     /**
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 709c57c..e8c6ff9 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -24,6 +24,7 @@
 
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
+import android.app.ActivityOptions;
 import android.app.Person;
 import android.app.WallpaperManager;
 import android.content.Context;
@@ -333,14 +334,12 @@
 
     public static void scaleRectAboutCenter(Rect r, float scale) {
         if (scale != 1.0f) {
-            int cx = r.centerX();
-            int cy = r.centerY();
-            r.offset(-cx, -cy);
-            r.left = (int) (r.left * scale + 0.5f);
-            r.top = (int) (r.top * scale + 0.5f);
-            r.right = (int) (r.right * scale + 0.5f);
-            r.bottom = (int) (r.bottom * scale + 0.5f);
-            r.offset(cx, cy);
+            float cx = r.exactCenterX();
+            float cy = r.exactCenterY();
+            r.left = Math.round(cx + (r.left - cx) * scale);
+            r.top = Math.round(cy + (r.top - cy) * scale);
+            r.right = Math.round(cx + (r.right - cx) * scale);
+            r.bottom = Math.round(cy + (r.bottom - cy) * scale);
         }
     }
 
@@ -561,6 +560,17 @@
     }
 
     /**
+     * Utility method to allow background activity launch for the provided activity options
+     */
+    public static ActivityOptions allowBGLaunch(ActivityOptions options) {
+        if (ATLEAST_U) {
+            options.setPendingIntentBackgroundActivityStartMode(
+                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+        }
+        return options;
+    }
+
+    /**
      * Returns the full drawable for info without any flattening or pre-processing.
      *
      * @param shouldThemeIcon If true, will theme icons when applicable
@@ -569,12 +579,13 @@
      */
     @TargetApi(Build.VERSION_CODES.TIRAMISU)
     public static Drawable getFullDrawable(Context context, ItemInfo info, int width, int height,
-            boolean shouldThemeIcon, Object[] outObj) {
+            boolean shouldThemeIcon, Object[] outObj, boolean[] outIsIconThemed) {
         Drawable icon = loadFullDrawableWithoutTheme(context, info, width, height, outObj);
         if (ATLEAST_T && icon instanceof AdaptiveIconDrawable && shouldThemeIcon) {
             AdaptiveIconDrawable aid = (AdaptiveIconDrawable) icon.mutate();
             Drawable mono = aid.getMonochrome();
             if (mono != null && Themes.isThemedIconEnabled(context)) {
+                outIsIconThemed[0] = true;
                 int[] colors = ThemedIconDrawable.getColors(context);
                 mono = mono.mutate();
                 mono.setTint(colors[1]);
@@ -635,7 +646,8 @@
      * badge. When dragged from workspace or folder, it may contain app AND/OR work profile badge
      **/
     @TargetApi(Build.VERSION_CODES.O)
-    public static Drawable getBadge(Context context, ItemInfo info, Object obj) {
+    public static Drawable getBadge(Context context, ItemInfo info, Object obj,
+            boolean isIconThemed) {
         LauncherAppState appState = LauncherAppState.getInstance(context);
         if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
             boolean iconBadged = (info instanceof ItemInfoWithIcon)
@@ -653,7 +665,8 @@
         } else {
             return Process.myUserHandle().equals(info.user)
                     ? new ColorDrawable(Color.TRANSPARENT)
-                    : context.getDrawable(R.drawable.ic_work_app_badge);
+                    : context.getDrawable(isIconThemed
+                            ? R.drawable.ic_work_app_badge_themed : R.drawable.ic_work_app_badge);
         }
     }
 
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 73bb828..8be8fed 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -67,9 +67,9 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
 import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
 import com.android.launcher3.celllayout.CellPosMapper;
@@ -562,9 +562,9 @@
         // Change the interpolators such that the fade animation plays before the move animation.
         // This prevents empty adjacent pages to overlay during animation
         mLayoutTransition.setInterpolator(LayoutTransition.DISAPPEARING,
-                Interpolators.clampToProgress(Interpolators.ACCEL_DEACCEL, 0, 0.5f));
+                Interpolators.clampToProgress(Interpolators.ACCELERATE_DECELERATE, 0, 0.5f));
         mLayoutTransition.setInterpolator(LayoutTransition.CHANGE_DISAPPEARING,
-                Interpolators.clampToProgress(Interpolators.ACCEL_DEACCEL, 0.5f, 1));
+                Interpolators.clampToProgress(Interpolators.ACCELERATE_DECELERATE, 0.5f, 1));
 
         mLayoutTransition.disableTransitionType(LayoutTransition.APPEARING);
         mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
@@ -731,6 +731,14 @@
         });
     }
 
+
+    /**
+     * Returns if the given screenId is already in the Workspace
+     */
+    public boolean containsScreenId(int screenId) {
+        return this.mWorkspaceScreens.containsKey(screenId);
+    }
+
     /**
      * Inserts extra empty pages to the end of the existing workspaces.
      * Usually we add one extra empty screen, but when two panel home is enabled we add
@@ -897,9 +905,8 @@
         mWorkspaceScreens.remove(emptyScreenId);
         mScreenOrder.removeValue(emptyScreenId);
 
-        int newScreenId = LauncherSettings.Settings.call(getContext().getContentResolver(),
-                        LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
-                .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+        int newScreenId = LauncherAppState.getInstance(getContext())
+                .getModel().getModelDbController().getNewScreenId();
         // Launcher database isn't aware of empty pages that are already bound, so we need to
         // skip those IDs manually.
         while (mWorkspaceScreens.containsKey(newScreenId)) {
@@ -1840,7 +1847,6 @@
                 != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
         boolean willBecomeShortcut =
                 (info.itemType == ITEM_TYPE_APPLICATION ||
-                        info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT ||
                         info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT);
 
         return (aboveShortcut && willBecomeShortcut);
@@ -2759,7 +2765,7 @@
             final PendingAddItemInfo pendingInfo = (PendingAddItemInfo) info;
 
             boolean findNearestVacantCell = true;
-            if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
+            if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
                 mTargetCell = findNearestArea(touchXY[0], touchXY[1], spanX, spanY,
                         cellLayout, mTargetCell);
                 float distance = cellLayout.getDistanceFromWorkspaceCellVisualCenter(
@@ -2832,8 +2838,7 @@
             View view;
 
             switch (info.itemType) {
-                case ITEM_TYPE_APPLICATION:
-                case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                 case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                 case LauncherSettings.Favorites.ITEM_TYPE_SEARCH_ACTION:
                     if (info instanceof WorkspaceItemFactory) {
diff --git a/src/com/android/launcher3/WorkspaceLayoutManager.java b/src/com/android/launcher3/WorkspaceLayoutManager.java
index 4768773..c6c38fc 100644
--- a/src/com/android/launcher3/WorkspaceLayoutManager.java
+++ b/src/com/android/launcher3/WorkspaceLayoutManager.java
@@ -55,6 +55,7 @@
         int y = presenterPos.cellY;
         if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
                 || info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+            Log.d(TAG, "add predicted icon " + child.getTag().toString() + " to home screen");
             int screenId = presenterPos.screenId;
             x = getHotseat().getCellXFromOrder(screenId);
             y = getHotseat().getCellYFromOrder(screenId);
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index 565d7da..e4f34ae 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -18,6 +18,9 @@
 
 import static androidx.dynamicanimation.animation.DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE;
 
+import static com.android.app.animation.Interpolators.ACCELERATE_2;
+import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.ZOOM_OUT;
 import static com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WORKSPACE_STATE;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
@@ -30,12 +33,8 @@
 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.WORKSPACE_PAGE_INDICATOR;
-import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.ZOOM_OUT;
 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
 import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
-import static com.android.launcher3.graphics.SysUiScrim.SYSUI_PROGRESS;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_TRANSLATE;
@@ -54,6 +53,7 @@
 import com.android.launcher3.LauncherState.PageAlphaProvider;
 import com.android.launcher3.LauncherState.PageTranslationProvider;
 import com.android.launcher3.LauncherState.ScaleAndTranslation;
+import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.anim.SpringAnimationBuilder;
@@ -196,12 +196,12 @@
                 state.getWorkspaceBackgroundAlpha(mLauncher), LINEAR);
 
         SysUiScrim sysUiScrim = mLauncher.getRootView().getSysUiScrim();
-        propertySetter.setFloat(sysUiScrim, SYSUI_PROGRESS,
+        propertySetter.setFloat(sysUiScrim.getSysUIProgress(), AnimatedFloat.VALUE,
                 state.hasFlag(FLAG_HAS_SYS_UI_SCRIM) ? 1 : 0, LINEAR);
 
         propertySetter.setViewBackgroundColor(mLauncher.getScrimView(),
                 state.getWorkspaceScrimColor(mLauncher),
-                config.getInterpolator(ANIM_SCRIM_FADE, ACCEL_2));
+                config.getInterpolator(ANIM_SCRIM_FADE, ACCELERATE_2));
     }
 
     public void applyChildState(LauncherState state, CellLayout cl, int childIndex) {
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 7e943a9..40382b2 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -18,10 +18,12 @@
 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_DISABLED_CARD;
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_EDU_CARD;
+import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_COUNT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB;
-import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE;
 
 import android.animation.Animator;
@@ -71,14 +73,12 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem;
-import com.android.launcher3.allapps.search.DefaultSearchAdapterProvider;
+import com.android.launcher3.allapps.search.AllAppsSearchUiDelegate;
 import com.android.launcher3.allapps.search.SearchAdapterProvider;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.keyboard.FocusedItemDecorator;
 import com.android.launcher3.model.StringCache;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
@@ -134,12 +134,13 @@
     protected final Point mFastScrollerOffset = new Point();
     protected final int mScrimColor;
     protected final float mHeaderThreshold;
+    protected final AllAppsSearchUiDelegate mSearchUiDelegate;
 
     // Used to animate Search results out and A-Z apps in, or vice-versa.
     private final SearchTransitionController mSearchTransitionController;
     private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
     private final Rect mInsets = new Rect();
-    private final AllAppsStore mAllAppsStore = new AllAppsStore();
+    private final AllAppsStore<T> mAllAppsStore;
     private final RecyclerView.OnScrollListener mScrollListener =
             new RecyclerView.OnScrollListener() {
                 @Override
@@ -192,6 +193,7 @@
     public ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         mActivityContext = ActivityContext.lookupContext(context);
+        mAllAppsStore = new AllAppsStore<>(mActivityContext);
 
         mScrimColor = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
         mHeaderThreshold = getResources().getDimensionPixelSize(
@@ -206,10 +208,6 @@
         mNavBarScrimPaint.setColor(Themes.getNavBarScrimColor(mActivityContext));
 
         AllAppsStore.OnUpdateListener onAppsUpdated = this::onAppsUpdated;
-        if (TestProtocol.sDebugTracing) {
-            Log.d(WORK_TAB_MISSING, "ActivityAllAppsContainer#init registeringListener: " +
-                    onAppsUpdated);
-        }
         mAllAppsStore.addUpdateListener(onAppsUpdated);
 
         // This is a focus listener that proxies focus from a view into the list view.  This is to
@@ -219,11 +217,21 @@
                 getActiveRecyclerView().requestFocus();
             }
         });
+        mSearchUiDelegate = createSearchUiDelegate();
         initContent();
 
         mSearchTransitionController = new SearchTransitionController(this);
     }
 
+    /** Creates the delegate for initializing search. */
+    protected AllAppsSearchUiDelegate createSearchUiDelegate() {
+        return new AllAppsSearchUiDelegate(this);
+    }
+
+    public AllAppsSearchUiDelegate getSearchUiDelegate() {
+        return mSearchUiDelegate;
+    }
+
     /**
      * Initializes the view hierarchy and internal variables. Any initialization which actually uses
      * these members should be done in {@link #onFinishInflate()}.
@@ -233,7 +241,7 @@
      *   onFinishInflate -> onPostCreate
      */
     protected void initContent() {
-        mMainAdapterProvider = createMainAdapterProvider();
+        mMainAdapterProvider = mSearchUiDelegate.createMainAdapterProvider();
 
         mAH.set(AdapterHolder.MAIN, new AdapterHolder(AdapterHolder.MAIN,
                 new AlphabeticalAppsList<>(mActivityContext, mAllAppsStore, null)));
@@ -250,9 +258,12 @@
         mFastScroller = findViewById(R.id.fast_scroller);
         mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup));
 
-        // Add the search box above everything else.
-        mSearchContainer = inflateSearchBox();
-        addView(mSearchContainer);
+        mSearchContainer = inflateSearchBar();
+        if (!isSearchBarFloating()) {
+            // Add the search box above everything else in this container (if the flag is enabled,
+            // it's added to drag layer in onAttach instead).
+            addView(mSearchContainer);
+        }
         mSearchUiManager = (SearchUiManager) mSearchContainer;
     }
 
@@ -283,6 +294,13 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
+        if (isSearchBarFloating()) {
+            // Note: for Taskbar this is removed in TaskbarAllAppsController#cleanUpOverlay when the
+            // panel is closed. Can't do so in onDetach because we are also a child of drag layer
+            // so can't remove its views during that dispatch.
+            mActivityContext.getDragLayer().addView(mSearchContainer);
+            mSearchUiDelegate.onInitializeSearchBar();
+        }
         mActivityContext.addOnDeviceProfileChangeListener(this);
     }
 
@@ -304,7 +322,7 @@
      * Temporarily force the bottom sheet to be visible on non-tablets.
      *
      * @param force {@code true} means bottom sheet will be visible on phones until {@code reset()}.
-     **/
+     */
     public void forceBottomSheetVisible(boolean force) {
         mForceBottomSheetVisible = force;
         updateBackgroundVisibility(mActivityContext.getDeviceProfile());
@@ -342,6 +360,7 @@
      */
     public void setSearchResults(ArrayList<AdapterItem> results, int searchResultCode) {
         setSearchResults(results);
+        mSearchUiDelegate.onSearchResultsChanged(results, searchResultCode);
     }
 
     private void animateToSearchState(boolean goingToSearch) {
@@ -374,7 +393,10 @@
                         rebindAdapters(false);
                         mRebindAdaptersAfterSearchAnimation = false;
                     }
-                    if (!goingToSearch) {
+
+                    if (goingToSearch) {
+                        mSearchUiDelegate.onAnimateToSearchStateCompleted();
+                    } else {
                         setSearchResults(null);
                         if (mViewPager != null) {
                             mViewPager.setCurrentPage(previousPage);
@@ -413,7 +435,7 @@
      * A-Z apps list.
      *
      * @param animate Whether to animate the header during the reset (e.g. switching profile tabs).
-     **/
+     */
     public void reset(boolean animate) {
         reset(animate, true);
     }
@@ -423,7 +445,7 @@
      *
      * @param animate Whether to animate the header during the reset (e.g. switching profile tabs).
      * @param exitSearch Whether to force exit the search state and return to A-Z apps list.
-     **/
+     */
     public void reset(boolean animate, boolean exitSearch) {
         for (int i = 0; i < mAH.size(); i++) {
             if (mAH.get(i).mRecyclerView != null) {
@@ -441,7 +463,7 @@
         updateHeaderScroll(0);
         if (exitSearch) {
             // Reset the search bar after transitioning home.
-            mSearchUiManager.resetSearch();
+            MAIN_EXECUTOR.getHandler().post(mSearchUiManager::resetSearch);
             // Animate to A-Z with 0 time to reset the animation with proper state management.
             animateToSearchState(false, 0);
         }
@@ -486,6 +508,9 @@
             // Will be called at the end of the animation.
             return;
         }
+        if (currentActivePage != SEARCH) {
+            mActivityContext.hideKeyboard();
+        }
         if (mAH.get(currentActivePage).mRecyclerView != null) {
             mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(mFastScroller);
         }
@@ -528,10 +553,14 @@
         mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView);
         mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView);
 
+        final AllAppsRecyclerView mainRecyclerView;
+        final AllAppsRecyclerView workRecyclerView;
         if (mUsingTabs) {
-            mAH.get(AdapterHolder.MAIN).setup(mViewPager.getChildAt(0), mPersonalMatcher);
-            mAH.get(AdapterHolder.WORK).setup(mViewPager.getChildAt(1), mWorkManager.getMatcher());
-            mAH.get(AdapterHolder.WORK).mRecyclerView.setId(R.id.apps_list_view_work);
+            mainRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(0);
+            workRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(1);
+            mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, mPersonalMatcher);
+            mAH.get(AdapterHolder.WORK).setup(workRecyclerView, mWorkManager.getMatcher());
+            workRecyclerView.setId(R.id.apps_list_view_work);
             if (FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) {
                 mAH.get(AdapterHolder.WORK).mRecyclerView.addOnScrollListener(
                         mWorkManager.newScrollListener());
@@ -543,7 +572,6 @@
                             mActivityContext.getStatsLogManager().logger()
                                     .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB);
                         }
-                        mActivityContext.hideKeyboard();
                     });
             findViewById(R.id.tab_work)
                     .setOnClickListener((View view) -> {
@@ -551,24 +579,30 @@
                             mActivityContext.getStatsLogManager().logger()
                                     .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB);
                         }
-                        mActivityContext.hideKeyboard();
                     });
             setDeviceManagementResources();
-            onActivePageChanged(mViewPager.getNextPage());
+            if (mHeader.isSetUp()) {
+                onActivePageChanged(mViewPager.getNextPage());
+            }
         } else {
-            mAH.get(AdapterHolder.MAIN).setup(findViewById(R.id.apps_list_view), null);
+            mainRecyclerView = findViewById(R.id.apps_list_view);
+            workRecyclerView = null;
+            mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, null);
             mAH.get(AdapterHolder.WORK).mRecyclerView = null;
         }
+        setUpCustomRecyclerViewPool(
+                mainRecyclerView,
+                workRecyclerView,
+                mAllAppsStore.getRecyclerViewPool());
         setupHeader();
 
-        if (isSearchBarOnBottom()) {
+        if (isSearchBarFloating()) {
             // Keep the scroller above the search bar.
             RelativeLayout.LayoutParams scrollerLayoutParams =
                     (LayoutParams) mFastScroller.getLayoutParams();
-            scrollerLayoutParams.addRule(RelativeLayout.ABOVE, R.id.search_container_all_apps);
-            scrollerLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
-            scrollerLayoutParams.bottomMargin = getResources().getDimensionPixelSize(
-                    R.dimen.fastscroll_bottom_margin_floating_search);
+            scrollerLayoutParams.bottomMargin = mSearchContainer.getHeight()
+                    + getResources().getDimensionPixelSize(
+                            R.dimen.fastscroll_bottom_margin_floating_search);
         }
 
         mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView);
@@ -576,6 +610,30 @@
         mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView);
     }
 
+    /**
+     * If {@link ENABLE_ALL_APPS_RV_PREINFLATION} is enabled, wire custom
+     * {@link RecyclerView.RecycledViewPool} to main and work {@link AllAppsRecyclerView}.
+     *
+     * Then if {@link ALL_APPS_GONE_VISIBILITY} is enabled, update max pool size. This is because
+     * all apps rv's hidden visibility is changed to {@link View#GONE} from {@link View#INVISIBLE),
+     * thus we cannot rely on layout pass to update pool size.
+     */
+    private static void setUpCustomRecyclerViewPool(
+            @NonNull AllAppsRecyclerView mainRecyclerView,
+            @Nullable AllAppsRecyclerView workRecyclerView,
+            @NonNull RecyclerView.RecycledViewPool recycledViewPool) {
+        if (!ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
+            return;
+        }
+        mainRecyclerView.setRecycledViewPool(recycledViewPool);
+        if (workRecyclerView != null) {
+            workRecyclerView.setRecycledViewPool(recycledViewPool);
+        }
+        if (ALL_APPS_GONE_VISIBILITY.get()) {
+            mainRecyclerView.updatePoolSize();
+        }
+    }
+
     private void replaceAppsRVContainer(boolean showTabs) {
         for (int i = AdapterHolder.MAIN; i <= AdapterHolder.WORK; i++) {
             AdapterHolder adapterHolder = mAH.get(i);
@@ -620,11 +678,9 @@
         removeCustomRules(getSearchRecyclerView());
         if (!isSearchSupported()) {
             layoutWithoutSearchContainer(rvContainer, showTabs);
-        } else if (isSearchBarOnBottom()) {
+        } else if (isSearchBarFloating()) {
             alignParentTop(rvContainer, showTabs);
             alignParentTop(getSearchRecyclerView(), /* tabs= */ false);
-            layoutAboveSearchContainer(rvContainer);
-            layoutAboveSearchContainer(getSearchRecyclerView());
         } else {
             layoutBelowSearchContainer(rvContainer, showTabs);
             layoutBelowSearchContainer(getSearchRecyclerView(), /* tabs= */ false);
@@ -655,7 +711,7 @@
         removeCustomRules(mHeader);
         if (!isSearchSupported()) {
             layoutWithoutSearchContainer(mHeader, false /* includeTabsMargin */);
-        } else if (isSearchBarOnBottom()) {
+        } else if (isSearchBarFloating()) {
             alignParentTop(mHeader, false /* includeTabsMargin */);
         } else {
             layoutBelowSearchContainer(mHeader, false /* includeTabsMargin */);
@@ -695,16 +751,62 @@
     }
 
     /**
-     * It is up to the search container view created by {@link #inflateSearchBox()} to use the
-     * floating search bar flag to move itself to the bottom of this container. This method checks
-     * if that had been done; otherwise the flag will be ignored.
-     *
-     * @return true if the search bar is at the bottom of the container (as opposed to the top).
-     **/
-    private boolean isSearchBarOnBottom() {
-        return FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()
-                && ((RelativeLayout.LayoutParams) mSearchContainer.getLayoutParams()).getRule(
-                ALIGN_PARENT_BOTTOM) == RelativeLayout.TRUE;
+     * @return true if the search bar is floating above this container (at the bottom of the screen)
+     */
+    protected boolean isSearchBarFloating() {
+        return mSearchUiDelegate.isSearchBarFloating();
+    }
+
+    /**
+     * Whether the <em>floating</em> search bar should appear as a small pill when not focused.
+     * <p>
+     * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely
+     * makes sense to use that method to derive an appropriate value for the current/target state.
+     */
+    public boolean shouldFloatingSearchBarBePillWhenUnfocused() {
+        return false;
+    }
+
+    /**
+     * How far from the bottom of the screen the <em>floating</em> search bar should rest when the
+     * IME is not present.
+     * <p>
+     * To hide offscreen, use a negative value.
+     * <p>
+     * Note: if the provided value is non-negative but less than the current bottom insets, the
+     * insets will be applied. As such, you can use 0 to default to this.
+     * <p>
+     * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely
+     * makes sense to use that method to derive an appropriate value for the current/target state.
+     */
+    public int getFloatingSearchBarRestingMarginBottom() {
+        return 0;
+    }
+
+    /**
+     * How far from the start of the screen the <em>floating</em> search bar should rest.
+     * <p>
+     * To use original margin, return a negative value.
+     * <p>
+     * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely
+     * makes sense to use that method to derive an appropriate value for the current/target state.
+     */
+    public int getFloatingSearchBarRestingMarginStart() {
+        DeviceProfile dp = mActivityContext.getDeviceProfile();
+        return dp.allAppsLeftRightMargin + dp.getAllAppsIconStartMargin();
+    }
+
+    /**
+     * How far from the end of the screen the <em>floating</em> search bar should rest.
+     * <p>
+     * To use original margin, return a negative value.
+     * <p>
+     * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely
+     * makes sense to use that method to derive an appropriate value for the current/target state.
+     */
+    public int getFloatingSearchBarRestingMarginEnd() {
+        DeviceProfile dp = mActivityContext.getDeviceProfile();
+        return dp.allAppsLeftRightMargin + dp.getAllAppsIconStartMargin();
     }
 
     private void layoutBelowSearchContainer(View v, boolean includeTabsMargin) {
@@ -724,15 +826,6 @@
         layoutParams.topMargin = topMargin;
     }
 
-    private void layoutAboveSearchContainer(View v) {
-        if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
-            return;
-        }
-
-        RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams();
-        layoutParams.addRule(RelativeLayout.ABOVE, R.id.search_container_all_apps);
-    }
-
     private void alignParentTop(View v, boolean includeTabsMargin) {
         if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
             return;
@@ -786,15 +879,10 @@
     }
 
     /**
-     * Inflates the search box
+     * Inflates the search bar
      */
-    protected View inflateSearchBox() {
-        return getLayoutInflater().inflate(R.layout.search_container_all_apps, this, false);
-    }
-
-    /** Creates the adapter provider for the main section. */
-    protected SearchAdapterProvider<?> createMainAdapterProvider() {
-        return new DefaultSearchAdapterProvider(mActivityContext);
+    protected View inflateSearchBar() {
+        return mSearchUiDelegate.inflateSearchBar();
     }
 
     /** The adapter provider for the main section. */
@@ -834,16 +922,7 @@
         container.put(R.id.work_tab_state_id, state);
     }
 
-    /**
-     * Sets the long click listener for icons
-     */
-    public void setOnIconLongClickListener(OnLongClickListener listener) {
-        for (AdapterHolder holder : mAH) {
-            holder.mAdapter.setOnIconLongClickListener(listener);
-        }
-    }
-
-    public AllAppsStore getAppsStore() {
+    public AllAppsStore<T> getAppsStore() {
         return mAllAppsStore;
     }
 
@@ -855,6 +934,7 @@
     public void onDeviceProfileChanged(DeviceProfile dp) {
         for (AdapterHolder holder : mAH) {
             holder.mAdapter.setAppsPerRow(dp.numShownAllAppsColumns);
+            holder.mAppsList.setNumAppsPerRowAllApps(dp.numShownAllAppsColumns);
             if (holder.mRecyclerView != null) {
                 // Remove all views and clear the pool, while keeping the data same. After this
                 // call, all the viewHolders will be recreated.
@@ -886,10 +966,6 @@
 
     private void onAppsUpdated() {
         mHasWorkApps = Stream.of(mAllAppsStore.getApps()).anyMatch(mWorkManager.getMatcher());
-        if (TestProtocol.sDebugTracing) {
-            Log.d(WORK_TAB_MISSING, "ActivityAllAppsContainerView#onAppsUpdated hasWorkApps: " +
-                    mHasWorkApps + " allApps: " + mAllAppsStore.getApps().length);
-        }
         if (!isSearching()) {
             rebindAdapters();
             if (mHasWorkApps) {
@@ -974,7 +1050,7 @@
     /**
      * The container for A-Z apps (the ViewPager for main+work tabs, or main RV). This is currently
      * hidden while searching.
-     **/
+     */
     public ViewGroup getAppsRecyclerViewContainer() {
         return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view);
     }
@@ -999,7 +1075,7 @@
     }
 
     public LayoutInflater getLayoutInflater() {
-        return LayoutInflater.from(getContext());
+        return mSearchUiDelegate.getLayoutInflater();
     }
 
     @Override
@@ -1021,7 +1097,7 @@
             setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0);
         } else {
             int topPadding = grid.allAppsTopPadding;
-            if (isSearchBarOnBottom() && !grid.isTablet) {
+            if (isSearchBarFloating() && !grid.isTablet) {
                 topPadding += getResources().getDimensionPixelSize(
                         R.dimen.all_apps_additional_top_padding_floating_search);
             }
@@ -1097,7 +1173,10 @@
         }
     }
 
-    protected boolean shouldShowTabs() {
+    /**
+     * Returns true if the container has work apps.
+     */
+    public boolean shouldShowTabs() {
         return mHasWorkApps;
     }
 
@@ -1257,7 +1336,7 @@
         final FloatingHeaderView headerView = getFloatingHeaderView();
         if (hasBottomSheet) {
             // Start adding header protection if search bar or tabs will attach to the top.
-            if (!FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get() || mUsingTabs) {
+            if (!isSearchBarFloating() || mUsingTabs) {
                 mTmpRectF.set(
                         leftWithScale,
                         topWithScale,
@@ -1313,7 +1392,7 @@
     /** Returns the position of the bottom edge of the header */
     public int getHeaderBottom() {
         int bottom = (int) getTranslationY() + mHeader.getClipTop();
-        if (isSearchBarOnBottom()) {
+        if (isSearchBarFloating()) {
             if (mActivityContext.getDeviceProfile().isTablet) {
                 return bottom + mBottomSheetBackground.getTop();
             }
@@ -1331,6 +1410,7 @@
 
     protected void onInitializeRecyclerView(RecyclerView rv) {
         rv.addOnScrollListener(mScrollListener);
+        mSearchUiDelegate.onInitializeRecyclerView(rv);
     }
 
     /** Returns the instance of @{code SearchTransitionController}. */
@@ -1383,6 +1463,9 @@
                 if (isWork() && mWorkManager.getWorkModeSwitch() != null) {
                     bottomOffset = mInsets.bottom + mWorkManager.getWorkModeSwitch().getHeight();
                 }
+                if (isSearchBarFloating()) {
+                    bottomOffset += mSearchContainer.getHeight();
+                }
                 mRecyclerView.setPadding(mPadding.left, mPadding.top, mPadding.right,
                         mPadding.bottom + bottomOffset);
             }
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 7c5c003..7edbeac 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY;
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo;
 import static com.android.launcher3.logger.LauncherAtom.SearchResultContainer;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN;
@@ -26,6 +27,8 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_COLLAPSE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_EXTEND;
+import static com.android.launcher3.recyclerview.AllAppsRecyclerViewPoolKt.EXTRA_ICONS_COUNT;
+import static com.android.launcher3.recyclerview.AllAppsRecyclerViewPoolKt.PREINFLATE_ICONS_ROW_COUNT;
 import static com.android.launcher3.util.LogConfig.SEARCH_LOGGING;
 
 import android.content.Context;
@@ -96,8 +99,18 @@
         int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1);
         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1);
-        pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, approxRows
-                * (mNumAppsPerRow + 1));
+
+        // If all apps' hidden visibility is INVISIBLE, we will need to preinflate one page of
+        // all apps icons for smooth scrolling.
+        int maxPoolSizeForAppIcons = (approxRows + 1) * grid.numShownAllAppsColumns;
+        if (ALL_APPS_GONE_VISIBILITY.get()) {
+            // If all apps' hidden visibility is GONE, we need to increase prefinated icons number
+            // by [PREINFLATE_ICONS_ROW_COUNT] rows + [EXTRA_ICONS_COUNT] for fast opening all apps.
+            maxPoolSizeForAppIcons +=
+                    PREINFLATE_ICONS_ROW_COUNT * grid.numShownAllAppsColumns + EXTRA_ICONS_COUNT;
+        }
+        pool.setMaxRecycledViews(
+                AllAppsGridAdapter.VIEW_TYPE_ICON, maxPoolSizeForAppIcons);
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index 06af970..e724858 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -15,24 +15,27 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION;
 import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR;
 import static com.android.launcher3.model.data.AppInfo.EMPTY_ARRAY;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK;
-import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
 
+import android.content.Context;
 import android.os.UserHandle;
-import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
 
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.launcher3.recyclerview.AllAppsRecyclerViewPool;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.views.ActivityContext;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -45,8 +48,10 @@
 
 /**
  * A utility class to maintain the collection of all apps.
+ *
+ * @param <T> The type of the context.
  */
-public class AllAppsStore {
+public class AllAppsStore<T extends Context & ActivityContext> {
 
     // Defer updates flag used to defer all apps updates to the next draw.
     public static final int DEFER_UPDATES_NEXT_DRAW = 1 << 0;
@@ -56,7 +61,7 @@
     private PackageUserKey mTempKey = new PackageUserKey(null, null);
     private AppInfo mTempInfo = new AppInfo();
 
-    private AppInfo[] mApps = EMPTY_ARRAY;
+    private @NonNull AppInfo[] mApps = EMPTY_ARRAY;
 
     private final List<OnUpdateListener> mUpdateListeners = new CopyOnWriteArrayList<>();
     private final ArrayList<ViewGroup> mIconContainers = new ArrayList<>();
@@ -64,20 +69,36 @@
     private int mModelFlags;
     private int mDeferUpdatesFlags = 0;
     private boolean mUpdatePending = false;
+    private final AllAppsRecyclerViewPool mAllAppsRecyclerViewPool = new AllAppsRecyclerViewPool();
+
+    private final T mContext;
 
     public AppInfo[] getApps() {
         return mApps;
     }
 
+    public AllAppsStore(@NonNull T context) {
+        mContext = context;
+    }
+
     /**
      * Sets the current set of apps and sets mapping for {@link PackageUserKey} to Uid for
      * the current set of apps.
      */
-    public void setApps(AppInfo[] apps, int flags, Map<PackageUserKey, Integer> map) {
-        mApps = apps;
+    public void setApps(@Nullable AppInfo[] apps, int flags, Map<PackageUserKey, Integer> map)  {
+        mApps = apps == null ? EMPTY_ARRAY : apps;
         mModelFlags = flags;
         notifyUpdate();
         mPackageUserKeytoUidMap = map;
+        // Preinflate all apps RV when apps has changed, which can happen after unlocking screen,
+        // rotating screen, or downloading/upgrading apps.
+        if (ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
+            mAllAppsRecyclerViewPool.preInflateAllAppsViewHolders(mContext);
+        }
+    }
+
+    RecycledViewPool getRecyclerViewPool() {
+        return mAllAppsRecyclerViewPool;
     }
 
     /**
@@ -134,9 +155,6 @@
             return;
         }
         for (OnUpdateListener listener : mUpdateListeners) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(WORK_TAB_MISSING, "AllAppsStore#notifyUpdate listener: " + listener);
-            }
             listener.onAppsUpdated();
         }
     }
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index d4f152a..c09a5b9 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -15,14 +15,15 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.app.animation.Interpolators.DECELERATE_1_7;
+import static com.android.app.animation.Interpolators.INSTANT;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
-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_BOTTOM_SHEET_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
@@ -45,6 +46,7 @@
 import androidx.annotation.FloatRange;
 import androidx.annotation.Nullable;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.Launcher;
@@ -53,7 +55,6 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimatorListeners;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.config.FeatureFlags;
@@ -234,7 +235,11 @@
      */
     public void setProgress(float progress) {
         mProgress = progress;
-        getAppsViewProgressTranslationY().setValue(mProgress * mShiftRange);
+        boolean fromBackground =
+                mLauncher.getStateManager().getCurrentStableState() == BACKGROUND_APP;
+        // Allow apps panel to shift the full screen if coming from another app.
+        float shiftRange = fromBackground ? mLauncher.getDeviceProfile().heightPx : mShiftRange;
+        getAppsViewProgressTranslationY().setValue(mProgress * shiftRange);
         mLauncher.onAllAppsTransition(1 - progress);
 
         boolean hasScrim = progress < NAV_BAR_COLOR_FORCE_UPDATE_THRESHOLD
@@ -380,7 +385,7 @@
 
         // need to decide depending on the release velocity
         Interpolator verticalProgressInterpolator = config.getInterpolator(ANIM_VERTICAL_PROGRESS,
-                config.userControlled ? LINEAR : DEACCEL_1_7);
+                config.userControlled ? LINEAR : DECELERATE_1_7);
         Animator anim = createSpringAnimation(mProgress, targetProgress);
         anim.setInterpolator(verticalProgressInterpolator);
         anim.addListener(getProgressAnimatorListener());
@@ -433,7 +438,8 @@
         mAppsView = appsView;
         mAppsView.setScrimView(scrimView);
 
-        mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT);
+        mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT,
+                FeatureFlags.ALL_APPS_GONE_VISIBILITY.get() ? View.GONE : View.INVISIBLE);
         mAppsViewAlpha.setUpdateVisibility(true);
         mAppsViewTranslationY = new MultiPropertyFactory<>(
                 mAppsView, VIEW_TRANSLATE_Y, APPS_VIEW_INDEX_COUNT, Float::sum);
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionListener.java b/src/com/android/launcher3/allapps/AllAppsTransitionListener.java
new file mode 100644
index 0000000..4a17e29
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionListener.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+/**
+ * An interface for listening to all-apps open-close transition
+ */
+public interface AllAppsTransitionListener {
+    /**
+     * Called when the transition starts
+     * @param toAllApps {@code true} if this transition is supposed to end in the AppApps UI
+     *
+     * @see ActivityAllAppsContainerView
+     */
+    void onAllAppsTransitionStart(boolean toAllApps);
+
+    /**
+     * Called when the transition ends
+     * @param toAllApps {@code true} if the final state is all-apps
+     *
+     * @see ActivityAllAppsContainerView
+     */
+    void onAllAppsTransitionEnd(boolean toAllApps);
+}
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 29767bf..ee4b5bc 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -69,7 +69,7 @@
     // The set of apps from the system
     private final List<AppInfo> mApps = new ArrayList<>();
     @Nullable
-    private final AllAppsStore mAllAppsStore;
+    private final AllAppsStore<T> mAllAppsStore;
 
     // The number of results in current adapter
     private int mAccessibilityResultsCount = 0;
@@ -82,22 +82,27 @@
     private final ArrayList<AdapterItem> mSearchResults = new ArrayList<>();
     private BaseAllAppsAdapter<T> mAdapter;
     private AppInfoComparator mAppNameComparator;
-    private final int mNumAppsPerRowAllApps;
+    private int mNumAppsPerRowAllApps;
     private int mNumAppRowsInAdapter;
     private Predicate<ItemInfo> mItemFilter;
 
-    public AlphabeticalAppsList(Context context, @Nullable AllAppsStore appsStore,
+    public AlphabeticalAppsList(Context context, @Nullable AllAppsStore<T> appsStore,
             WorkProfileManager workProfileManager) {
         mAllAppsStore = appsStore;
         mActivityContext = ActivityContext.lookupContext(context);
         mAppNameComparator = new AppInfoComparator(context);
         mWorkProviderManager = workProfileManager;
-        mNumAppsPerRowAllApps = mActivityContext.getDeviceProfile().inv.numAllAppsColumns;
+        mNumAppsPerRowAllApps = mActivityContext.getDeviceProfile().numShownAllAppsColumns;
         if (mAllAppsStore != null) {
             mAllAppsStore.addUpdateListener(this);
         }
     }
 
+    /** Set the number of apps per row when device profile changes. */
+    public void setNumAppsPerRowAllApps(int numAppsPerRow) {
+        mNumAppsPerRowAllApps = numAppsPerRow;
+    }
+
     public void updateItemFilter(Predicate<ItemInfo> itemFilter) {
         this.mItemFilter = itemFilter;
         onAppsUpdated();
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 8fa4276..be0a898 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -15,10 +15,7 @@
  */
 package com.android.launcher3.allapps;
 
-import static com.android.launcher3.touch.ItemLongClickListener.INSTANCE_ALL_APPS;
-
 import android.content.Context;
-import android.content.res.Resources;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnClickListener;
@@ -27,13 +24,12 @@
 import android.view.ViewGroup;
 import android.widget.TextView;
 
-import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.R;
-import com.android.launcher3.allapps.search.SearchAdapterProvider;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.search.SearchAdapterProvider;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.views.ActivityContext;
@@ -139,31 +135,24 @@
 
     protected final LayoutInflater mLayoutInflater;
     protected final OnClickListener mOnIconClickListener;
-    protected OnLongClickListener mOnIconLongClickListener = INSTANCE_ALL_APPS;
+    protected final OnLongClickListener mOnIconLongClickListener;
     protected OnFocusChangeListener mIconFocusListener;
     private final int mExtraTextHeight;
 
     public BaseAllAppsAdapter(T activityContext, LayoutInflater inflater,
             AlphabeticalAppsList<T> apps, SearchAdapterProvider<?> adapterProvider) {
-        Resources res = activityContext.getResources();
         mActivityContext = activityContext;
         mApps = apps;
         mLayoutInflater = inflater;
 
         mOnIconClickListener = mActivityContext.getItemOnClickListener();
+        mOnIconLongClickListener = mActivityContext.getAllAppsItemLongClickListener();
 
         mAdapterProvider = adapterProvider;
         mExtraTextHeight = Utilities.calculateTextHeight(
                 mActivityContext.getDeviceProfile().allAppsIconTextSizePx);
     }
 
-    /**
-     * Sets the long click listener for icons
-     */
-    public void setOnIconLongClickListener(@Nullable OnLongClickListener listener) {
-        mOnIconLongClickListener = listener;
-    }
-
     /** Checks if the passed viewType represents all apps divider. */
     public static boolean isDividerViewType(int viewType) {
         return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER);
diff --git a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
index aefedae..5e48177 100644
--- a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
@@ -22,6 +22,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.statemanager.StateManager;
 
 /**
  * AllAppsContainerView with launcher specific callbacks
@@ -53,4 +54,87 @@
     public boolean isInAllApps() {
         return mActivityContext.getStateManager().isInStableState(LauncherState.ALL_APPS);
     }
+
+    @Override
+    public boolean shouldFloatingSearchBarBePillWhenUnfocused() {
+        if (!isSearchBarFloating()) {
+            return false;
+        }
+        Launcher launcher = mActivityContext;
+        StateManager<LauncherState> manager = launcher.getStateManager();
+        if (manager.isInTransition() && manager.getTargetState() != null) {
+            return manager.getTargetState().shouldFloatingSearchBarUsePillWhenUnfocused(launcher);
+        }
+        return manager.getCurrentStableState()
+                .shouldFloatingSearchBarUsePillWhenUnfocused(launcher);
+    }
+
+    @Override
+    public int getFloatingSearchBarRestingMarginBottom() {
+        if (!isSearchBarFloating()) {
+            return super.getFloatingSearchBarRestingMarginBottom();
+        }
+        Launcher launcher = mActivityContext;
+        StateManager<LauncherState> stateManager = launcher.getStateManager();
+
+        // We want to rest at the current state's resting position, unless we are in transition and
+        // the target state's resting position is higher (that way if we are closing the keyboard,
+        // we can stop translating at that point).
+        int currentStateMarginBottom = stateManager.getCurrentStableState()
+                .getFloatingSearchBarRestingMarginBottom(launcher);
+        int targetStateMarginBottom = -1;
+        if (stateManager.isInTransition() && stateManager.getTargetState() != null) {
+            targetStateMarginBottom = stateManager.getTargetState()
+                    .getFloatingSearchBarRestingMarginBottom(launcher);
+            if (targetStateMarginBottom < 0) {
+                // Go ahead and move offscreen.
+                return targetStateMarginBottom;
+            }
+        }
+        return Math.max(targetStateMarginBottom, currentStateMarginBottom);
+    }
+
+    @Override
+    public int getFloatingSearchBarRestingMarginStart() {
+        if (!isSearchBarFloating()) {
+            return super.getFloatingSearchBarRestingMarginStart();
+        }
+
+        StateManager<LauncherState> stateManager = mActivityContext.getStateManager();
+
+        // Special case to not expand the search bar when exiting All Apps on phones.
+        if (stateManager.getCurrentStableState() == LauncherState.ALL_APPS
+                && mActivityContext.getDeviceProfile().isPhone) {
+            return LauncherState.ALL_APPS.getFloatingSearchBarRestingMarginStart(mActivityContext);
+        }
+
+        if (stateManager.isInTransition() && stateManager.getTargetState() != null) {
+            return stateManager.getTargetState()
+                    .getFloatingSearchBarRestingMarginStart(mActivityContext);
+        }
+        return stateManager.getCurrentStableState()
+                .getFloatingSearchBarRestingMarginStart(mActivityContext);
+    }
+
+    @Override
+    public int getFloatingSearchBarRestingMarginEnd() {
+        if (!isSearchBarFloating()) {
+            return super.getFloatingSearchBarRestingMarginEnd();
+        }
+
+        StateManager<LauncherState> stateManager = mActivityContext.getStateManager();
+
+        // Special case to not expand the search bar when exiting All Apps on phones.
+        if (stateManager.getCurrentStableState() == LauncherState.ALL_APPS
+                && mActivityContext.getDeviceProfile().isPhone) {
+            return LauncherState.ALL_APPS.getFloatingSearchBarRestingMarginEnd(mActivityContext);
+        }
+
+        if (stateManager.isInTransition() && stateManager.getTargetState() != null) {
+            return stateManager.getTargetState()
+                    .getFloatingSearchBarRestingMarginEnd(mActivityContext);
+        }
+        return stateManager.getCurrentStableState()
+                .getFloatingSearchBarRestingMarginEnd(mActivityContext);
+    }
 }
diff --git a/src/com/android/launcher3/allapps/SearchTransitionController.java b/src/com/android/launcher3/allapps/SearchTransitionController.java
index b01ea53..eb1bc0a 100644
--- a/src/com/android/launcher3/allapps/SearchTransitionController.java
+++ b/src/com/android/launcher3/allapps/SearchTransitionController.java
@@ -20,12 +20,12 @@
 
 import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
 
+import static com.android.app.animation.Interpolators.DECELERATE_1_7;
+import static com.android.app.animation.Interpolators.INSTANT;
+import static com.android.app.animation.Interpolators.clampToProgress;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
-import static com.android.launcher3.anim.Interpolators.INSTANT;
-import static com.android.launcher3.anim.Interpolators.clampToProgress;
 
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
@@ -48,7 +48,7 @@
     private static final String LOG_TAG = "SearchTransitionCtrl";
 
     // Interpolator when the user taps the QSB while already in All Apps.
-    private static final Interpolator INTERPOLATOR_WITHIN_ALL_APPS = DEACCEL_1_7;
+    private static final Interpolator INTERPOLATOR_WITHIN_ALL_APPS = DECELERATE_1_7;
     // Interpolator when the user taps the QSB from home screen, so transition to all apps is
     // happening simultaneously.
     private static final Interpolator INTERPOLATOR_TRANSITIONING_TO_ALL_APPS = INSTANT;
diff --git a/src/com/android/launcher3/allapps/SearchUiManager.java b/src/com/android/launcher3/allapps/SearchUiManager.java
index 2174936..bfd8967 100644
--- a/src/com/android/launcher3/allapps/SearchUiManager.java
+++ b/src/com/android/launcher3/allapps/SearchUiManager.java
@@ -49,6 +49,14 @@
     ExtendedEditText getEditText();
 
     /**
+     * Hint to the edit text that it is about to be focused or unfocused. This can be used to start
+     * animating the edit box accordingly, e.g. after a gesture completes.
+     *
+     * @param focused true if the edit text is about to be focused, false if it will be unfocused
+     */
+    default void prepareToFocusEditText(boolean focused) {}
+
+    /**
      * Sets whether EditText background should be visible
      * @param maxAlpha defines the maximum alpha the background should animates to
      */
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
index f84876b..144381c 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -17,7 +17,6 @@
 
 import static com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.getTabWidth;
 
-import android.animation.LayoutTransition;
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
@@ -36,7 +35,6 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.StringCache;
 import com.android.launcher3.views.ActivityContext;
@@ -93,8 +91,6 @@
 
         setInsets(mActivityContext.getDeviceProfile().getInsets());
         updateStringFromCache();
-
-        getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
     }
 
     @Override
@@ -105,7 +101,7 @@
         if (lp != null) {
             int bottomMargin = getResources().getDimensionPixelSize(R.dimen.work_fab_margin_bottom);
             DeviceProfile dp = ActivityContext.lookupContext(getContext()).getDeviceProfile();
-            if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) {
+            if (mActivityContext.getAppsView().isSearchBarFloating()) {
                 bottomMargin += dp.hotseatQsbHeight;
             }
 
diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java
index 44c233f..1ac8d87 100644
--- a/src/com/android/launcher3/allapps/WorkProfileManager.java
+++ b/src/com/android/launcher3/allapps/WorkProfileManager.java
@@ -25,7 +25,6 @@
 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.testing.shared.TestProtocol.WORK_TAB_MISSING;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import android.os.Build;
@@ -47,7 +46,6 @@
 import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip;
 
 import java.lang.annotation.Retention;
@@ -144,10 +142,6 @@
     }
 
     private void updateCurrentState(@WorkProfileState int currentState) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(WORK_TAB_MISSING, "WorkProfileManager#updateCurrentState: " +
-                    currentState, new Throwable());
-        }
         mCurrentState = currentState;
         if (getAH() != null) {
             getAH().mAppsList.updateAdapterItems();
@@ -166,10 +160,6 @@
      * Creates and attaches for profile toggle button to {@link ActivityAllAppsContainerView}
      */
     public boolean attachWorkModeSwitch() {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(WORK_TAB_MISSING, "ActivityAllAppsContainerView#attachWorkModeSwitch "
-                    + "mWorkModeSwitch: " + mWorkModeSwitch);
-        }
         if (!mAllApps.getAppsStore().hasModelFlag(
                 FLAG_HAS_SHORTCUT_PERMISSION | FLAG_QUIET_MODE_CHANGE_PERMISSION)) {
             Log.e(TAG, "unable to attach work mode switch; Missing required permissions");
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index 4427a49..ecbc7a9 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -160,7 +160,7 @@
      * Focuses the search field to handle key events.
      */
     public void focusSearchField() {
-        mInput.showKeyboard();
+        mInput.showKeyboard(true /* shouldFocus */);
     }
 
     /**
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java b/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java
new file mode 100644
index 0000000..2347bfd
--- /dev/null
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.allapps.search;
+
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.ActivityAllAppsContainerView;
+import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem;
+import com.android.launcher3.views.ActivityContext;
+
+import java.util.List;
+
+/** Initializes the search box and its interactions with All Apps. */
+public class AllAppsSearchUiDelegate {
+
+    protected final ActivityAllAppsContainerView<?> mAppsView;
+    protected final ActivityContext mActivityContext;
+
+    public AllAppsSearchUiDelegate(ActivityAllAppsContainerView<?> appsView) {
+        mAppsView = appsView;
+        mActivityContext = ActivityContext.lookupContext(mAppsView.getContext());
+    }
+
+    /** Invoked when an All Apps {@link RecyclerView} is initialized. */
+    public void onInitializeRecyclerView(RecyclerView rv) {
+        // Do nothing.
+    }
+
+    /** Invoked when search results are updated in All Apps. */
+    public void onSearchResultsChanged(List<AdapterItem> results, int searchResultCode) {
+        // Do nothing.
+    }
+
+    /** Invoked when transition animations to go to search is completed . */
+    public void onAnimateToSearchStateCompleted() {
+        // Do nothing
+    }
+
+    /** Invoked when the search bar has been added to All Apps. */
+    public void onInitializeSearchBar() {
+        // Do nothing.
+    }
+
+    /** Invoked when the search bar has been removed from All Apps. */
+    public void onDestroySearchBar() {
+        // Do nothing.
+    }
+
+    /** The layout inflater for All Apps and search UI. */
+    public LayoutInflater getLayoutInflater() {
+        return LayoutInflater.from(mAppsView.getContext());
+    }
+
+    /** Inflate the search bar for All Apps. */
+    public View inflateSearchBar() {
+        return getLayoutInflater().inflate(R.layout.search_container_all_apps, mAppsView, false);
+    }
+
+    /** Whether the search box is floating above the apps surface (inset by the IME). */
+    public boolean isSearchBarFloating() {
+        return false;
+    }
+
+    /** Creates the adapter provider for the main section. */
+    public SearchAdapterProvider<?> createMainAdapterProvider() {
+        return new DefaultSearchAdapterProvider(mActivityContext);
+    }
+}
diff --git a/src/com/android/launcher3/anim/AlphaUpdateListener.java b/src/com/android/launcher3/anim/AlphaUpdateListener.java
index 8dad1b4..4382174 100644
--- a/src/com/android/launcher3/anim/AlphaUpdateListener.java
+++ b/src/com/android/launcher3/anim/AlphaUpdateListener.java
@@ -53,8 +53,18 @@
     }
 
     public static void updateVisibility(View view) {
-        if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != View.INVISIBLE) {
-            view.setVisibility(View.INVISIBLE);
+        updateVisibility(view, View.INVISIBLE);
+    }
+
+    /**
+     * Update view's visibility.
+     *
+     * @param view View that needs to update visibility.
+     * @param hiddenVisibility {@link View#GONE} or {@link View#INVISIBLE}
+     */
+    public static void updateVisibility(View view, int hiddenVisibility) {
+        if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != hiddenVisibility) {
+            view.setVisibility(hiddenVisibility);
         } else if (view.getAlpha() > ALPHA_CUTOFF_THRESHOLD
                 && view.getVisibility() != View.VISIBLE) {
             if (view instanceof ViewGroup) {
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 1cc0c21..d11a51f 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -15,10 +15,10 @@
  */
 package com.android.launcher3.anim;
 
+import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.clampToProgress;
+import static com.android.app.animation.Interpolators.scrollInterpolatorForVelocity;
 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;
 import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
 
 import android.animation.Animator;
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
deleted file mode 100644
index e886543..0000000
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.anim;
-
-import android.graphics.Path;
-import android.view.animation.AccelerateDecelerateInterpolator;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
-import android.view.animation.LinearInterpolator;
-import android.view.animation.OvershootInterpolator;
-import android.view.animation.PathInterpolator;
-
-import com.android.launcher3.Utilities;
-
-/**
- * Common interpolators used in Launcher
- */
-public class Interpolators {
-
-    public static final Interpolator LINEAR = new LinearInterpolator();
-
-    public static final Interpolator ACCEL = new AccelerateInterpolator();
-    public static final Interpolator ACCEL_0_5 = new AccelerateInterpolator(0.5f);
-    public static final Interpolator ACCEL_0_75 = new AccelerateInterpolator(0.75f);
-    public static final Interpolator ACCEL_1_5 = new AccelerateInterpolator(1.5f);
-    public static final Interpolator ACCEL_2 = new AccelerateInterpolator(2);
-
-    public static final Interpolator DEACCEL = new DecelerateInterpolator();
-    public static final Interpolator DEACCEL_1_5 = new DecelerateInterpolator(1.5f);
-    public static final Interpolator DEACCEL_1_7 = new DecelerateInterpolator(1.7f);
-    public static final Interpolator DEACCEL_2 = new DecelerateInterpolator(2);
-    public static final Interpolator DEACCEL_2_5 = new DecelerateInterpolator(2.5f);
-    public static final Interpolator DEACCEL_3 = new DecelerateInterpolator(3f);
-
-    public static final Interpolator ACCEL_DEACCEL = new AccelerateDecelerateInterpolator();
-
-    public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
-
-    public static final Interpolator AGGRESSIVE_EASE = new PathInterpolator(0.2f, 0f, 0f, 1f);
-    public static final Interpolator AGGRESSIVE_EASE_IN_OUT = new PathInterpolator(0.6f,0, 0.4f, 1);
-
-    public static final Interpolator DECELERATED_EASE = new PathInterpolator(0, 0, .2f, 1f);
-    public static final Interpolator ACCELERATED_EASE = new PathInterpolator(0.4f, 0, 1f, 1f);
-    public static final Interpolator PREDICTIVE_BACK_DECELERATED_EASE =
-            new PathInterpolator(0, 0, 0, 1f);
-
-    /**
-     * The default emphasized interpolator. Used for hero / emphasized movement of content.
-     */
-    public static final Interpolator EMPHASIZED = createEmphasizedInterpolator();
-    public static final Interpolator EMPHASIZED_ACCELERATE = new PathInterpolator(
-            0.3f, 0f, 0.8f, 0.15f);
-    public static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator(
-            0.05f, 0.7f, 0.1f, 1f);
-
-    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;
-
-    static {
-        Path exaggeratedEase = new Path();
-        exaggeratedEase.moveTo(0, 0);
-        exaggeratedEase.cubicTo(0.05f, 0f, 0.133333f, 0.08f, 0.166666f, 0.4f);
-        exaggeratedEase.cubicTo(0.225f, 0.94f, 0.5f, 1f, 1f, 1f);
-        EXAGGERATED_EASE = new PathInterpolator(exaggeratedEase);
-    }
-
-    public static final Interpolator OVERSHOOT_0_75 = new OvershootInterpolator(0.75f);
-    public static final Interpolator OVERSHOOT_1_2 = new OvershootInterpolator(1.2f);
-    public static final Interpolator OVERSHOOT_1_7 = new OvershootInterpolator(1.7f);
-
-    public static final Interpolator TOUCH_RESPONSE_INTERPOLATOR =
-            new PathInterpolator(0.3f, 0f, 0.1f, 1f);
-    public static final Interpolator TOUCH_RESPONSE_INTERPOLATOR_ACCEL_DEACCEL =
-            v -> ACCEL_DEACCEL.getInterpolation(TOUCH_RESPONSE_INTERPOLATOR.getInterpolation(v));
-
-    /**
-     * Inversion of ZOOM_OUT, compounded with an ease-out.
-     */
-    public static final Interpolator ZOOM_IN = new Interpolator() {
-        @Override
-        public float getInterpolation(float v) {
-            return DEACCEL_3.getInterpolation(1 - ZOOM_OUT.getInterpolation(1 - v));
-        }
-    };
-
-    public static final Interpolator ZOOM_OUT = new Interpolator() {
-
-        private static final float FOCAL_LENGTH = 0.35f;
-
-        @Override
-        public float getInterpolation(float v) {
-            return zInterpolate(v);
-        }
-
-        /**
-         * This interpolator emulates the rate at which the perceived scale of an object changes
-         * as its distance from a camera increases. When this interpolator is applied to a scale
-         * animation on a view, it evokes the sense that the object is shrinking due to moving away
-         * from the camera.
-         */
-        private float zInterpolate(float input) {
-            return (1.0f - FOCAL_LENGTH / (FOCAL_LENGTH + input)) /
-                    (1.0f - FOCAL_LENGTH / (FOCAL_LENGTH + 1.0f));
-        }
-    };
-
-    public static final Interpolator SCROLL = new Interpolator() {
-        @Override
-        public float getInterpolation(float t) {
-            t -= 1.0f;
-            return t*t*t*t*t + 1;
-        }
-    };
-
-    public static final Interpolator SCROLL_CUBIC = new Interpolator() {
-        @Override
-        public float getInterpolation(float t) {
-            t -= 1.0f;
-            return t*t*t + 1;
-        }
-    };
-
-    private static final float FAST_FLING_PX_MS = 10;
-
-    public static Interpolator scrollInterpolatorForVelocity(float velocity) {
-        return Math.abs(velocity) > FAST_FLING_PX_MS ? SCROLL : SCROLL_CUBIC;
-    }
-
-    /**
-     * Create an OvershootInterpolator with tension directly related to the velocity (in px/ms).
-     * @param velocity The start velocity of the animation we want to overshoot.
-     */
-    public static Interpolator overshootInterpolatorForVelocity(float velocity) {
-        return new OvershootInterpolator(Math.min(Math.abs(velocity), 3f));
-    }
-
-    /**
-     * Returns a function that runs the given interpolator such that the entire progress is set
-     * between the given bounds. That is, we set the interpolation to 0 until lowerBound and reach
-     * 1 by upperBound.
-     */
-    public static Interpolator clampToProgress(Interpolator interpolator, float lowerBound,
-            float upperBound) {
-        if (upperBound < lowerBound) {
-            throw new IllegalArgumentException(
-                    String.format("upperBound (%f) must be greater than lowerBound (%f)",
-                            upperBound, lowerBound));
-        }
-        return t -> clampToProgress(interpolator, t, lowerBound, upperBound);
-    }
-
-    /**
-     * Returns the progress value's progress between the lower and upper bounds. That is, the
-     * progress will be 0f from 0f to lowerBound, and reach 1f by upperBound.
-     *
-     * Between lowerBound and upperBound, the progress value will be interpolated using the provided
-     * interpolator.
-     */
-    public static float clampToProgress(
-            Interpolator interpolator, float progress, float lowerBound, float upperBound) {
-        if (upperBound < lowerBound) {
-            throw new IllegalArgumentException(
-                    String.format("upperBound (%f) must be greater than lowerBound (%f)",
-                            upperBound, lowerBound));
-        }
-
-        if (progress == lowerBound && progress == upperBound) {
-            return progress == 0f ? 0 : 1;
-        }
-        if (progress < lowerBound) {
-            return 0;
-        }
-        if (progress > upperBound) {
-            return 1;
-        }
-        return interpolator.getInterpolation((progress - lowerBound) / (upperBound - lowerBound));
-    }
-
-    /**
-     * Returns the progress value's progress between the lower and upper bounds. That is, the
-     * progress will be 0f from 0f to lowerBound, and reach 1f by upperBound.
-     */
-    public static float clampToProgress(float progress, float lowerBound, float upperBound) {
-        return clampToProgress(Interpolators.LINEAR, progress, lowerBound, upperBound);
-    }
-
-    /**
-     * Runs the given interpolator such that the interpolated value is mapped to the given range.
-     * This is useful, for example, if we only use this interpolator for part of the animation,
-     * such as to take over a user-controlled animation when they let go.
-     */
-    public static Interpolator mapToProgress(Interpolator interpolator, float lowerBound,
-            float upperBound) {
-        return t -> Utilities.mapRange(interpolator.getInterpolation(t), lowerBound, upperBound);
-    }
-
-    /**
-     * Returns the reverse of the provided interpolator, following the formula: g(x) = 1 - f(1 - x).
-     * In practice, this means that if f is an interpolator used to model a value animating between
-     * m and n, g is the interpolator to use to obtain the specular behavior when animating from n
-     * to m.
-     */
-    public static Interpolator reverse(Interpolator interpolator) {
-        return t -> 1 - interpolator.getInterpolation(1 - t);
-    }
-
-    // Create the default emphasized interpolator
-    private static PathInterpolator createEmphasizedInterpolator() {
-        Path path = new Path();
-        // Doing the same as fast_out_extra_slow_in
-        path.moveTo(0f, 0f);
-        path.cubicTo(0.05f, 0f, 0.133333f, 0.06f, 0.166666f, 0.4f);
-        path.cubicTo(0.208333f, 0.82f, 0.25f, 1f, 1f, 1f);
-        return new PathInterpolator(path);
-    }
-}
diff --git a/src/com/android/launcher3/anim/KeyboardInsetAnimationCallback.java b/src/com/android/launcher3/anim/KeyboardInsetAnimationCallback.java
index b911928..39386fa 100644
--- a/src/com/android/launcher3/anim/KeyboardInsetAnimationCallback.java
+++ b/src/com/android/launcher3/anim/KeyboardInsetAnimationCallback.java
@@ -44,14 +44,30 @@
 
     private float mInitialTranslation;
     private float mTerminalTranslation;
+    private KeyboardTranslationState mKeyboardTranslationState = KeyboardTranslationState.SYSTEM;
+
+    /** Current state of the keyboard. */
+    public enum KeyboardTranslationState {
+        // We are not controlling the keyboard, and it may or may not be translating.
+        SYSTEM,
+        // We are about to gain control of the keyboard, but the current state may be transient.
+        MANUAL_PREPARED,
+        // We are manually translating the keyboard.
+        MANUAL_ONGOING
+    }
 
     public KeyboardInsetAnimationCallback(View view) {
         super(DISPATCH_MODE_STOP);
         mView = view;
     }
 
+    public KeyboardTranslationState getKeyboardTranslationState() {
+        return mKeyboardTranslationState;
+    }
+
     @Override
     public void onPrepare(WindowInsetsAnimation animation) {
+        mKeyboardTranslationState = KeyboardTranslationState.MANUAL_PREPARED;
         mInitialTranslation = mView.getTranslationY();
     }
 
@@ -62,6 +78,7 @@
         mTerminalTranslation = mView.getTranslationY();
         // Reset the translation in case the view is drawn before onProgress gets called.
         mView.setTranslationY(mInitialTranslation);
+        mKeyboardTranslationState = KeyboardTranslationState.MANUAL_ONGOING;
         if (mView instanceof KeyboardInsetListener) {
             ((KeyboardInsetListener) mView).onTranslationStart();
         }
@@ -90,6 +107,10 @@
             mView.setTranslationY(translationY);
         }
 
+        if (mView instanceof KeyboardInsetListener) {
+            ((KeyboardInsetListener) mView).onKeyboardAlphaChanged(animation.getAlpha());
+        }
+
         return windowInsets;
     }
 
@@ -98,7 +119,7 @@
         if (mView instanceof KeyboardInsetListener) {
             ((KeyboardInsetListener) mView).onTranslationEnd();
         }
-        super.onEnd(animation);
+        mKeyboardTranslationState = KeyboardTranslationState.SYSTEM;
     }
 
     /**
@@ -111,6 +132,13 @@
         void onTranslationStart();
 
         /**
+         * Called from {@link KeyboardInsetAnimationCallback#onProgress}
+         *
+         * @param alpha the current IME alpha
+         */
+        default void onKeyboardAlphaChanged(float alpha) {}
+
+        /**
          * Called from {@link KeyboardInsetAnimationCallback#onEnd}
          */
         void onTranslationEnd();
diff --git a/src/com/android/launcher3/anim/SpringAnimationBuilder.java b/src/com/android/launcher3/anim/SpringAnimationBuilder.java
index 40fa0cf..bc7b7f0 100644
--- a/src/com/android/launcher3/anim/SpringAnimationBuilder.java
+++ b/src/com/android/launcher3/anim/SpringAnimationBuilder.java
@@ -15,7 +15,7 @@
  */
 package com.android.launcher3.anim;
 
-import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.LINEAR;
 
 import android.animation.Animator;
 import android.animation.ValueAnimator;
diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java
new file mode 100644
index 0000000..1dc4ad2
--- /dev/null
+++ b/src/com/android/launcher3/apppairs/AppPairIcon.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.apppairs;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.R;
+import com.android.launcher3.dragndrop.DraggableView;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.views.ActivityContext;
+
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * A {@link android.widget.FrameLayout} used to represent an app pair icon on the workspace.
+ */
+public class AppPairIcon extends FrameLayout implements DraggableView {
+
+    private ActivityContext mActivity;
+    private BubbleTextView mAppPairName;
+    private FolderInfo mInfo;
+
+    public AppPairIcon(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AppPairIcon(Context context) {
+        super(context);
+    }
+
+    /**
+     * Builds an AppPairIcon to be added to the Launcher
+     */
+    public static AppPairIcon inflateIcon(int resId, ActivityContext activity,
+            @Nullable ViewGroup group, FolderInfo appPairInfo) {
+
+        LayoutInflater inflater = (group != null)
+                ? LayoutInflater.from(group.getContext())
+                : activity.getLayoutInflater();
+        AppPairIcon icon = (AppPairIcon) inflater.inflate(resId, group, false);
+
+        // Sort contents, so that left-hand app comes first
+        Collections.sort(appPairInfo.contents, Comparator.comparingInt(a -> a.rank));
+
+        icon.setClipToPadding(false);
+        icon.mAppPairName = icon.findViewById(R.id.app_pair_icon_name);
+
+        // TODO (jeremysim b/274189428): Replace this placeholder icon
+        WorkspaceItemInfo placeholder = new WorkspaceItemInfo();
+        placeholder.newIcon(icon.getContext());
+        icon.mAppPairName.applyFromWorkspaceItem(placeholder);
+
+        icon.mAppPairName.setText(appPairInfo.title);
+
+        icon.setTag(appPairInfo);
+        icon.setOnClickListener(activity.getItemOnClickListener());
+        icon.mInfo = appPairInfo;
+        icon.mActivity = activity;
+
+        icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
+
+        return icon;
+    }
+
+    @Override
+    public int getViewType() {
+        return DRAGGABLE_ICON;
+    }
+
+    @Override
+    public void getWorkspaceVisualDragBounds(Rect bounds) {
+        mAppPairName.getIconBounds(bounds);
+    }
+
+    public FolderInfo getInfo() {
+        return mInfo;
+    }
+}
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index e08c671..77bd898 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -82,15 +82,12 @@
      * <p>
      */
     // TODO(Block 1): Clean up flags
-    public static final BooleanFlag ENABLE_ONE_SEARCH_MOTION = getReleaseFlag(270394223,
-            "ENABLE_ONE_SEARCH_MOTION", ENABLED, "Enables animations in OneSearch.");
-
     public static final BooleanFlag ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES = getReleaseFlag(
-            270394041, "ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES", DISABLED,
+            270394041, "ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES", ENABLED,
             "Enable option to replace decorator-based search result backgrounds with drawables");
 
     public static final BooleanFlag ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION = getReleaseFlag(
-            270394392, "ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION", DISABLED,
+            270394392, "ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION", ENABLED,
             "Enable option to launch search results using the new view container transitions");
 
     // TODO(Block 2): Clean up flags
@@ -119,7 +116,8 @@
     // TODO(Block 4): Cleanup flags
     public static final BooleanFlag ENABLE_FLOATING_SEARCH_BAR =
             getReleaseFlag(268388460, "ENABLE_FLOATING_SEARCH_BAR", DISABLED,
-                    "Keep All Apps search bar at the bottom (but above keyboard if open)");
+                    "Allow search bar to persist and animate across states, and attach to"
+                            + " the keyboard from the bottom of the screen");
 
     public static final BooleanFlag ENABLE_ALL_APPS_FROM_OVERVIEW =
             getDebugFlag(275132633, "ENABLE_ALL_APPS_FROM_OVERVIEW", DISABLED,
@@ -131,7 +129,7 @@
 
     // TODO(Block 5): Clean up flags
     public static final BooleanFlag ENABLE_TWOLINE_DEVICESEARCH = getDebugFlag(201388851,
-            "ENABLE_TWOLINE_DEVICESEARCH", TEAMFOOD,
+            "ENABLE_TWOLINE_DEVICESEARCH", ENABLED,
             "Enable two line label for icons with labels on device search.");
 
     public static final BooleanFlag ENABLE_ICON_IN_TEXT_HEADER = getDebugFlag(270395143,
@@ -185,17 +183,13 @@
             "Enables predictive back animation from all apps and widgets to home");
 
     // TODO(Block 11): Clean up flags
-    public static final BooleanFlag ENABLE_TWO_PANEL_HOME = getDebugFlag(270392643,
-            "ENABLE_TWO_PANEL_HOME", ENABLED,
-            "Uses two panel on home screen. Only applicable on large screen devices.");
-
-    public static final BooleanFlag FOLDABLE_WORKSPACE_REORDER = getDebugFlag(270395070,
-            "FOLDABLE_WORKSPACE_REORDER", DISABLED,
-            "In foldables, when reordering the icons and widgets, is now going to use both sides");
-
     public static final BooleanFlag FOLDABLE_SINGLE_PAGE = getDebugFlag(270395274,
             "FOLDABLE_SINGLE_PAGE", DISABLED, "Use a single page for the workspace");
 
+    public static final BooleanFlag ENABLE_PARAMETRIZE_REORDER = getDebugFlag(289420844,
+            "ENABLE_PARAMETRIZE_REORDER", DISABLED,
+            "Enables generating the reorder using a set of parameters");
+
     // TODO(Block 12): Clean up flags
     public static final BooleanFlag ENABLE_MULTI_INSTANCE = getDebugFlag(270396680,
             "ENABLE_MULTI_INSTANCE", DISABLED,
@@ -236,7 +230,7 @@
             "COLLECT_SEARCH_HISTORY", DISABLED, "Allow launcher to collect search history for log");
 
     public static final BooleanFlag ENABLE_TWOLINE_ALLAPPS = getDebugFlag(270390937,
-            "ENABLE_TWOLINE_ALLAPPS", TEAMFOOD, "Enables two line label inside all apps.");
+            "ENABLE_TWOLINE_ALLAPPS", DISABLED, "Enables two line label inside all apps.");
 
     public static final BooleanFlag IME_STICKY_SNACKBAR_EDU = getDebugFlag(270391693,
             "IME_STICKY_SNACKBAR_EDU", ENABLED, "Show sticky IME edu in AllApps");
@@ -253,6 +247,10 @@
             "INJECT_FALLBACK_APP_CORPUS_RESULTS", DISABLED,
             "Inject fallback app corpus result when AiAi fails to return it.");
 
+    public static final BooleanFlag ENABLE_LONG_PRESS_NAV_HANDLE =
+            getReleaseFlag(282993230, "ENABLE_LONG_PRESS_NAV_HANDLE", TEAMFOOD,
+                    "Enables long pressing on the bottom bar nav handle to trigger events.");
+
     // TODO(Block 17): Clean up flags
     public static final BooleanFlag ENABLE_TASKBAR_PINNING = getDebugFlag(270396583,
             "ENABLE_TASKBAR_PINNING", DISABLED,
@@ -260,11 +258,6 @@
                     + "taskbar flavors");
 
     // TODO(Block 18): Clean up flags
-    public static final BooleanFlag ENABLE_LAUNCH_FROM_STAGED_APP = getDebugFlag(270395567,
-            "ENABLE_LAUNCH_FROM_STAGED_APP", ENABLED,
-            "Enable the ability to tap a staged app during split select to launch it in full "
-                    + "screen");
-
     public static final BooleanFlag ENABLE_APP_PAIRS = getDebugFlag(274189428,
             "ENABLE_APP_PAIRS", DISABLED,
             "Enables the ability to create and save app pairs on the Home screen for easy"
@@ -287,16 +280,15 @@
             "ENABLE_BACK_SWIPE_HOME_ANIMATION", ENABLED,
             "Enables home animation to icon when user swipes back.");
 
+    public static final BooleanFlag ENABLE_DYNAMIC_TASKBAR_THRESHOLDS = getDebugFlag(294252473,
+            "ENABLE_DYNAMIC_TASKBAR_THRESHOLDS", ENABLED,
+            "Enables taskbar thresholds that scale based on screen size.");
+
     // TODO(Block 21): Clean up flags
     public static final BooleanFlag ENABLE_APP_ICON_FOR_INLINE_SHORTCUTS = getDebugFlag(270395087,
             "ENABLE_APP_ICON_IN_INLINE_SHORTCUTS", DISABLED, "Show app icon for inline shortcut");
 
     // TODO(Block 22): Clean up flags
-    public static final BooleanFlag RECEIVE_UNFOLD_EVENTS_FROM_SYSUI = getDebugFlag(270397209,
-            "RECEIVE_UNFOLD_EVENTS_FROM_SYSUI", ENABLED,
-            "Enables receiving unfold animation events from sysui instead of calculating "
-                    + "them in launcher process using hinge sensor values.");
-
     public static final BooleanFlag ENABLE_WIDGET_TRANSITION_FOR_RESIZING = getDebugFlag(268553314,
             "ENABLE_WIDGET_TRANSITION_FOR_RESIZING", DISABLED,
             "Enable widget transition animation when resizing the widgets");
@@ -309,11 +301,11 @@
 
     // TODO(Block 23): Clean up flags
     public static final BooleanFlag ENABLE_GRID_ONLY_OVERVIEW = getDebugFlag(270397206,
-            "ENABLE_GRID_ONLY_OVERVIEW", DISABLED,
+            "ENABLE_GRID_ONLY_OVERVIEW", TEAMFOOD,
             "Enable a grid-only overview without a focused task.");
 
     public static final BooleanFlag ENABLE_CURSOR_HOVER_STATES = getDebugFlag(243191650,
-            "ENABLE_CURSOR_HOVER_STATES", DISABLED,
+            "ENABLE_CURSOR_HOVER_STATES", TEAMFOOD,
             "Enables cursor hover states for certain elements.");
 
     // TODO(Block 24): Clean up flags
@@ -368,10 +360,6 @@
             "Use inbuilt monochrome icons if app doesn't provide one");
 
     // TODO(Block 28): Clean up flags
-    public static final BooleanFlag ENABLE_SPLIT_FROM_WORKSPACE = getDebugFlag(270393906,
-            "ENABLE_SPLIT_FROM_WORKSPACE", ENABLED,
-            "Enable initiating split screen from workspace.");
-
     public static final BooleanFlag ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS =
             getDebugFlag(270394122, "ENABLE_SPLIT_FROM_FULLSCREEN_SHORTCUT", DISABLED,
                     "Enable splitting from fullscreen app with keyboard shortcuts");
@@ -380,8 +368,12 @@
             270393453, "ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE", DISABLED,
             "Enable initiating split screen from workspace to workspace.");
 
+    public static final BooleanFlag ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE = getDebugFlag(
+            279586624, "ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE", DISABLED,
+            "Enable initiating split screen from desktop mode to workspace.");
+
     public static final BooleanFlag ENABLE_TRACKPAD_GESTURE = getDebugFlag(271010401,
-            "ENABLE_TRACKPAD_GESTURE", DISABLED, "Enables trackpad gesture.");
+            "ENABLE_TRACKPAD_GESTURE", ENABLED, "Enables trackpad gesture.");
 
     // TODO(Block 29): Clean up flags
     public static final BooleanFlag ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT = getDebugFlag(270393897,
@@ -400,17 +392,25 @@
             "USE_SEARCH_REQUEST_TIMEOUT_OVERRIDES", DISABLED,
             "Use local overrides for search request timeout");
 
-    // TODO(Block 31)
-    public static final BooleanFlag ENABLE_SPLIT_LAUNCH_DATA_REFACTOR = getDebugFlag(279494325,
-            "ENABLE_SPLIT_LAUNCH_DATA_REFACTOR", ENABLED,
-            "Use refactored split launching code path");
+    // TODO(Block 31): Clean up flags
 
-    // TODO(Block 32): Empty block
-
+    // TODO(Block 32): Clean up flags
     public static final BooleanFlag ENABLE_RESPONSIVE_WORKSPACE = getDebugFlag(241386436,
             "ENABLE_RESPONSIVE_WORKSPACE", DISABLED,
             "Enables new workspace grid calculations method.");
 
+    // TODO(Block 33): Clean up flags
+    public static final BooleanFlag ENABLE_ALL_APPS_RV_PREINFLATION = getDebugFlag(288161355,
+            "ENABLE_ALL_APPS_RV_PREINFLATION", ENABLED,
+            "Enables preinflating all apps icons to avoid scrolling jank.");
+
+    // TODO(Block 34): Clean up flags
+    public static final BooleanFlag ALL_APPS_GONE_VISIBILITY = getDebugFlag(291651514,
+            "ALL_APPS_GONE_VISIBILITY", ENABLED,
+            "Set all apps container view's hidden visibility to GONE instead of INVISIBLE.");
+
+    // TODO(Block 35): Empty block
+
     public static class BooleanFlag {
 
         private final boolean mCurrentValue;
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index 00f4285..213c458 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -26,7 +26,6 @@
 import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY;
 
 import android.annotation.TargetApi;
-import android.app.ActivityOptions;
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ClipData;
@@ -68,6 +67,7 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.pm.PinRequestHelper;
+import com.android.launcher3.uioverrides.ApiWrapper;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.views.AbstractSlideInView;
@@ -259,9 +259,7 @@
                         .setPackage(getPackageName())
                         .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         Launcher.ACTIVITY_TRACKER.registerCallback(listener);
-        startActivity(homeIntent,
-                ActivityOptions.makeCustomAnimation(this, 0, android.R.anim.fade_out)
-                        .toBundle());
+        startActivity(homeIntent, ApiWrapper.createFadeOutAnimOptions(this).toBundle());
         logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_DRAGGED);
         mFinishOnPause = true;
         return false;
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 9867268..0d51d48 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -28,9 +28,9 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 366870b..f18f900 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -19,11 +19,11 @@
 
 import static android.animation.ObjectAnimator.ofFloat;
 
+import static com.android.app.animation.Interpolators.DECELERATE_1_5;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.Utilities.mapRange;
 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
 
 import android.animation.Animator;
@@ -42,6 +42,7 @@
 import android.view.accessibility.AccessibilityManager;
 import android.view.animation.Interpolator;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DropTargetBar;
 import com.android.launcher3.Launcher;
@@ -49,7 +50,6 @@
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.SpringProperty;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
@@ -340,14 +340,14 @@
         if (duration < 0) {
             duration = res.getInteger(R.integer.config_dropAnimMaxDuration);
             if (dist < maxDist) {
-                duration *= DEACCEL_1_5.getInterpolation(dist / maxDist);
+                duration *= DECELERATE_1_5.getInterpolation(dist / maxDist);
             }
             duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration));
         }
 
         // Fall back to cubic ease out interpolator for the animation if none is specified
         TimeInterpolator interpolator =
-                motionInterpolator == null ? DEACCEL_1_5 : motionInterpolator;
+                motionInterpolator == null ? DECELERATE_1_5 : motionInterpolator;
 
         // Animate the view
         PendingAnimation anim = new PendingAnimation(duration);
@@ -475,7 +475,7 @@
 
     @Override
     public void onOverlayScrollChanged(float progress) {
-        float alpha = 1 - Interpolators.DEACCEL_3.getInterpolation(progress);
+        float alpha = 1 - Interpolators.DECELERATE_3.getInterpolation(progress);
         float transX = getMeasuredWidth() * progress;
 
         if (mIsRtl) {
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 0d0717e..adfdc89 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -55,9 +55,9 @@
 import androidx.dynamicanimation.animation.SpringAnimation;
 import androidx.dynamicanimation.animation.SpringForce;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.icons.FastBitmapDrawable;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.model.data.ItemInfo;
@@ -99,7 +99,9 @@
 
     // Whether mAnim has started. Unlike mAnim.isStarted(), this is true even after mAnim ends.
     private boolean mScaleAnimStarted;
-    private Runnable mOnAnimEndCallback = null;
+    private boolean mShiftAnimStarted;
+    private Runnable mOnScaleAnimEndCallback;
+    private Runnable mOnShiftAnimEndCallback;
 
     private int mLastTouchX;
     private int mLastTouchY;
@@ -186,13 +188,26 @@
             @Override
             public void onAnimationEnd(Animator animation) {
                 super.onAnimationEnd(animation);
-                if (mOnAnimEndCallback != null) {
-                    mOnAnimEndCallback.run();
+                if (mOnScaleAnimEndCallback != null) {
+                    mOnScaleAnimEndCallback.run();
                 }
             }
         });
         // Set up the shift animator.
         mShiftAnim = ValueAnimator.ofFloat(0f, 1f);
+        mShiftAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mShiftAnimStarted = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mOnShiftAnimEndCallback != null) {
+                    mOnShiftAnimEndCallback.run();
+                }
+            }
+        });
 
         setDragRegion(new Rect(0, 0, width, height));
 
@@ -211,8 +226,14 @@
         setWillNotDraw(false);
     }
 
-    public void setOnAnimationEndCallback(Runnable callback) {
-        mOnAnimEndCallback = callback;
+    /** Callback invoked when the scale animation ends. */
+    public void setOnScaleAnimEndCallback(Runnable callback) {
+        mOnScaleAnimEndCallback = callback;
+    }
+
+    /** Callback invoked when the shift animation ends. */
+    public void setOnShiftAnimEndCallback(Runnable callback) {
+        mOnShiftAnimEndCallback = callback;
     }
 
     /**
@@ -224,11 +245,11 @@
         // Load the adaptive icon on a background thread and add the view in ui thread.
         MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
             Object[] outObj = new Object[1];
+            boolean[] outIsIconThemed = new boolean[1];
             int w = mWidth;
             int h = mHeight;
             Drawable dr = Utilities.getFullDrawable(mActivity, info, w, h,
-                    true /* shouldThemeIcon */, outObj);
-
+                    true /* shouldThemeIcon */, outObj, outIsIconThemed);
             if (dr instanceof AdaptiveIconDrawable) {
                 int blurMargin = (int) mActivity.getResources()
                         .getDimension(R.dimen.blur_size_medium_outline) / 2;
@@ -237,7 +258,7 @@
                 bounds.inset(blurMargin, blurMargin);
                 // Badge is applied after icon normalization so the bounds for badge should not
                 // be scaled down due to icon normalization.
-                mBadge = getBadge(mActivity, info, outObj[0]);
+                mBadge = getBadge(mActivity, info, outObj[0], outIsIconThemed[0]);
                 FastBitmapDrawable.setBadgeBounds(mBadge, bounds);
 
                 // Do not draw the background in case of folder as its translucent
@@ -371,7 +392,7 @@
         AnimatorSet anim = new AnimatorSet();
         anim.play(ObjectAnimator.ofFloat(newContent, VIEW_ALPHA, 0, 1));
         anim.play(ObjectAnimator.ofFloat(mContent, VIEW_ALPHA, 0));
-        anim.setDuration(duration).setInterpolator(Interpolators.DEACCEL_1_5);
+        anim.setDuration(duration).setInterpolator(Interpolators.DECELERATE_1_5);
         anim.start();
     }
 
@@ -416,10 +437,16 @@
         }
     }
 
+    /** {@code true} if the scale animation has finished. */
     public boolean isScaleAnimationFinished() {
         return mScaleAnimStarted && !mScaleAnim.isRunning();
     }
 
+    /** {@code true} if the shift animation has finished. */
+    public boolean isShiftAnimationFinished() {
+        return mShiftAnimStarted && !mShiftAnim.isRunning();
+    }
+
     /**
      * Move the window containing this view.
      *
diff --git a/src/com/android/launcher3/dragndrop/PinItemDragListener.java b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
index af43ae8..48b5646 100644
--- a/src/com/android/launcher3/dragndrop/PinItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
@@ -31,7 +31,6 @@
 import android.widget.RemoteViews;
 
 import com.android.launcher3.DragSource;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.PendingAddItemInfo;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
@@ -72,15 +71,6 @@
     }
 
     @Override
-    public boolean init(Launcher launcher, boolean alreadyOnHome) {
-        super.init(launcher, alreadyOnHome);
-        if (!alreadyOnHome) {
-            launcher.useFadeOutAnimationForLauncherStart(mCancelSignal);
-        }
-        return false;
-    }
-
-    @Override
     protected PendingItemDragHelper createDragHelper() {
         final PendingAddItemInfo item;
         if (mRequest.getRequestType() == PinItemRequest.REQUEST_TYPE_SHORTCUT) {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 4ae54e6..55a539a 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -535,7 +535,7 @@
                     mFolderName.selectAll();
                 }
             }
-            mFolderName.showKeyboard();
+            mFolderName.showKeyboard(true /* shouldFocus */);
             mFolderName.displayCompletions(
                     Stream.of(mInfo.suggestedFolderNames.getLabels())
                             .filter(Objects::nonNull)
@@ -881,7 +881,6 @@
         final ItemInfo item = d.dragInfo;
         final int itemType = item.itemType;
         return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
-                itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT ||
                 itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT));
     }
 
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index dd82ecf..9e2e2bf 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -105,11 +105,11 @@
         mDelay = res.getInteger(R.integer.config_folderDelay);
 
         mFolderInterpolator = AnimationUtils.loadInterpolator(mContext,
-                R.interpolator.folder_interpolator);
+                R.interpolator.standard_interpolator);
         mLargeFolderPreviewItemOpenInterpolator = AnimationUtils.loadInterpolator(mContext,
                 R.interpolator.large_folder_preview_item_open_interpolator);
         mLargeFolderPreviewItemCloseInterpolator = AnimationUtils.loadInterpolator(mContext,
-                R.interpolator.large_folder_preview_item_close_interpolator);
+                R.interpolator.standard_accelerate_interpolator);
     }
 
     /**
@@ -236,9 +236,9 @@
                 mFolder, startRect, endRect, finalRadius, !mIsOpening));
 
         // Create reveal animator for the folder content (capture the top 4 icons 2x2)
-        int width = mDeviceProfile.folderCellLayoutBorderSpacePx
+        int width = mDeviceProfile.folderCellLayoutBorderSpacePx.x
                 + mDeviceProfile.folderCellWidthPx * 2;
-        int height = mDeviceProfile.folderCellLayoutBorderSpacePx
+        int height = mDeviceProfile.folderCellLayoutBorderSpacePx.y
                 + mDeviceProfile.folderCellHeightPx * 2;
         int page = mIsOpening ? mContent.getCurrentPage() : mContent.getDestinationPage();
         int left = mContent.getPaddingLeft() + page * lp.width;
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 2c1100f..53d0efb 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.folder;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
 import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION;
@@ -42,6 +43,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.Alarm;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
@@ -56,7 +58,6 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
 import com.android.launcher3.dot.FolderDotInfo;
 import com.android.launcher3.dragndrop.BaseItemDragListener;
@@ -260,7 +261,6 @@
     private boolean willAcceptItem(ItemInfo item) {
         final int itemType = item.itemType;
         return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
-                itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT ||
                 itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) &&
                 item != mInfo && !mFolder.isOpen());
     }
@@ -407,7 +407,7 @@
             final int finalIndex = index;
             dragLayer.animateView(animateView, to, finalAlpha,
                     finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
-                    Interpolators.DEACCEL_2,
+                    Interpolators.DECELERATE_2,
                     () -> {
                         mPreviewItemManager.hidePreviewItem(finalIndex, false);
                         mFolder.showItem(item);
@@ -628,7 +628,7 @@
             Utilities.scaleRectAboutCenter(iconBounds, iconScale);
 
             // If we are animating to the accepting state, animate the dot out.
-            mDotParams.scale = Math.max(0, mDotScale - mBackground.getScaleProgress());
+            mDotParams.scale = Math.max(0, mDotScale - mBackground.getAcceptScaleProgress());
             mDotParams.dotColor = mBackground.getDotColor();
             mDotRenderer.draw(canvas, mDotParams);
         }
@@ -802,6 +802,14 @@
         }
     }
 
+    @Override
+    public void onHoverChanged(boolean hovered) {
+        super.onHoverChanged(hovered);
+        if (ENABLE_CURSOR_HOVER_STATES.get()) {
+            mBackground.setHovered(hovered);
+        }
+    }
+
     /**
      * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon.
      */
diff --git a/src/com/android/launcher3/folder/LauncherDelegate.java b/src/com/android/launcher3/folder/LauncherDelegate.java
index 7ac2472..c06a0f3 100644
--- a/src/com/android/launcher3/folder/LauncherDelegate.java
+++ b/src/com/android/launcher3/folder/LauncherDelegate.java
@@ -94,9 +94,6 @@
                         // folder
                         CellLayout cellLayout = mLauncher.getCellLayout(info.container,
                                 mLauncher.getCellPosMapper().mapModelToPresenter(info).screenId);
-                        if (cellLayout == null) {
-                            return;
-                        }
                         finalItem =  info.contents.remove(0);
                         newIcon = mLauncher.createShortcut(cellLayout, finalItem);
                         mLauncher.getModelWriter().addOrMoveItemInDatabase(finalItem,
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index 406955c..b320ceb 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.folder;
 
+import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
+import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
 import static com.android.launcher3.graphics.IconShape.getShape;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
@@ -39,6 +41,9 @@
 import android.graphics.Shader;
 import android.util.Property;
 import android.view.View;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
@@ -55,7 +60,10 @@
     private static final boolean DRAW_SHADOW = false;
     private static final boolean DRAW_STROKE = false;
 
-    private static final int CONSUMPTION_ANIMATION_DURATION = 100;
+    @VisibleForTesting protected static final int CONSUMPTION_ANIMATION_DURATION = 100;
+
+    @VisibleForTesting protected static final float HOVER_SCALE = 1.1f;
+    @VisibleForTesting protected static final int HOVER_ANIMATION_DURATION = 300;
 
     private final PorterDuffXfermode mShadowPorterDuffXfermode
             = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
@@ -86,17 +94,21 @@
     public boolean isClipping = true;
 
     // Drawing / animation configurations
-    private static final float ACCEPT_SCALE_FACTOR = 1.20f;
+    @VisibleForTesting protected static final float ACCEPT_SCALE_FACTOR = 1.20f;
 
     // Expressed on a scale from 0 to 255.
     private static final int BG_OPACITY = 255;
     private static final int MAX_BG_OPACITY = 255;
     private static final int SHADOW_OPACITY = 40;
 
-    private ValueAnimator mScaleAnimator;
+    @VisibleForTesting protected ValueAnimator mScaleAnimator;
     private ObjectAnimator mStrokeAlphaAnimator;
     private ObjectAnimator mShadowAnimator;
 
+    @VisibleForTesting protected boolean mIsAccepting;
+    @VisibleForTesting protected boolean mIsHovered;
+    @VisibleForTesting protected boolean mIsHoveredOrAnimating;
+
     private static final Property<PreviewBackground, Integer> STROKE_ALPHA =
             new Property<PreviewBackground, Integer>(Integer.class, "strokeAlpha") {
                 @Override
@@ -203,11 +215,11 @@
     }
 
     /**
-     * Returns the progress of the scale animation, where 0 means the scale is at 1f
-     * and 1 means the scale is at ACCEPT_SCALE_FACTOR.
+     * Returns the progress of the scale animation to accept state, where 0 means the scale is at
+     * 1f and 1 means the scale is at ACCEPT_SCALE_FACTOR. Returns 0 when scaled due to hover.
      */
-    float getScaleProgress() {
-        return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
+    float getAcceptScaleProgress() {
+        return mIsHoveredOrAnimating ? 0 : (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
     }
 
     void invalidate() {
@@ -385,60 +397,70 @@
         return mDrawingDelegate != null;
     }
 
-    private void animateScale(float finalScale, final Runnable onStart, final Runnable onEnd) {
-        final float scale0 = mScale;
-        final float scale1 = finalScale;
-
+    protected void animateScale(boolean isAccepting, boolean isHovered) {
         if (mScaleAnimator != null) {
             mScaleAnimator.cancel();
         }
 
-        mScaleAnimator = ValueAnimator.ofFloat(0f, 1.0f);
-
-        mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-            @Override
-            public void onAnimationUpdate(ValueAnimator animation) {
-                float prog = animation.getAnimatedFraction();
-                mScale = prog * scale1 + (1 - prog) * scale0;
-                invalidate();
+        final float startScale = mScale;
+        final float endScale = isAccepting ? ACCEPT_SCALE_FACTOR : (isHovered ? HOVER_SCALE : 1f);
+        Interpolator interpolator =
+                isAccepting != mIsAccepting ? ACCELERATE_DECELERATE : EMPHASIZED_DECELERATE;
+        int duration = isAccepting != mIsAccepting ? CONSUMPTION_ANIMATION_DURATION
+                : HOVER_ANIMATION_DURATION;
+        mIsAccepting = isAccepting;
+        mIsHovered = isHovered;
+        if (startScale == endScale) {
+            if (!mIsAccepting) {
+                clearDrawingDelegate();
             }
+            mIsHoveredOrAnimating = mIsHovered;
+            return;
+        }
+
+
+        mScaleAnimator = ValueAnimator.ofFloat(0f, 1.0f);
+        mScaleAnimator.addUpdateListener(animation -> {
+            float prog = animation.getAnimatedFraction();
+            mScale = prog * endScale + (1 - prog) * startScale;
+            invalidate();
         });
         mScaleAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
-                if (onStart != null) {
-                    onStart.run();
+                if (mIsHovered) {
+                    mIsHoveredOrAnimating = true;
                 }
             }
 
             @Override
             public void onAnimationEnd(Animator animation) {
-                if (onEnd != null) {
-                    onEnd.run();
+                if (!mIsAccepting) {
+                    clearDrawingDelegate();
                 }
+                mIsHoveredOrAnimating = mIsHovered;
                 mScaleAnimator = null;
             }
         });
-
-        mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
+        mScaleAnimator.setInterpolator(interpolator);
+        mScaleAnimator.setDuration(duration);
         mScaleAnimator.start();
     }
 
     public void animateToAccept(CellLayout cl, int cellX, int cellY) {
-        animateScale(ACCEPT_SCALE_FACTOR, () -> delegateDrawing(cl, cellX, cellY), null);
+        delegateDrawing(cl, cellX, cellY);
+        animateScale(/* isAccepting= */ true, mIsHovered);
     }
 
     public void animateToRest() {
-        // This can be called multiple times -- we need to make sure the drawing delegate
-        // is saved and restored at the beginning of the animation, since cancelling the
-        // existing animation can clear the delgate.
-        CellLayout cl = mDrawingDelegate;
-        int cellX = mDelegateCellX;
-        int cellY = mDelegateCellY;
-        animateScale(1f, () -> delegateDrawing(cl, cellX, cellY), this::clearDrawingDelegate);
+        animateScale(/* isAccepting= */ false, mIsHovered);
     }
 
     public float getStrokeWidth() {
         return mStrokeWidth;
     }
+
+    protected void setHovered(boolean hovered) {
+        animateScale(mIsAccepting, /* isHovered= */ hovered);
+    }
 }
diff --git a/src/com/android/launcher3/graphics/DragPreviewProvider.java b/src/com/android/launcher3/graphics/DragPreviewProvider.java
index 7457f30..4208343 100644
--- a/src/com/android/launcher3/graphics/DragPreviewProvider.java
+++ b/src/com/android/launcher3/graphics/DragPreviewProvider.java
@@ -16,15 +16,19 @@
 
 package com.android.launcher3.graphics;
 
+import static com.android.launcher3.BubbleTextView.DISPLAY_SEARCH_RESULT_APP_ROW;
+
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
 import android.view.View;
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.R;
 import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.icons.BitmapRenderer;
@@ -37,7 +41,6 @@
  * A utility class to generate preview bitmap for dragging.
  */
 public class DragPreviewProvider {
-
     private final Rect mTempRect = new Rect();
 
     protected final View mView;
@@ -99,6 +102,15 @@
             height = mView.getHeight();
         }
 
+        if (mView instanceof BubbleTextView btv
+                && btv.getIconDisplay() == DISPLAY_SEARCH_RESULT_APP_ROW) {
+            FastBitmapDrawable icon = ((BubbleTextView) mView).getIcon();
+            Drawable drawable = icon.getConstantState().newDrawable();
+            float xInset = (float) blurSizeOutline / (float) (width + blurSizeOutline);
+            float yInset = (float) blurSizeOutline / (float) (height + blurSizeOutline);
+            return new InsetDrawable(drawable, xInset / 2, yInset / 2, xInset / 2, yInset / 2);
+        }
+
         return new FastBitmapDrawable(
                 BitmapRenderer.createHardwareBitmap(width + blurSizeOutline,
                         height + blurSizeOutline, (c) -> drawDragView(c, scale)));
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 47677ea..ae44f0a 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -34,13 +34,9 @@
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
 import android.content.ContextWrapper;
-import android.content.Intent;
 import android.content.res.TypedArray;
-import android.graphics.Color;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.ColorDrawable;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
@@ -49,12 +45,12 @@
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.view.ContextThemeWrapper;
+import android.view.Display;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.WindowInsets;
-import android.view.WindowManager;
+import android.widget.FrameLayout;
 import android.widget.TextClock;
 
 import androidx.annotation.NonNull;
@@ -74,12 +70,11 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.WorkspaceLayoutManager;
+import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
 import com.android.launcher3.celllayout.CellPosMapper;
 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.LauncherIcons;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
@@ -98,6 +93,7 @@
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
+import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.WindowManagerProxy;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
@@ -183,7 +179,6 @@
     private final DeviceProfile mDp;
     private final DeviceProfile mDpOrig;
     private final Rect mInsets;
-    private final WorkspaceItemInfo mWorkspaceItemInfo;
     private final LayoutInflater mHomeElementInflater;
     private final InsettableFrameLayout mRootView;
     private final Hotseat mHotseat;
@@ -211,29 +206,9 @@
         } else {
             mDpOrig = mDp;
         }
-
-        WindowInsets currentWindowInsets = context.getSystemService(WindowManager.class)
-                .getCurrentWindowMetrics().getWindowInsets();
-        mInsets = new Rect(
-                currentWindowInsets.getSystemWindowInsetLeft(),
-                currentWindowInsets.getSystemWindowInsetTop(),
-                currentWindowInsets.getSystemWindowInsetRight(),
-                mDp.isTaskbarPresent ? 0 : currentWindowInsets.getSystemWindowInsetBottom());
+        mInsets = getInsets(context);
         mDp.updateInsets(mInsets);
 
-        BaseIconFactory iconFactory =
-                new BaseIconFactory(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize) { };
-        BitmapInfo iconInfo = iconFactory.createBadgedIconBitmap(
-                new AdaptiveIconDrawable(
-                        new ColorDrawable(Color.WHITE),
-                        new ColorDrawable(Color.WHITE)));
-
-        mWorkspaceItemInfo = new WorkspaceItemInfo();
-        mWorkspaceItemInfo.bitmap = iconInfo;
-        mWorkspaceItemInfo.intent = new Intent();
-        mWorkspaceItemInfo.contentDescription = mWorkspaceItemInfo.title =
-                context.getString(R.string.label_application);
-
         mHomeElementInflater = LayoutInflater.from(
                 new ContextThemeWrapper(this, R.style.HomeScreenElementTheme));
         mHomeElementInflater.setFactory2(this);
@@ -283,6 +258,26 @@
         mAppWidgetHost = new LauncherPreviewAppWidgetHost(context);
     }
 
+    /**
+     * Returns the insets of the screen closest to the display given by the context
+     */
+    private Rect getInsets(Context context) {
+        DisplayController.Info info = DisplayController.INSTANCE.get(context).getInfo();
+        float maxDiff = Float.MAX_VALUE;
+        Display display = context.getDisplay();
+        Rect insets = new Rect();
+        for (WindowBounds supportedBound : info.supportedBounds) {
+            double diff = Math.pow(display.getWidth() - supportedBound.availableSize.x, 2)
+                    + Math.pow(display.getHeight() - supportedBound.availableSize.y, 2);
+            if (supportedBound.rotationHint == context.getDisplay().getRotation()
+                    && diff < maxDiff) {
+                maxDiff = (float) diff;
+                insets = supportedBound.insets;
+            }
+        }
+        return new Rect(insets);
+    }
+
     /** Populate preview and render it. */
     public View getRenderedView(BgDataModel dataModel,
             Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
@@ -378,12 +373,13 @@
         addInScreenFromBind(icon, info);
     }
 
-    private void inflateAndAddFolder(FolderInfo info) {
+    private void inflateAndAddCollectionIcon(FolderInfo info) {
         CellLayout screen = info.container == Favorites.CONTAINER_DESKTOP
                 ? mWorkspaceScreens.get(info.screenId)
                 : mHotseat;
-        FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, screen,
-                info);
+        FrameLayout folderIcon = info.itemType == Favorites.ITEM_TYPE_FOLDER
+                ? FolderIcon.inflateIcon(R.layout.folder_icon, this, screen, info)
+                : AppPairIcon.inflateIcon(R.layout.app_pair_icon, this, screen, info);
         addInScreenFromBind(folderIcon, info);
     }
 
@@ -483,12 +479,12 @@
         for (ItemInfo itemInfo : currentWorkspaceItems) {
             switch (itemInfo.itemType) {
                 case Favorites.ITEM_TYPE_APPLICATION:
-                case Favorites.ITEM_TYPE_SHORTCUT:
                 case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                     inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
                     break;
                 case Favorites.ITEM_TYPE_FOLDER:
-                    inflateAndAddFolder((FolderInfo) itemInfo);
+                case Favorites.ITEM_TYPE_APP_PAIR:
+                    inflateAndAddCollectionIcon((FolderInfo) itemInfo);
                     break;
                 default:
                     break;
diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index d366c4a..307052a 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -17,8 +17,8 @@
 
 package com.android.launcher3.graphics;
 
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V2;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V3;
 
@@ -155,19 +155,19 @@
         // Progress color
         float[] m3HCT = new float[3];
         ColorUtils.colorToM3HCT(primaryIconColor, m3HCT);
-        mProgressColor = ColorUtils.M3HCTtoColor(
+        mProgressColor = ColorUtils.M3HCTToColor(
                 m3HCT[0],
                 m3HCT[1],
                 isDarkMode ? Math.max(m3HCT[2], 55) : Math.min(m3HCT[2], 40));
 
         // Track color
-        mTrackColor = ColorUtils.M3HCTtoColor(
+        mTrackColor = ColorUtils.M3HCTToColor(
                 m3HCT[0],
                 16,
                 isDarkMode ? 30 : 90
         );
         // Plate color
-        mPlateColor = ColorUtils.M3HCTtoColor(
+        mPlateColor = ColorUtils.M3HCTToColor(
                 m3HCT[0],
                 isDarkMode ? 36 : 24,
                 isDarkMode ? (isThemed() ? 10 : 20) : 80
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index e89c0c5..aebcdd4 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.graphics;
 
+import static android.view.Display.DEFAULT_DISPLAY;
+
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -80,11 +82,12 @@
     private static final String KEY_DISPLAY_ID = "display_id";
     private static final String KEY_COLORS = "wallpaper_colors";
 
-    private final Context mContext;
-    private final InvariantDeviceProfile mIdp;
+    private Context mContext;
     private final IBinder mHostToken;
     private final int mWidth;
     private final int mHeight;
+    private String mGridName;
+
     private final Display mDisplay;
     private final WallpaperColors mWallpaperColors;
     private final RunnableList mOnDestroyCallbacks = new RunnableList();
@@ -97,15 +100,13 @@
 
     public PreviewSurfaceRenderer(Context context, Bundle bundle) throws Exception {
         mContext = context;
-
-        String gridName = bundle.getString("name");
+        mGridName = bundle.getString("name");
         bundle.remove("name");
-        if (gridName == null) {
-            gridName = InvariantDeviceProfile.getCurrentGridName(context);
+        if (mGridName == null) {
+            mGridName = InvariantDeviceProfile.getCurrentGridName(context);
         }
         mWallpaperColors = bundle.getParcelable(KEY_COLORS);
         mHideQsb = bundle.getBoolean(GridCustomizationsProvider.KEY_HIDE_BOTTOM_ROW);
-        mIdp = new InvariantDeviceProfile(context, gridName);
 
         mHostToken = bundle.getBinder(KEY_HOST_TOKEN);
         mWidth = bundle.getInt(KEY_VIEW_WIDTH);
@@ -113,9 +114,9 @@
         mDisplay = context.getSystemService(DisplayManager.class)
                 .getDisplay(bundle.getInt(KEY_DISPLAY_ID));
 
-        mSurfaceControlViewHost = MAIN_EXECUTOR
-                .submit(() -> new SurfaceControlViewHost(mContext, mDisplay, mHostToken))
-                .get(5, TimeUnit.SECONDS);
+        mSurfaceControlViewHost = MAIN_EXECUTOR.submit(() -> new SurfaceControlViewHost(mContext,
+                context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY),
+                mHostToken)).get(5, TimeUnit.SECONDS);
         mOnDestroyCallbacks.add(mSurfaceControlViewHost::release);
     }
 
@@ -195,28 +196,33 @@
         }
     }
 
+    /***
+     * Generates a new context overriding the theme color and the display size without affecting the
+     * main application context
+     */
+    private Context getPreviewContext() {
+        Context context = mContext.createDisplayContext(mDisplay);
+        if (mWallpaperColors == null) {
+            return new ContextThemeWrapper(context,
+                    Themes.getActivityThemeRes(context));
+        }
+        if (Utilities.ATLEAST_R) {
+            context = context.createWindowContext(
+                    LayoutParams.TYPE_APPLICATION_OVERLAY, null);
+        }
+        LocalColorExtractor.newInstance(context)
+                .applyColorsOverride(context, mWallpaperColors);
+        return new ContextThemeWrapper(context,
+                Themes.getActivityThemeRes(context, mWallpaperColors.getColorHints()));
+    }
+
     @WorkerThread
     private void loadModelData() {
-        final Context inflationContext;
-        if (mWallpaperColors != null) {
-            // Create a themed context, without affecting the main application context
-            Context context = mContext.createDisplayContext(mDisplay);
-            if (Utilities.ATLEAST_R) {
-                context = context.createWindowContext(
-                        LayoutParams.TYPE_APPLICATION_OVERLAY, null);
-            }
-            LocalColorExtractor.newInstance(mContext)
-                    .applyColorsOverride(context, mWallpaperColors);
-            inflationContext = new ContextThemeWrapper(context,
-                    Themes.getActivityThemeRes(context, mWallpaperColors.getColorHints()));
-        } else {
-            inflationContext = new ContextThemeWrapper(mContext,
-                    Themes.getActivityThemeRes(mContext));
-        }
-
-        if (GridSizeMigrationUtil.needsToMigrate(inflationContext, mIdp)) {
+        final Context inflationContext = getPreviewContext();
+        final InvariantDeviceProfile idp = new InvariantDeviceProfile(inflationContext, mGridName);
+        if (GridSizeMigrationUtil.needsToMigrate(inflationContext, idp)) {
             // Start the migration
-            PreviewContext previewContext = new PreviewContext(inflationContext, mIdp);
+            PreviewContext previewContext = new PreviewContext(inflationContext, idp);
             // Copy existing data to preview DB
             LauncherDbUtils.copyTable(LauncherAppState.getInstance(mContext)
                     .getModel().getModelDbController().getDb(),
@@ -239,7 +245,7 @@
 
                 @Override
                 public void run() {
-                    DeviceProfile deviceProfile = mIdp.getDeviceProfile(previewContext);
+                    DeviceProfile deviceProfile = idp.getDeviceProfile(previewContext);
                     String query =
                             LauncherSettings.Favorites.SCREEN + " = " + Workspace.FIRST_SCREEN_ID
                                     + " or " + LauncherSettings.Favorites.CONTAINER + " = "
@@ -254,7 +260,8 @@
                             getLoadedLauncherWidgetInfo(previewContext.getBaseContext());
 
                     MAIN_EXECUTOR.execute(() -> {
-                        renderView(previewContext, mBgDataModel, mWidgetProvidersMap, spanInfo);
+                        renderView(previewContext, mBgDataModel, mWidgetProvidersMap, spanInfo,
+                                idp);
                         mOnDestroyCallbacks.add(previewContext::onDestroy);
                     });
                 }
@@ -263,7 +270,7 @@
             LauncherAppState.getInstance(inflationContext).getModel().loadAsync(dataModel -> {
                 if (dataModel != null) {
                     MAIN_EXECUTOR.execute(() -> renderView(inflationContext, dataModel, null,
-                            null));
+                            null, idp));
                 } else {
                     Log.e(TAG, "Model loading failed");
                 }
@@ -274,11 +281,11 @@
     @UiThread
     private void renderView(Context inflationContext, BgDataModel dataModel,
             Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap,
-            @Nullable final SparseArray<Size> launcherWidgetSpanInfo) {
+            @Nullable final SparseArray<Size> launcherWidgetSpanInfo, InvariantDeviceProfile idp) {
         if (mDestroyed) {
             return;
         }
-        mRenderer = new LauncherPreviewRenderer(inflationContext, mIdp,
+        mRenderer = new LauncherPreviewRenderer(inflationContext, idp,
                 mWallpaperColors, launcherWidgetSpanInfo);
         mRenderer.hideBottomRow(mHideQsb);
         View view = mRenderer.getRenderedView(dataModel, widgetProviderInfoMap);
diff --git a/src/com/android/launcher3/graphics/SysUiScrim.java b/src/com/android/launcher3/graphics/SysUiScrim.java
index 21ebc98..66001d8 100644
--- a/src/com/android/launcher3/graphics/SysUiScrim.java
+++ b/src/com/android/launcher3/graphics/SysUiScrim.java
@@ -13,29 +13,31 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.android.launcher3.graphics;
 
+import static android.graphics.Paint.DITHER_FLAG;
+import static android.graphics.Paint.FILTER_BITMAP_FLAG;
+
 import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION;
-import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
 
 import android.animation.ObjectAnimator;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
-import android.graphics.Color;
 import android.graphics.LinearGradient;
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.Shader;
-import android.graphics.drawable.Drawable;
 import android.util.DisplayMetrics;
-import android.util.FloatProperty;
 import android.view.View;
 
+import androidx.annotation.ColorInt;
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.util.ScreenOnTracker;
 import com.android.launcher3.util.ScreenOnTracker.ScreenOnListener;
@@ -46,33 +48,6 @@
  */
 public class SysUiScrim implements View.OnAttachStateChangeListener {
 
-    public static final FloatProperty<SysUiScrim> SYSUI_PROGRESS =
-            new FloatProperty<SysUiScrim>("sysUiProgress") {
-                @Override
-                public Float get(SysUiScrim scrim) {
-                    return scrim.mSysUiProgress;
-                }
-
-                @Override
-                public void setValue(SysUiScrim scrim, float value) {
-                    scrim.setSysUiProgress(value);
-                }
-            };
-
-    private static final FloatProperty<SysUiScrim> SYSUI_ANIM_MULTIPLIER =
-            new FloatProperty<SysUiScrim>("sysUiAnimMultiplier") {
-                @Override
-                public Float get(SysUiScrim scrim) {
-                    return scrim.mSysUiAnimMultiplier;
-                }
-
-                @Override
-                public void setValue(SysUiScrim scrim, float value) {
-                    scrim.mSysUiAnimMultiplier = value;
-                    scrim.reapplySysUiAlpha();
-                }
-            };
-
     /**
      * Receiver used to get a signal that the user unlocked their device.
      */
@@ -92,44 +67,53 @@
         }
     };
 
-    private static final int MAX_HOTSEAT_SCRIM_ALPHA = 100;
-    private static final int ALPHA_MASK_HEIGHT_DP = 500;
-    private static final int ALPHA_MASK_BITMAP_DP = 200;
-    private static final int ALPHA_MASK_WIDTH_DP = 2;
+    private static final int MAX_SYSUI_SCRIM_ALPHA = 255;
+    private static final int ALPHA_MASK_BITMAP_WIDTH_DP = 2;
+
+    private static final int BOTTOM_MASK_HEIGHT_DP = 200;
+    private static final int TOP_MASK_HEIGHT_DP = 100;
 
     private boolean mDrawTopScrim, mDrawBottomScrim;
 
-    private final RectF mFinalMaskRect = new RectF();
-    private final Paint mBottomMaskPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
-    private final Bitmap mBottomMask;
-    private final int mMaskHeight;
+    private final RectF mTopMaskRect = new RectF();
+    private final Paint mTopMaskPaint = new Paint(FILTER_BITMAP_FLAG | DITHER_FLAG);
+    private final Bitmap mTopMaskBitmap;
+    private final int mTopMaskHeight;
+
+    private final RectF mBottomMaskRect = new RectF();
+    private final Paint mBottomMaskPaint = new Paint(FILTER_BITMAP_FLAG | DITHER_FLAG);
+    private final Bitmap mBottomMaskBitmap;
+    private final int mBottomMaskHeight;
 
     private final View mRoot;
     private final BaseDraggingActivity mActivity;
-    private final Drawable mTopScrim;
-
-    private float mSysUiProgress = 1;
-    private boolean mHideSysUiScrim;
+    private final boolean mHideSysUiScrim;
+    private boolean mSkipScrimAnimationForTest = false;
 
     private boolean mAnimateScrimOnNextDraw = false;
-    private float mSysUiAnimMultiplier = 1;
+    private final AnimatedFloat mSysUiAnimMultiplier = new AnimatedFloat(this::reapplySysUiAlpha);
+    private final AnimatedFloat mSysUiProgress = new AnimatedFloat(this::reapplySysUiAlpha);
 
     public SysUiScrim(View view) {
         mRoot = view;
         mActivity = BaseDraggingActivity.fromContext(view.getContext());
-        mMaskHeight = ResourceUtils.pxFromDp(ALPHA_MASK_BITMAP_DP,
-                view.getResources().getDisplayMetrics());
-        mTopScrim = Themes.getAttrDrawable(view.getContext(), R.attr.workspaceStatusBarScrim);
-        if (mTopScrim != null) {
-            mTopScrim.setDither(true);
-            mBottomMask = createDitheredAlphaMask();
-            mHideSysUiScrim = false;
-        } else {
-            mBottomMask = null;
-            mHideSysUiScrim = true;
-        }
+        DisplayMetrics dm = mActivity.getResources().getDisplayMetrics();
 
-        view.addOnAttachStateChangeListener(this);
+        mTopMaskHeight = ResourceUtils.pxFromDp(TOP_MASK_HEIGHT_DP, dm);
+        mBottomMaskHeight = ResourceUtils.pxFromDp(BOTTOM_MASK_HEIGHT_DP, dm);
+        mHideSysUiScrim = Themes.getAttrBoolean(view.getContext(), R.attr.isWorkspaceDarkText);
+
+        mTopMaskBitmap = mHideSysUiScrim ? null : createDitheredAlphaMask(mTopMaskHeight,
+                new int[]{0x50FFFFFF, 0x0AFFFFFF, 0x00FFFFFF},
+                new float[]{0f, 0.7f, 1f});
+        mTopMaskPaint.setColor(0xFF222222);
+        mBottomMaskBitmap = mHideSysUiScrim ? null : createDitheredAlphaMask(mBottomMaskHeight,
+                new int[]{0x00FFFFFF, 0x2FFFFFFF},
+                new float[]{0f, 1f});
+
+        if (!KEYGUARD_ANIMATION.get() && !mHideSysUiScrim) {
+            view.addOnAttachStateChangeListener(this);
+        }
     }
 
     /**
@@ -137,16 +121,16 @@
      */
     public void draw(Canvas canvas) {
         if (!mHideSysUiScrim) {
-            if (mSysUiProgress <= 0) {
+            if (mSysUiProgress.value <= 0) {
                 mAnimateScrimOnNextDraw = false;
                 return;
             }
 
             if (mAnimateScrimOnNextDraw) {
-                mSysUiAnimMultiplier = 0;
+                mSysUiAnimMultiplier.value = 0;
                 reapplySysUiAlphaNoInvalidate();
 
-                ObjectAnimator oa = createSysuiMultiplierAnim(1);
+                ObjectAnimator oa = mSysUiAnimMultiplier.animateToValue(1);
                 oa.setDuration(600);
                 oa.setStartDelay(mActivity.getWindow().getTransitionBackgroundFadeDuration());
                 oa.start();
@@ -154,21 +138,26 @@
             }
 
             if (mDrawTopScrim) {
-                mTopScrim.draw(canvas);
+                canvas.drawBitmap(mTopMaskBitmap, null, mTopMaskRect, mTopMaskPaint);
             }
             if (mDrawBottomScrim) {
-                canvas.drawBitmap(mBottomMask, null, mFinalMaskRect, mBottomMaskPaint);
+                canvas.drawBitmap(mBottomMaskBitmap, null, mBottomMaskRect, mBottomMaskPaint);
             }
         }
     }
 
     /**
-     * @return an ObjectAnimator that controls the fade in/out of the sys ui scrim.
+     * Returns the sysui multiplier property for controlling fade in/out of the scrim
      */
-    public ObjectAnimator createSysuiMultiplierAnim(float... values) {
-        ObjectAnimator anim = ObjectAnimator.ofFloat(this, SYSUI_ANIM_MULTIPLIER, values);
-        anim.setAutoCancel(true);
-        return anim;
+    public AnimatedFloat getSysUIMultiplier() {
+        return mSysUiAnimMultiplier;
+    }
+
+    /**
+     * Returns the sysui progress property for controlling fade in/out of the scrim
+     */
+    public AnimatedFloat getSysUIProgress() {
+        return mSysUiProgress;
     }
 
     /**
@@ -180,44 +169,35 @@
      */
     public void onInsetsChanged(Rect insets) {
         DeviceProfile dp = mActivity.getDeviceProfile();
-        mDrawTopScrim = mTopScrim != null && insets.top > 0;
-        mDrawBottomScrim = mBottomMask != null
-                && !dp.isVerticalBarLayout()
-                && !dp.isGestureMode
-                && !dp.isTaskbarPresent;
+        mDrawTopScrim = insets.top > 0;
+        mDrawBottomScrim = !dp.isVerticalBarLayout() && !dp.isGestureMode && !dp.isTaskbarPresent;
     }
 
     @Override
     public void onViewAttachedToWindow(View view) {
-        if (!KEYGUARD_ANIMATION.get() && mTopScrim != null) {
-            ScreenOnTracker.INSTANCE.get(mActivity).addListener(mScreenOnListener);
-        }
+        ScreenOnTracker.INSTANCE.get(mActivity).addListener(mScreenOnListener);
     }
 
     @Override
     public void onViewDetachedFromWindow(View view) {
-        if (!KEYGUARD_ANIMATION.get() && mTopScrim != null) {
-            ScreenOnTracker.INSTANCE.get(mActivity).removeListener(mScreenOnListener);
-        }
+        ScreenOnTracker.INSTANCE.get(mActivity).removeListener(mScreenOnListener);
     }
 
     /**
      * Set the width and height of the view being scrimmed
-     * @param w
-     * @param h
      */
     public void setSize(int w, int h) {
-        if (mTopScrim != null) {
-            mTopScrim.setBounds(0, 0, w, h);
-            mFinalMaskRect.set(0, h - mMaskHeight, w, h);
-        }
+        mTopMaskRect.set(0, 0, w, mTopMaskHeight);
+        mBottomMaskRect.set(0, h - mBottomMaskHeight, w, h);
     }
 
-    private void setSysUiProgress(float progress) {
-        if (progress != mSysUiProgress) {
-            mSysUiProgress = progress;
-            reapplySysUiAlpha();
-        }
+    /**
+     * Sets whether the SysUiScrim should hide for testing.
+     */
+    @VisibleForTesting
+    public void skipScrimAnimation() {
+        mSkipScrimAnimationForTest = true;
+        reapplySysUiAlpha();
     }
 
     private void reapplySysUiAlpha() {
@@ -228,29 +208,22 @@
     }
 
     private void reapplySysUiAlphaNoInvalidate() {
-        float factor = mSysUiProgress * mSysUiAnimMultiplier;
-        mBottomMaskPaint.setAlpha(Math.round(MAX_HOTSEAT_SCRIM_ALPHA * factor));
-        if (mTopScrim != null) {
-            mTopScrim.setAlpha(Math.round(255 * factor));
-        }
+        float factor = mSysUiProgress.value * mSysUiAnimMultiplier.value;
+        if (mSkipScrimAnimationForTest) factor = 1f;
+        mBottomMaskPaint.setAlpha(Math.round(MAX_SYSUI_SCRIM_ALPHA * factor));
+        mTopMaskPaint.setAlpha(Math.round(MAX_SYSUI_SCRIM_ALPHA * factor));
     }
 
-    private Bitmap createDitheredAlphaMask() {
+    private Bitmap createDitheredAlphaMask(int height, @ColorInt int[] colors, float[] positions) {
         DisplayMetrics dm = mActivity.getResources().getDisplayMetrics();
-        int width = ResourceUtils.pxFromDp(ALPHA_MASK_WIDTH_DP, dm);
-        int gradientHeight = ResourceUtils.pxFromDp(ALPHA_MASK_HEIGHT_DP, dm);
-        Bitmap dst = Bitmap.createBitmap(width, mMaskHeight, Bitmap.Config.ALPHA_8);
+        int width = ResourceUtils.pxFromDp(ALPHA_MASK_BITMAP_WIDTH_DP, dm);
+        Bitmap dst = Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8);
         Canvas c = new Canvas(dst);
-        Paint paint = new Paint(Paint.DITHER_FLAG);
-        LinearGradient lg = new LinearGradient(0, 0, 0, gradientHeight,
-                new int[]{
-                        0x00FFFFFF,
-                        setColorAlphaBound(Color.WHITE, (int) (0xFF * 0.95)),
-                        0xFFFFFFFF},
-                new float[]{0f, 0.8f, 1f},
-                Shader.TileMode.CLAMP);
+        Paint paint = new Paint(DITHER_FLAG);
+        LinearGradient lg = new LinearGradient(0, 0, 0, height,
+                colors, positions, Shader.TileMode.CLAMP);
         paint.setShader(lg);
-        c.drawRect(0, 0, width, gradientHeight, paint);
+        c.drawPaint(paint);
         return dst;
     }
 }
diff --git a/src/com/android/launcher3/logging/KeyboardStateManager.java b/src/com/android/launcher3/logging/KeyboardStateManager.java
index 6dc0a0b..d0f9c74 100644
--- a/src/com/android/launcher3/logging/KeyboardStateManager.java
+++ b/src/com/android/launcher3/logging/KeyboardStateManager.java
@@ -24,7 +24,10 @@
  */
 public class KeyboardStateManager {
     private long mUpdatedTime;
-    private int mImeHeight;
+    private int mImeHeightPx;
+    // Height of the keyboard when it's shown.
+    // mImeShownHeightPx>=mImeHeightPx always.
+    private int mImeShownHeightPx;
 
     public enum KeyboardState {
         NO_IME_ACTION,
@@ -34,8 +37,9 @@
 
     private KeyboardState mKeyboardState;
 
-    public KeyboardStateManager() {
+    public KeyboardStateManager(int defaultImeShownHeightPx) {
         mKeyboardState = NO_IME_ACTION;
+        mImeShownHeightPx = defaultImeShownHeightPx;
     }
 
     /**
@@ -64,13 +68,25 @@
      * Returns keyboard's current height.
      */
     public int getImeHeight() {
-        return mImeHeight;
+        return mImeHeightPx;
     }
 
     /**
-     * Setter method to set keyboard height.
+     * Returns keyboard's height in pixels when shown.
      */
-    public void setImeHeight(int imeHeight) {
-        mImeHeight = imeHeight;
+    public int getImeShownHeight() {
+        return mImeShownHeightPx;
+    }
+
+    /**
+     * Setter method to set keyboard height in pixels.
+     */
+    public void setImeHeight(int imeHeightPx) {
+        mImeHeightPx = imeHeightPx;
+        if (mImeHeightPx > 0) {
+            // Update the mImeShownHeightPx with the actual ime height when shown and store it
+            // for future sessions.
+            mImeShownHeightPx = mImeHeightPx;
+        }
     }
 }
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 66ed779..265378c 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -32,12 +32,9 @@
 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.util.IntArray;
 import com.android.launcher3.util.ResourceBasedOverride;
 import com.android.launcher3.views.ActivityContext;
 
-import java.util.List;
-
 /**
  * Handles the user event logging in R+.
  *
@@ -59,6 +56,7 @@
     private InstanceId mInstanceId;
 
     protected @Nullable ActivityContext mActivityContext = null;
+    protected @Nullable Context mContext = null;
     private KeyboardStateManager mKeyboardStateManager;
 
     /**
@@ -623,6 +621,12 @@
         @UiEvent(doc = "User has invoked split to left half with a keyboard shortcut.")
         LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP(1233),
 
+        @UiEvent(doc = "User has invoked split to right half from desktop mode.")
+        LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM(1412),
+
+        @UiEvent(doc = "User has invoked split to left half from desktop mode.")
+        LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP(1464),
+
         @UiEvent(doc = "User has collapsed the work FAB button by scrolling down in the all apps"
                 + " work A-Z list.")
         LAUNCHER_WORK_FAB_BUTTON_COLLAPSE(1276),
@@ -643,11 +647,17 @@
         @UiEvent(doc = "User has swiped upwards from the gesture handle to show transient taskbar.")
         LAUNCHER_TRANSIENT_TASKBAR_SHOW(1331),
 
+        @UiEvent(doc = "User has clicked an app pair and launched directly into split screen.")
+        LAUNCHER_APP_PAIR_LAUNCH(1374),
+
+        @UiEvent(doc = "User saved an app pair.")
+        LAUNCHER_APP_PAIR_SAVE(1456),
+
         @UiEvent(doc = "App launched through pending intent")
-        LAUNCHER_APP_LAUNCH_PENDING_INTENT(1394),
-        ;
+        LAUNCHER_APP_LAUNCH_PENDING_INTENT(1394)
 
         // ADD MORE
+        ;
 
         private final int mId;
 
@@ -831,6 +841,10 @@
      */
     public interface StatsLatencyLogger {
 
+        /**
+         * Should be in sync with:
+         * google3/wireless/android/sysui/aster/asterstats/launcher_event_processed.proto
+         */
         enum LatencyType {
             UNKNOWN(0),
             // example: launcher restart that happens via daily backup and restore
@@ -956,33 +970,32 @@
         }
 
         /**
-         * Sets list of {@link com.android.app.search.ResultType} for the impression event.
+         * Sets {@link com.android.app.search.ResultType} for the impression event.
          */
-        default StatsImpressionLogger withResultType(IntArray resultType) {
+        default StatsImpressionLogger withResultType(int resultType) {
             return this;
         }
 
         /**
-         * Sets list of count for each of {@link com.android.app.search.ResultType} for the
-         * impression event.
-         */
-        default StatsImpressionLogger withResultCount(IntArray resultCount) {
-            return this;
-        }
-
-        /**
-         * Sets list of boolean for each of {@link com.android.app.search.ResultType} that indicates
+         * Sets boolean for each of {@link com.android.app.search.ResultType} that indicates
          * if this result is above keyboard or not for the impression event.
          */
-        default StatsImpressionLogger withAboveKeyboard(List<Boolean> aboveKeyboard) {
+        default StatsImpressionLogger withAboveKeyboard(boolean aboveKeyboard) {
             return this;
         }
 
         /**
-         * Sets list of uid for each of {@link com.android.app.search.ResultType} that indicates
+         * Sets uid for each of {@link com.android.app.search.ResultType} that indicates
          * package name for the impression event.
          */
-        default StatsImpressionLogger withUids(IntArray uid) {
+        default StatsImpressionLogger withUid(int uid) {
+            return this;
+        }
+
+        /**
+         * Sets result source that indicates the origin of the result for the impression event.
+         */
+        default StatsImpressionLogger withResultSource(int resultSource) {
             return this;
         }
 
@@ -1031,7 +1044,9 @@
      */
     public KeyboardStateManager keyboardStateManager() {
         if (mKeyboardStateManager == null) {
-            mKeyboardStateManager = new KeyboardStateManager();
+            mKeyboardStateManager = new KeyboardStateManager(
+                    mContext != null ? mContext.getResources().getDimensionPixelSize(
+                            R.dimen.default_ime_height) : 0);
         }
         return mKeyboardStateManager;
     }
@@ -1067,6 +1082,7 @@
         StatsLogManager manager = Overrides.getObject(StatsLogManager.class,
                 context.getApplicationContext(), R.string.stats_log_manager_class);
         manager.mActivityContext = ActivityContext.lookupContextNoThrow(context);
+        manager.mContext = context;
         return manager;
     }
 }
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index 27d1f78..5e86bd6 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -91,8 +91,7 @@
             List<ItemInfo> filteredItems = new ArrayList<>();
             for (Pair<ItemInfo, Object> entry : mItemList) {
                 ItemInfo item = entry.first;
-                if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
-                        item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
+                if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
                     // Short-circuit this logic if the icon exists somewhere on the workspace
                     if (shortcutExists(dataModel, item.getIntent(), item.user)) {
                         continue;
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index 5997637..dbb29b8 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.os.Process;
+import android.os.Trace;
 import android.util.Log;
 
 import com.android.launcher3.InvariantDeviceProfile;
@@ -83,13 +84,18 @@
      * Binds all loaded data to actual views on the main thread.
      */
     public void bindWorkspace(boolean incrementBindId, boolean isBindSync) {
-        if (FeatureFlags.ENABLE_WORKSPACE_LOADING_OPTIMIZATION.get()) {
-            DisjointWorkspaceBinder workspaceBinder =
+        Trace.beginSection("BaseLauncherBinder#bindWorkspace");
+        try {
+            if (FeatureFlags.ENABLE_WORKSPACE_LOADING_OPTIMIZATION.get()) {
+                DisjointWorkspaceBinder workspaceBinder =
                     initWorkspaceBinder(incrementBindId, mBgDataModel.collectWorkspaceScreens());
-            workspaceBinder.bindCurrentWorkspacePages(isBindSync);
-            workspaceBinder.bindOtherWorkspacePages();
-        } else {
-            bindWorkspaceAllAtOnce(incrementBindId, isBindSync);
+                workspaceBinder.bindCurrentWorkspacePages(isBindSync);
+                workspaceBinder.bindOtherWorkspacePages();
+            } else {
+                bindWorkspaceAllAtOnce(incrementBindId, isBindSync);
+            }
+        } finally {
+            Trace.endSection();
         }
     }
 
@@ -106,6 +112,7 @@
         synchronized (mBgDataModel) {
             if (incrementBindId) {
                 mBgDataModel.lastBindId++;
+                mBgDataModel.lastLoadId = mApp.getModel().getLastLoadId();
             }
             mMyBindingId = mBgDataModel.lastBindId;
             return new DisjointWorkspaceBinder(workspacePages);
@@ -126,6 +133,7 @@
             mBgDataModel.extraItems.forEach(extraItems::add);
             if (incrementBindId) {
                 mBgDataModel.lastBindId++;
+                mBgDataModel.lastLoadId = mApp.getModel().getLastLoadId();
             }
             mMyBindingId = mBgDataModel.lastBindId;
             workspaceItemCount = mBgDataModel.itemsIdMap.size();
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index 866e222..97f540e 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -15,9 +15,6 @@
  */
 package com.android.launcher3.model;
 
-import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
-import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
-
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -33,7 +30,6 @@
 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.testing.shared.TestProtocol;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
@@ -77,7 +73,6 @@
     @Override
     public final void run() {
         boolean isModelLoaded = Objects.requireNonNull(mModel).isModelLoaded();
-        testLogD(WORK_TAB_MISSING, "modelLoaded: " + isModelLoaded + " forTask: " + this);
         if (!isModelLoaded) {
             if (DEBUG_TASKS) {
                 Log.d(TAG, "Ignoring model task since loader is pending=" + this);
@@ -115,10 +110,6 @@
         List<WorkspaceItemInfo> workspaceUpdates = allUpdates.stream()
                 .filter(info -> info.id != ItemInfo.NO_ID)
                 .collect(Collectors.toList());
-        if (TestProtocol.sDebugTracing) {
-            Log.d(WORK_TAB_MISSING, "allUpdates: " + allUpdates.size() + ", workspaceUpdates "
-                    + workspaceUpdates.size());
-        }
         if (!workspaceUpdates.isEmpty()) {
             scheduleCallbackTask(c -> c.bindWorkspaceItemsChanged(workspaceUpdates));
         }
@@ -157,12 +148,7 @@
     }
 
     public void bindApplicationsIfNeeded() {
-        boolean changeFlag = mAllAppsList.getAndResetChangeFlag();
-        if (TestProtocol.sDebugTracing) {
-            Log.d(WORK_TAB_MISSING, "bindApplicationsIfNeeded changeFlag? " +
-                    changeFlag);
-        }
-        if (changeFlag) {
+        if (mAllAppsList.getAndResetChangeFlag()) {
             AppInfo[] apps = mAllAppsList.copyData();
             int flags = mAllAppsList.getFlags();
             Map<PackageUserKey, Integer> packageUserKeytoUidMap = Arrays.stream(apps).collect(
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index d94df51..7bcd038 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -126,6 +126,11 @@
     public int lastBindId = 0;
 
     /**
+     * Load id for which the callbacks were successfully bound
+     */
+    public int lastLoadId = -1;
+
+    /**
      * Clears all the data
      */
     public synchronized void clear() {
@@ -211,7 +216,6 @@
                     // Fall through.
                 }
                 case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
-                case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                     workspaceItems.remove(item);
                     break;
                 case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
@@ -246,7 +250,6 @@
                 break;
             case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
             case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
-            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                 if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
                         item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
                     workspaceItems.add(item);
diff --git a/src/com/android/launcher3/model/DeviceGridState.java b/src/com/android/launcher3/model/DeviceGridState.java
index edc8c1b..f24d1d2 100644
--- a/src/com/android/launcher3/model/DeviceGridState.java
+++ b/src/com/android/launcher3/model/DeviceGridState.java
@@ -33,6 +33,7 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
 
 import java.util.Locale;
 import java.util.Objects;
@@ -92,6 +93,9 @@
      * Stores the device state to shared preferences
      */
     public void writeToPrefs(Context context) {
+        if (context instanceof SandboxContext) {
+            return;
+        }
         LauncherPrefs.get(context).put(
                 WORKSPACE_SIZE.to(mGridSizeString),
                 HOTSEAT_COUNT.to(mNumHotseat),
diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
index 9a6cde6..c233872 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
@@ -45,7 +45,6 @@
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
@@ -133,10 +132,8 @@
             Log.v(TAG, "Workspace migration completed in "
                     + (System.currentTimeMillis() - migrationStartTime));
 
-            if (!(context instanceof SandboxContext)) {
-                // Save current configuration, so that the migration does not run again.
-                destDeviceState.writeToPrefs(context);
-            }
+            // Save current configuration, so that the migration does not run again.
+            destDeviceState.writeToPrefs(context);
         }
     }
 
@@ -455,7 +452,6 @@
                 try {
                     // calculate weight
                     switch (entry.itemType) {
-                        case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                         case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                         case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
                             entry.mIntent = c.getString(indexIntent);
@@ -531,7 +527,6 @@
                 try {
                     // calculate weight
                     switch (entry.itemType) {
-                        case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                         case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                         case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
                             entry.mIntent = c.getString(indexIntent);
diff --git a/src/com/android/launcher3/model/ItemInstallQueue.java b/src/com/android/launcher3/model/ItemInstallQueue.java
index fa0511c..9a3abd4 100644
--- a/src/com/android/launcher3/model/ItemInstallQueue.java
+++ b/src/com/android/launcher3/model/ItemInstallQueue.java
@@ -286,7 +286,6 @@
 
                     final WorkspaceItemInfo si = new WorkspaceItemInfo();
                     si.user = user;
-                    si.itemType = ITEM_TYPE_APPLICATION;
 
                     LauncherActivityInfo lai;
                     boolean usePackageIcon = laiList.isEmpty();
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 2054d93..33332f0 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -193,9 +193,7 @@
 
     public IconRequestInfo<WorkspaceItemInfo> createIconRequestInfo(
             WorkspaceItemInfo wai, boolean useLowResIcon) {
-        byte[] iconBlob = itemType == Favorites.ITEM_TYPE_SHORTCUT
-                || itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT
-                || restoreFlag != 0
+        byte[] iconBlob = itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT || restoreFlag != 0
                 ? getIconBlob() : null;
 
         return new IconRequestInfo<>(wai, mActivityInfo, iconBlob, useLowResIcon);
@@ -347,7 +345,6 @@
         }
 
         final WorkspaceItemInfo info = new WorkspaceItemInfo();
-        info.itemType = Favorites.ITEM_TYPE_APPLICATION;
         info.user = user;
         info.intent = newIntent;
 
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index ca356b0..0e68db2 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -24,8 +24,6 @@
 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.testing.shared.TestProtocol.WORK_TAB_MISSING;
-import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 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;
@@ -33,7 +31,6 @@
 import android.annotation.SuppressLint;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -61,7 +58,6 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.LauncherSettings.Settings;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.Folder;
@@ -203,7 +199,7 @@
             }
         }
 
-        Object traceToken = TraceHelper.INSTANCE.beginSection(TAG);
+        TraceHelper.INSTANCE.beginSection(TAG);
         LoaderMemoryLogger memoryLogger = new LoaderMemoryLogger();
         try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
             List<ShortcutInfo> allShortcuts = new ArrayList<>();
@@ -327,7 +323,7 @@
             memoryLogger.printLogs();
             throw e;
         }
-        TraceHelper.INSTANCE.endSection(traceToken);
+        TraceHelper.INSTANCE.endSection();
     }
 
     public synchronized void stopLocked() {
@@ -361,25 +357,15 @@
             String selection,
             @Nullable LoaderMemoryLogger memoryLogger) {
         final Context context = mApp.getContext();
-        final ContentResolver contentResolver = context.getContentResolver();
         final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
         final boolean isSafeMode = pmHelper.isSafeMode();
         final boolean isSdCardReady = Utilities.isBootCompleted();
         final WidgetManagerHelper widgetHelper = new WidgetManagerHelper(context);
 
-        boolean clearDb = false;
-        if (!mApp.getModel().getModelDbController().migrateGridIfNeeded()) {
-            // Migration failed. Clear workspace.
-            clearDb = true;
-        }
-
-        if (clearDb) {
-            Log.d(TAG, "loadWorkspace: resetting launcher database");
-            Settings.call(contentResolver, Settings.METHOD_CREATE_EMPTY_DB);
-        }
-
+        ModelDbController dbController = mApp.getModel().getModelDbController();
+        dbController.tryMigrateDB();
         Log.d(TAG, "loadWorkspace: loading default favorites");
-        Settings.call(contentResolver, Settings.METHOD_LOAD_DEFAULT_FAVORITES);
+        dbController.loadDefaultFavoritesIfNecessary();
 
         synchronized (mBgDataModel) {
             mBgDataModel.clear();
@@ -388,17 +374,18 @@
             final HashMap<PackageUserKey, SessionInfo> installingPkgs =
                     mSessionHelper.getActiveSessions();
             installingPkgs.forEach(mApp.getIconCache()::updateSessionCache);
+            FileLog.d(TAG, "loadWorkspace: Packages with active install sessions: "
+                    + installingPkgs.values());
 
             final PackageUserKey tempPackageKey = new PackageUserKey(null, null);
             mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs);
 
             mShortcutKeyToPinnedShortcuts = new HashMap<>();
-            ModelDbController dbController = mApp.getModel().getModelDbController();
             final LoaderCursor c = new LoaderCursor(
                     dbController.query(TABLE_NAME, null, selection, null, null),
                     mApp, mUserManagerState);
             final Bundle extras = c.getExtras();
-            mDbName = extras == null ? null : extras.getString(Settings.EXTRA_DB_NAME);
+            mDbName = extras == null ? null : extras.getString(ModelDbController.EXTRA_DB_NAME);
             try {
                 final LongSparseArray<Boolean> unlockedUsers = new LongSparseArray<>();
 
@@ -505,7 +492,6 @@
 
             boolean allowMissingTarget = false;
             switch (c.itemType) {
-                case Favorites.ITEM_TYPE_SHORTCUT:
                 case Favorites.ITEM_TYPE_APPLICATION:
                 case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                     Intent intent = c.parseIntent();
@@ -519,9 +505,8 @@
                     ComponentName cn = intent.getComponent();
                     String targetPkg = cn == null ? intent.getPackage() : cn.getPackageName();
 
-                    if (TextUtils.isEmpty(targetPkg)
-                            && c.itemType != Favorites.ITEM_TYPE_SHORTCUT) {
-                        c.markDeleted("Only legacy shortcuts can have null package");
+                    if (TextUtils.isEmpty(targetPkg)) {
+                        c.markDeleted("Shortcuts can't have null package");
                         return;
                     }
 
@@ -707,9 +692,11 @@
                     break;
 
                 case Favorites.ITEM_TYPE_FOLDER:
+                case Favorites.ITEM_TYPE_APP_PAIR:
                     FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id);
                     c.applyCommonProperties(folderInfo);
 
+                    folderInfo.itemType = c.itemType;
                     // Do not trim the folder label, as is was set by the user.
                     folderInfo.title = c.getString(c.mTitleIndex);
                     folderInfo.spanX = 1;
@@ -917,9 +904,7 @@
     private void sanitizeFolders(boolean itemsDeleted) {
         if (itemsDeleted) {
             // Remove any empty folder
-            int[] deletedFolderIds = Settings.call(mApp.getContext().getContentResolver(),
-                            Settings.METHOD_DELETE_EMPTY_FOLDERS)
-                    .getIntArray(Settings.EXTRA_VALUE);
+            IntArray deletedFolderIds = mApp.getModel().getModelDbController().deleteEmptyFolders();
             synchronized (mBgDataModel) {
                 for (int folderId : deletedFolderIds) {
                     mBgDataModel.workspaceItems.remove(mBgDataModel.folders.get(folderId));
@@ -932,11 +917,9 @@
 
     private void sanitizeWidgetsShortcutsAndPackages() {
         Context context = mApp.getContext();
-        ContentResolver contentResolver = context.getContentResolver();
 
         // Remove any ghost widgets
-        Settings.call(contentResolver,
-                Settings.METHOD_REMOVE_GHOST_WIDGETS);
+        mApp.getModel().getModelDbController().removeGhostWidgets();
 
         // Update pinned state of model shortcuts
         mBgDataModel.updateShortcutPinnedState(context);
@@ -951,7 +934,6 @@
     }
 
     private List<LauncherActivityInfo> loadAllApps() {
-        testLogD(WORK_TAB_MISSING, "loadingAllApps");
         final List<UserHandle> profiles = mUserCache.getUserProfiles();
         List<LauncherActivityInfo> allActivityList = new ArrayList<>();
         // Clear the list of apps
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index f0e5ef6..e10e72d 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -37,7 +37,6 @@
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
 import android.net.Uri;
-import android.os.Binder;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
@@ -85,6 +84,7 @@
     private static final String TAG = "LauncherProvider";
 
     private static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
+    public static final String EXTRA_DB_NAME = "db_name";
 
     protected DatabaseHelper mOpenHelper;
 
@@ -140,7 +140,7 @@
                 table, projection, selection, selectionArgs, null, null, sortOrder);
 
         final Bundle extra = new Bundle();
-        extra.putString(LauncherSettings.Settings.EXTRA_DB_NAME, mOpenHelper.getDatabaseName());
+        extra.putString(EXTRA_DB_NAME, mOpenHelper.getDatabaseName());
         result.setExtras(extra);
         return result;
     }
@@ -162,28 +162,6 @@
     }
 
     /**
-     * Similar to insert but for adding multiple values in a transaction.
-     */
-    @WorkerThread
-    public int bulkInsert(String table, ContentValues[] values) {
-        createDbIfNotExists();
-
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        try (SQLiteTransaction t = new SQLiteTransaction(db)) {
-            int numValues = values.length;
-            for (int i = 0; i < numValues; i++) {
-                addModifiedTime(values[i]);
-                if (mOpenHelper.dbInsertAndCheck(db, table, values[i]) < 0) {
-                    return 0;
-                }
-            }
-            onAddOrDeleteOp(db);
-            t.commit();
-        }
-        return values.length;
-    }
-
-    /**
      * Refer {@link SQLiteDatabase#delete(String, String, String[])}
      */
     @WorkerThread
@@ -191,10 +169,6 @@
         createDbIfNotExists();
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
 
-        if (Binder.getCallingPid() != Process.myPid()
-                && Favorites.TABLE_NAME.equalsIgnoreCase(table)) {
-            mOpenHelper.removeGhostWidgets(mOpenHelper.getWritableDatabase());
-        }
         int count = db.delete(table, selection, selectionArgs);
         if (count > 0) {
             onAddOrDeleteOp(db);
@@ -250,6 +224,7 @@
     public void createEmptyDB() {
         createDbIfNotExists();
         mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
+        LauncherPrefs.get(mContext).putSync(getEmptyDbCreatedKey().to(true));
     }
 
     /**
@@ -280,14 +255,34 @@
                 mOpenHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
     }
 
+
+    /**
+     * Migrates the DB if needed. If the migration failed, it clears the DB.
+     */
+    public void tryMigrateDB() {
+        if (!migrateGridIfNeeded()) {
+            Log.d(TAG, "Migration failed: resetting launcher database");
+            createEmptyDB();
+            LauncherPrefs.get(mContext).putSync(
+                    getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()).to(true));
+
+            // Write the grid state to avoid another migration
+            new DeviceGridState(LauncherAppState.getIDP(mContext)).writeToPrefs(mContext);
+        }
+    }
+
     /**
      * Migrates the DB if needed, and returns false if the migration failed
      * and DB needs to be cleared.
      * @return true if migration was success or ignored, false if migration failed
      * and the DB should be reset.
      */
-    public boolean migrateGridIfNeeded() {
+    private boolean migrateGridIfNeeded() {
         createDbIfNotExists();
+        if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
+            // If we have already create a new DB, ignore migration
+            return false;
+        }
         InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
         if (!GridSizeMigrationUtil.needsToMigrate(mContext, idp)) {
             return true;
@@ -358,7 +353,7 @@
     }
 
     private void clearFlagEmptyDbCreated() {
-        LauncherPrefs.get(mContext).removeSync(getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()));
+        LauncherPrefs.get(mContext).removeSync(getEmptyDbCreatedKey());
     }
 
     /**
@@ -372,7 +367,7 @@
     public synchronized void loadDefaultFavoritesIfNecessary() {
         createDbIfNotExists();
 
-        if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()))) {
+        if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
             Log.d(TAG, "loading default workspace");
 
             LauncherWidgetHolder widgetHolder = mOpenHelper.newLauncherWidgetHolder();
@@ -471,7 +466,7 @@
                 () -> parser, AutoInstallsLayout.TAG_WORKSPACE);
     }
 
-    private static Uri getLayoutUri(String authority, Context ctx) {
+    public static Uri getLayoutUri(String authority, Context ctx) {
         InvariantDeviceProfile grid = LauncherAppState.getIDP(ctx);
         return new Uri.Builder().scheme("content").authority(authority).path("launcher_layout")
                 .appendQueryParameter("version", "1")
@@ -491,6 +486,10 @@
                 mOpenHelper, mContext.getResources(), defaultLayout);
     }
 
+    private ConstantItem<Boolean> getEmptyDbCreatedKey() {
+        return getEmptyDbCreatedKey(mOpenHelper.getDatabaseName());
+    }
+
     /**
      * Re-composite given key in respect to database. If the current db is
      * {@link LauncherFiles#LAUNCHER_DB}, return the key as-is. Otherwise append the db name to
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index ddb8b05..2358a9f 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -16,13 +16,12 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
+import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
-import android.content.ContentProviderOperation;
-import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
-import android.net.Uri;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -32,9 +31,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.LauncherProvider;
 import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.LauncherSettings.Settings;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.celllayout.CellPosMapper.CellPos;
@@ -45,6 +42,7 @@
 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.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.ItemInfoMatcher;
@@ -80,7 +78,7 @@
     private final boolean mVerifyChanges;
 
     // Keep track of delete operations that occur when an Undo option is present; we may not commit.
-    private final List<Runnable> mDeleteRunnables = new ArrayList<>();
+    private final List<ModelTask> mDeleteRunnables = new ArrayList<>();
     private boolean mPreparingToUndo;
     private final CellPosMapper mCellPosMapper;
 
@@ -217,8 +215,7 @@
         item.spanX = spanX;
         item.spanY = spanY;
         notifyItemModified(item);
-
-        MODEL_EXECUTOR.execute(new UpdateItemRunnable(item, () ->
+        new UpdateItemRunnable(item, () ->
                 new ContentWriter(mContext)
                         .put(Favorites.CONTAINER, item.container)
                         .put(Favorites.CELLX, item.cellX)
@@ -226,7 +223,8 @@
                         .put(Favorites.RANK, item.rank)
                         .put(Favorites.SPANX, item.spanX)
                         .put(Favorites.SPANY, item.spanY)
-                        .put(Favorites.SCREEN, item.screenId)));
+                        .put(Favorites.SCREEN, item.screenId))
+                .executeOnModelThread();
     }
 
     /**
@@ -234,11 +232,11 @@
      */
     public void updateItemInDatabase(ItemInfo item) {
         notifyItemModified(item);
-        MODEL_EXECUTOR.execute(new UpdateItemRunnable(item, () -> {
+        new UpdateItemRunnable(item, () -> {
             ContentWriter writer = new ContentWriter(mContext);
             item.onAddToDatabase(writer);
             return writer;
-        }));
+        }).executeOnModelThread();
     }
 
     private void notifyItemModified(ItemInfo item) {
@@ -253,13 +251,12 @@
             int container, int screenId, int cellX, int cellY) {
         updateItemInfoProps(item, container, screenId, cellX, cellY);
 
-        final ContentResolver cr = mContext.getContentResolver();
-        item.id = Settings.call(cr, Settings.METHOD_NEW_ITEM_ID).getInt(Settings.EXTRA_VALUE);
+        item.id = mModel.getModelDbController().generateNewItemId();
         notifyOtherCallbacks(c -> c.bindItems(Collections.singletonList(item), false));
 
         ModelVerifier verifier = new ModelVerifier();
         final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
-        MODEL_EXECUTOR.execute(() -> {
+        newModelTask(() -> {
             // Write the item on background thread, as some properties might have been updated in
             // the background.
             final ContentWriter writer = new ContentWriter(mContext);
@@ -272,7 +269,7 @@
                 mBgDataModel.addItem(mContext, item, true);
                 verifier.verifyModel();
             }
-        });
+        }).executeOnModelThread();
     }
 
     /**
@@ -303,15 +300,13 @@
                 Collectors.joining(","))
                 + ". Reason: [" + (TextUtils.isEmpty(reason) ? "unknown" : reason) + "]");
         notifyDelete(items);
-        enqueueDeleteRunnable(() -> {
+        enqueueDeleteRunnable(newModelTask(() -> {
             for (ItemInfo item : items) {
-                final Uri uri = Favorites.getContentUri(item.id);
-                mContext.getContentResolver().delete(uri, null, null);
-
+                mModel.getModelDbController().delete(TABLE_NAME, itemIdMatch(item.id), null);
                 mBgDataModel.removeItem(mContext, item);
                 verifier.verifyModel();
             }
-        });
+        }));
     }
 
     /**
@@ -321,7 +316,7 @@
         ModelVerifier verifier = new ModelVerifier();
         notifyDelete(Collections.singleton(info));
 
-        enqueueDeleteRunnable(() -> {
+        enqueueDeleteRunnable(newModelTask(() -> {
             mModel.getModelDbController().delete(Favorites.TABLE_NAME,
                     Favorites.CONTAINER + "=" + info.id, null);
             mBgDataModel.removeItem(mContext, info.contents);
@@ -331,7 +326,7 @@
                     Favorites._ID + "=" + info.id, null);
             mBgDataModel.removeItem(mContext, info);
             verifier.verifyModel();
-        });
+        }));
     }
 
     /**
@@ -343,7 +338,7 @@
         if (holder != null && !info.isCustomWidget() && info.isWidgetIdAllocated()) {
             // Deleting an app widget ID is a void call but writes to disk before returning
             // to the caller...
-            enqueueDeleteRunnable(() -> holder.deleteAppWidgetId(info.appWidgetId));
+            enqueueDeleteRunnable(newModelTask(() -> holder.deleteAppWidgetId(info.appWidgetId)));
         }
         deleteItemFromDatabase(info, reason);
     }
@@ -373,19 +368,17 @@
      * {@link #commitDelete()} is called (or abandoned if {@link #abortDelete} is called).
      * Otherwise, we run the Runnable immediately.
      */
-    private void enqueueDeleteRunnable(Runnable r) {
+    private void enqueueDeleteRunnable(ModelTask r) {
         if (mPreparingToUndo) {
             mDeleteRunnables.add(r);
         } else {
-            MODEL_EXECUTOR.execute(r);
+            r.executeOnModelThread();
         }
     }
 
     public void commitDelete() {
         mPreparingToUndo = false;
-        for (Runnable runnable : mDeleteRunnables) {
-            MODEL_EXECUTOR.execute(runnable);
-        }
+        mDeleteRunnables.forEach(ModelTask::executeOnModelThread);
         mDeleteRunnables.clear();
     }
 
@@ -426,10 +419,9 @@
         }
 
         @Override
-        public void run() {
-            Uri uri = Favorites.getContentUri(mItemId);
-            mContext.getContentResolver().update(uri, mWriter.get().getValues(mContext),
-                    null, null);
+        public void runImpl() {
+            mModel.getModelDbController().update(
+                    TABLE_NAME, mWriter.get().getValues(mContext), itemIdMatch(mItemId), null);
             updateItemArrays(mItem, mItemId);
         }
     }
@@ -444,27 +436,24 @@
         }
 
         @Override
-        public void run() {
-            ArrayList<ContentProviderOperation> ops = new ArrayList<>();
-            int count = mItems.size();
-            for (int i = 0; i < count; i++) {
-                ItemInfo item = mItems.get(i);
-                final int itemId = item.id;
-                final Uri uri = Favorites.getContentUri(itemId);
-                ContentValues values = mValues.get(i);
-
-                ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
-                updateItemArrays(item, itemId);
-            }
-            try {
-                mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, ops);
+        public void runImpl() {
+            try (SQLiteTransaction t = mModel.getModelDbController().newTransaction()) {
+                int count = mItems.size();
+                for (int i = 0; i < count; i++) {
+                    ItemInfo item = mItems.get(i);
+                    final int itemId = item.id;
+                    mModel.getModelDbController().update(
+                            TABLE_NAME, mValues.get(i), itemIdMatch(itemId), null);
+                    updateItemArrays(item, itemId);
+                }
+                t.commit();
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }
     }
 
-    private abstract class UpdateItemBaseRunnable implements Runnable {
+    private abstract class UpdateItemBaseRunnable extends ModelTask {
         private final StackTraceElement[] mStackTrace;
         private final ModelVerifier mVerifier = new ModelVerifier();
 
@@ -498,9 +487,9 @@
                                 modelItem.container == Favorites.CONTAINER_HOTSEAT)) {
                     switch (modelItem.itemType) {
                         case Favorites.ITEM_TYPE_APPLICATION:
-                        case Favorites.ITEM_TYPE_SHORTCUT:
                         case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                         case Favorites.ITEM_TYPE_FOLDER:
+                        case Favorites.ITEM_TYPE_APP_PAIR:
                             if (!mBgDataModel.workspaceItems.contains(modelItem)) {
                                 mBgDataModel.workspaceItems.add(modelItem);
                             }
@@ -516,6 +505,35 @@
         }
     }
 
+    private abstract class ModelTask implements Runnable {
+
+        private final int mLoadId = mBgDataModel.lastLoadId;
+
+        @Override
+        public final void run() {
+            if (mLoadId != mModel.getLastLoadId()) {
+                Log.d(TAG, "Model changed before the task could execute");
+                return;
+            }
+            runImpl();
+        }
+
+        public final void executeOnModelThread() {
+            MODEL_EXECUTOR.execute(this);
+        }
+
+        public abstract void runImpl();
+    }
+
+    private ModelTask newModelTask(Runnable r) {
+        return new ModelTask() {
+            @Override
+            public void runImpl() {
+                r.run();
+            }
+        };
+    }
+
     /**
      * Utility class to verify model updates are propagated properly to the callback.
      */
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 8c938f4..2591550 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -93,11 +93,6 @@
         mOp = op;
         mUser = user;
         mPackages = packages;
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.WORK_TAB_MISSING, "PackageUpdatedTask mOp: " + mOp +
-                    " packageCount: " + mPackages.length + " user: " + user);
-            DEBUG = true;
-        }
     }
 
     @Override
@@ -142,9 +137,6 @@
                         // The update may have changed which shortcuts/widgets are available.
                         // Refresh the widgets for the package if we have an activity running.
                         Launcher launcher = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
-                        if (TestProtocol.sDebugTracing) {
-                            Log.d(TestProtocol.WORK_TAB_MISSING, "launcher: " + launcher);
-                        }
                         if (launcher != null) {
                             launcher.refreshAndBindWidgetsForPackageUser(
                                     new PackageUserKey(packages[i], mUser));
diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java
index cb78138..63ca35b 100644
--- a/src/com/android/launcher3/model/UserLockStateChangedTask.java
+++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java
@@ -16,12 +16,10 @@
 package com.android.launcher3.model;
 
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
-import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
 
 import android.content.Context;
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
-import android.util.Log;
 
 import androidx.annotation.NonNull;
 
@@ -31,7 +29,6 @@
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
-import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
 
@@ -63,10 +60,6 @@
         if (mIsUserUnlocked) {
             QueryResult shortcuts = new ShortcutRequest(context, mUser)
                     .query(ShortcutRequest.PINNED);
-            if (TestProtocol.sDebugTracing) {
-                Log.d(WORK_TAB_MISSING, "shortcutQuery success? "
-                        + shortcuts.wasSuccess());
-            }
             if (shortcuts.wasSuccess()) {
                 for (ShortcutInfo shortcut : shortcuts) {
                     pinnedShortcuts.put(ShortcutKey.fromInfo(shortcut), shortcut);
@@ -89,9 +82,6 @@
                     if (mIsUserUnlocked) {
                         ShortcutKey key = ShortcutKey.fromItemInfo(si);
                         ShortcutInfo shortcut = pinnedShortcuts.get(key);
-                        if (TestProtocol.sDebugTracing) {
-                            Log.d(WORK_TAB_MISSING, "shortcutInfo: " + shortcut);
-                        }
                         // We couldn't verify the shortcut during loader. If its no longer available
                         // (probably due to clear data), delete the workspace item as well
                         if (shortcut == null) {
diff --git a/src/com/android/launcher3/model/WorkspaceItemSpaceFinder.java b/src/com/android/launcher3/model/WorkspaceItemSpaceFinder.java
index 93fc6a5..1fc8a03 100644
--- a/src/com/android/launcher3/model/WorkspaceItemSpaceFinder.java
+++ b/src/com/android/launcher3/model/WorkspaceItemSpaceFinder.java
@@ -82,9 +82,7 @@
 
         if (!found) {
             // Still no position found. Add a new screen to the end.
-            screenId = LauncherSettings.Settings.call(app.getContext().getContentResolver(),
-                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
-                    .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+            screenId = app.getModel().getModelDbController().getNewScreenId();
 
             // Save the screen id for binding in the workspace
             workspaceScreens.add(screenId);
diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java
index e5a0eb1..9bf6d43 100644
--- a/src/com/android/launcher3/model/data/FolderInfo.java
+++ b/src/com/android/launcher3/model/data/FolderInfo.java
@@ -119,8 +119,8 @@
     public static FolderInfo createAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
         FolderInfo newAppPair = new FolderInfo();
         newAppPair.itemType = LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
-        newAppPair.contents.add(app1);
-        newAppPair.contents.add(app2);
+        newAppPair.add(app1, /* animate */ false);
+        newAppPair.add(app2, /* animate */ false);
         return newAppPair;
     }
 
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index 660929c..9afa459 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -30,7 +30,6 @@
 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 static com.android.launcher3.shortcuts.ShortcutKey.EXTRA_SHORTCUT_ID;
@@ -50,7 +49,6 @@
 import com.android.launcher3.LauncherSettings.Animation;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logger.LauncherAtom.AllAppsContainer;
 import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
@@ -88,7 +86,6 @@
 
     /**
      * 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_APP_PAIR},
@@ -325,9 +322,7 @@
      * Returns whether this item should use the background animation.
      */
     public boolean shouldUseBackgroundAnimation() {
-        return animationType == LauncherSettings.Animation.VIEW_BACKGROUND
-                && FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()
-                && FeatureFlags.ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION.get();
+        return animationType == LauncherSettings.Animation.VIEW_BACKGROUND;
     }
 
     /**
@@ -368,13 +363,6 @@
                                 })
                                 .orElse(LauncherAtom.Shortcut.newBuilder()));
                 break;
-            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
diff --git a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
index 01606d4..3ce194d 100644
--- a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
+++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
@@ -96,7 +96,7 @@
 
 
     public WorkspaceItemInfo() {
-        itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+        itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
     }
 
     public WorkspaceItemInfo(WorkspaceItemInfo info) {
@@ -205,8 +205,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 && hasStatusFlag(
+                FLAG_SUPPORTS_WEB_UI | FLAG_AUTOINSTALL_ICON | FLAG_RESTORED_ICON)) {
             // Legacy shortcuts and promise icons with web UI may not have a componentName but just
             // a packageName. In that case create a empty componentName instead of adding additional
             // check everywhere.
diff --git a/src/com/android/launcher3/notification/NotificationContainer.java b/src/com/android/launcher3/notification/NotificationContainer.java
index 9eb05cd..7cc9ad3 100644
--- a/src/com/android/launcher3/notification/NotificationContainer.java
+++ b/src/com/android/launcher3/notification/NotificationContainer.java
@@ -15,7 +15,7 @@
  */
 package com.android.launcher3.notification;
 
-import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.app.animation.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
 
 import android.animation.Animator;
diff --git a/src/com/android/launcher3/notification/NotificationInfo.java b/src/com/android/launcher3/notification/NotificationInfo.java
index bb2c37f..f4468fd 100644
--- a/src/com/android/launcher3/notification/NotificationInfo.java
+++ b/src/com/android/launcher3/notification/NotificationInfo.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ACTION_POPUP;
 import static com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_ALL_APPS;
+import static com.android.launcher3.Utilities.allowBGLaunch;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_LAUNCH_TAP;
 
 import android.app.ActivityOptions;
@@ -26,7 +27,6 @@
 import android.content.Context;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
-import android.os.Bundle;
 import android.service.notification.StatusBarNotification;
 import android.view.View;
 
@@ -103,10 +103,10 @@
             return;
         }
         final ActivityContext context = ActivityContext.lookupContext(view.getContext());
-        Bundle activityOptions = ActivityOptions.makeClipRevealAnimation(
-                view, 0, 0, view.getWidth(), view.getHeight()).toBundle();
+        ActivityOptions options = allowBGLaunch(ActivityOptions.makeClipRevealAnimation(
+                view, 0, 0, view.getWidth(), view.getHeight()));
         try {
-            intent.send(null, 0, null, null, null, null, activityOptions);
+            intent.send(null, 0, null, null, null, null, options.toBundle());
             context.getStatsLogManager().logger().withItemInfo(mItemInfo)
                     .log(LAUNCHER_NOTIFICATION_LAUNCH_TAP);
         } catch (PendingIntent.CanceledException e) {
diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
index 16a4057..ecd018b 100644
--- a/src/com/android/launcher3/notification/NotificationMainView.java
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -16,8 +16,8 @@
 
 package com.android.launcher3.notification;
 
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.Utilities.mapToRange;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DISMISSED;
 
 import android.animation.AnimatorSet;
diff --git a/src/com/android/launcher3/pageindicators/PageIndicator.java b/src/com/android/launcher3/pageindicators/PageIndicator.java
index 570d6ff..4ab2037 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicator.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicator.java
@@ -27,6 +27,14 @@
     void setMarkersCount(int numMarkers);
 
     /**
+     * Sets flag to indicate when the screens are in the process of binding so that we don't animate
+     * during that period.
+     */
+    default void setAreScreensBinding(boolean areScreensBinding) {
+        // No-op by default
+    }
+
+    /**
      * Sets the flag if the Page Indicator should autohide.
      */
     default void setShouldAutoHide(boolean shouldAutoHide) {
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index b2c64b3..ce71275 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -130,6 +130,7 @@
      */
     private float mCurrentPosition;
     private float mFinalPosition;
+    private boolean mAreScreensBinding;
     private ObjectAnimator mAnimator;
     private @Nullable ObjectAnimator mAlphaAnimator;
 
@@ -163,7 +164,7 @@
 
     @Override
     public void setScroll(int currentScroll, int totalScroll) {
-        if (SHOW_DOT_PAGINATION.get() && mActivePage != 0 && currentScroll == 0) {
+        if (SHOW_DOT_PAGINATION.get() && currentScroll == 0 && totalScroll == 0) {
             CURRENT_POSITION.set(this, (float) mActivePage);
             return;
         }
@@ -172,6 +173,11 @@
             return;
         }
 
+        // Skip scroll update during binding. We will update it when binding completes.
+        if (mAreScreensBinding) {
+            return;
+        }
+
         if (mShouldAutoHide) {
             animatePaginationToAlpha(VISIBLE_ALPHA);
         }
@@ -359,6 +365,16 @@
     }
 
     @Override
+    public void setAreScreensBinding(boolean areScreensBinding) {
+        // Reapply correct current position which was skipped during setScroll.
+        if (mAreScreensBinding && !areScreensBinding) {
+            CURRENT_POSITION.set(this, (float) mActivePage);
+        }
+
+        mAreScreensBinding = areScreensBinding;
+    }
+
+    @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         // Add extra spacing of mDotRadius on all sides so than entry animation could be run.
         int width = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY ?
@@ -381,7 +397,9 @@
 
         // Draw all page indicators;
         float circleGap = mCircleGap;
-        float startX = (getWidth() - (mNumPages * circleGap) + mDotRadius) / 2;
+        float startX = ((float) getWidth() / 2)
+                - (mCircleGap * (((float) mNumPages - 1) / 2))
+                - mDotRadius;
 
         float x = startX + mDotRadius;
         float y = getHeight() / 2;
@@ -420,9 +438,9 @@
         float startCircle = (int) mCurrentPosition;
         float delta = mCurrentPosition - startCircle;
         float diameter = 2 * mDotRadius;
-        float startX;
-
-        startX = ((getWidth() - (mNumPages * mCircleGap) + mDotRadius) / 2);
+        float startX = ((float) getWidth() / 2)
+                - (mCircleGap * (((float) mNumPages - 1) / 2))
+                - mDotRadius;
         sTempRect.top = (getHeight() * 0.5f) - mDotRadius;
         sTempRect.bottom = (getHeight() * 0.5f) + mDotRadius;
         sTempRect.left = startX + (startCircle * mCircleGap);
diff --git a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
index b24ee34..351ebce 100644
--- a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
+++ b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
@@ -16,8 +16,11 @@
 
 package com.android.launcher3.pm;
 
+import static com.android.launcher3.Utilities.allowBGLaunch;
+
 import android.annotation.TargetApi;
 import android.app.Activity;
+import android.app.ActivityOptions;
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
 import android.content.Context;
@@ -72,7 +75,7 @@
     }
 
     public int getItemType() {
-        return LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+        return LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
     }
 
     @Override
@@ -138,8 +141,10 @@
             }
             IntentSender is = activity.getSystemService(LauncherApps.class)
                     .getShortcutConfigActivityIntent(mInfo);
+            ActivityOptions options = allowBGLaunch(ActivityOptions.makeBasic());
             try {
-                activity.startIntentSenderForResult(is, requestCode, null, 0, 0, 0);
+                activity.startIntentSenderForResult(is, requestCode, null, 0, 0, 0,
+                        options.toBundle());
                 return true;
             } catch (IntentSender.SendIntentException e) {
                 Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 24a9609..92822ab 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -16,16 +16,19 @@
 
 package com.android.launcher3.pm;
 
-import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
-import static com.android.launcher3.testing.shared.TestProtocol.sDebugTracing;
-import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
+import static com.android.launcher3.Utilities.ATLEAST_U;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.content.Context;
 import android.content.Intent;
+import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArrayMap;
-import android.util.LongSparseArray;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
@@ -34,136 +37,121 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
 
 /**
  * Class which manages a local cache of user handles to avoid system rpc
  */
-public class UserCache {
+public class UserCache implements SafeCloseable {
+
+    public static final String ACTION_PROFILE_ADDED = ATLEAST_U
+            ? Intent.ACTION_PROFILE_ADDED : Intent.ACTION_MANAGED_PROFILE_ADDED;
+    public static final String ACTION_PROFILE_REMOVED = ATLEAST_U
+            ? Intent.ACTION_PROFILE_REMOVED : Intent.ACTION_MANAGED_PROFILE_REMOVED;
+
+    public static final String ACTION_PROFILE_UNLOCKED = ATLEAST_U
+            ? Intent.ACTION_PROFILE_ACCESSIBLE : Intent.ACTION_MANAGED_PROFILE_UNLOCKED;
+    public static final String ACTION_PROFILE_LOCKED = ATLEAST_U
+            ? Intent.ACTION_PROFILE_INACCESSIBLE : Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE;
 
     public static final MainThreadInitializedObject<UserCache> INSTANCE =
             new MainThreadInitializedObject<>(UserCache::new);
 
-    private final Context mContext;
-    private final UserManager mUserManager;
-    private final ArrayList<Runnable> mUserChangeListeners = new ArrayList<>();
+    private final List<BiConsumer<UserHandle, String>> mUserEventListeners = new ArrayList<>();
     private final SimpleBroadcastReceiver mUserChangeReceiver =
             new SimpleBroadcastReceiver(this::onUsersChanged);
 
-    private LongSparseArray<UserHandle> mUsers;
-    // Create a separate reverse map as LongSparseArray.indexOfValue checks if objects are same
-    // and not {@link Object#equals}
-    private ArrayMap<UserHandle, Long> mUserToSerialMap;
+    private final Context mContext;
+
+    @NonNull
+    private Map<UserHandle, Long> mUserToSerialMap;
 
     private UserCache(Context context) {
         mContext = context;
-        mUserManager = context.getSystemService(UserManager.class);
+        mUserToSerialMap = Collections.emptyMap();
+        MODEL_EXECUTOR.execute(this::initAsync);
     }
 
+    @Override
+    public void close() {
+        MODEL_EXECUTOR.execute(() -> mUserChangeReceiver.unregisterReceiverSafely(mContext));
+    }
+
+    @WorkerThread
+    private void initAsync() {
+        mUserChangeReceiver.register(mContext,
+                Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
+                Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
+                ACTION_PROFILE_ADDED,
+                ACTION_PROFILE_REMOVED,
+                ACTION_PROFILE_UNLOCKED,
+                ACTION_PROFILE_LOCKED);
+        updateCache();
+    }
+
+    @AnyThread
     private void onUsersChanged(Intent intent) {
-        testLogD(WORK_TAB_MISSING, "onUsersChanged intent: " + intent);
-        enableAndResetCache();
-        mUserChangeListeners.forEach(Runnable::run);
+        MODEL_EXECUTOR.execute(this::updateCache);
+        UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
+        if (user == null) {
+            return;
+        }
+        String action = intent.getAction();
+        mUserEventListeners.forEach(l -> l.accept(user, action));
+    }
+
+    @WorkerThread
+    private void updateCache() {
+        mUserToSerialMap = queryAllUsers(mContext.getSystemService(UserManager.class));
     }
 
     /**
      * Adds a listener for user additions and removals
      */
-    public SafeCloseable addUserChangeListener(Runnable command) {
-        synchronized (this) {
-            if (mUserChangeListeners.isEmpty()) {
-                // Enable caching and start listening for user broadcast
-                mUserChangeReceiver.register(mContext,
-                        Intent.ACTION_MANAGED_PROFILE_ADDED,
-                        Intent.ACTION_MANAGED_PROFILE_REMOVED);
-                enableAndResetCache();
-            }
-            mUserChangeListeners.add(command);
-            return () -> removeUserChangeListener(command);
-        }
-    }
-
-    private void enableAndResetCache() {
-        synchronized (this) {
-            mUsers = new LongSparseArray<>();
-            mUserToSerialMap = new ArrayMap<>();
-            List<UserHandle> users = mUserManager.getUserProfiles();
-            if (users != null) {
-                for (UserHandle user : users) {
-                    testLogD(WORK_TAB_MISSING, "caching user: " + user);
-                    long serial = mUserManager.getSerialNumberForUser(user);
-                    mUsers.put(serial, user);
-                    mUserToSerialMap.put(user, serial);
-                }
-            }
-        }
-    }
-
-    private void removeUserChangeListener(Runnable command) {
-        synchronized (this) {
-            mUserChangeListeners.remove(command);
-            if (mUserChangeListeners.isEmpty()) {
-                // Disable cache and stop listening
-                mContext.unregisterReceiver(mUserChangeReceiver);
-
-                mUsers = null;
-                mUserToSerialMap = null;
-            }
-        }
+    public SafeCloseable addUserEventListener(BiConsumer<UserHandle, String> listener) {
+        mUserEventListeners.add(listener);
+        return () -> mUserEventListeners.remove(listener);
     }
 
     /**
      * @see UserManager#getSerialNumberForUser(UserHandle)
      */
     public long getSerialNumberForUser(UserHandle user) {
-        synchronized (this) {
-            if (mUserToSerialMap != null) {
-                Long serial = mUserToSerialMap.get(user);
-                return serial == null ? 0 : serial;
-            }
-        }
-        return mUserManager.getSerialNumberForUser(user);
+        Long serial = mUserToSerialMap.get(user);
+        return serial == null ? 0 : serial;
     }
 
     /**
      * @see UserManager#getUserForSerialNumber(long)
      */
     public UserHandle getUserForSerialNumber(long serialNumber) {
-        synchronized (this) {
-            if (mUsers != null) {
-                return mUsers.get(serialNumber);
-            }
-        }
-        return mUserManager.getUserForSerialNumber(serialNumber);
+        Long value = serialNumber;
+        return mUserToSerialMap
+                .entrySet()
+                .stream()
+                .filter(entry -> value.equals(entry.getValue()))
+                .findFirst()
+                .map(Map.Entry::getKey)
+                .orElse(Process.myUserHandle());
     }
 
     /**
      * @see UserManager#getUserProfiles()
      */
     public List<UserHandle> getUserProfiles() {
-        StringBuilder usersToReturn = new StringBuilder();
-        List<UserHandle> users;
-        if (sDebugTracing) {
-            users = mUserManager.getUserProfiles();
-            for (UserHandle u : users) {
-                usersToReturn.append(u).append(" && ");
-            }
-            testLogD(WORK_TAB_MISSING, "users from userManager: " + usersToReturn);
-        }
+        return List.copyOf(mUserToSerialMap.keySet());
+    }
 
-        synchronized (this) {
-            if (mUsers != null) {
-                usersToReturn = new StringBuilder();
-                for (UserHandle u : mUserToSerialMap.keySet()) {
-                    usersToReturn.append(u).append(" && ");
-                }
-                testLogD(WORK_TAB_MISSING, "users from cache: " + usersToReturn);
-                return new ArrayList<>(mUserToSerialMap.keySet());
-            } else {
-                testLogD(WORK_TAB_MISSING, "users from cache null");
+    private static Map<UserHandle, Long> queryAllUsers(UserManager userManager) {
+        Map<UserHandle, Long> users = new ArrayMap<>();
+        List<UserHandle> usersActual = userManager.getUserProfiles();
+        if (usersActual != null) {
+            for (UserHandle user : usersActual) {
+                long serial = userManager.getSerialNumberForUser(user);
+                users.put(user, serial);
             }
         }
-
-        users = mUserManager.getUserProfiles();
-        return users == null ? Collections.emptyList() : users;
+        return users;
     }
 }
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 72f99cb..e0f245f 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -18,11 +18,11 @@
 
 import static androidx.core.content.ContextCompat.getColorStateList;
 
-import static com.android.launcher3.anim.Interpolators.ACCELERATED_EASE;
-import static com.android.launcher3.anim.Interpolators.DECELERATED_EASE;
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED_ACCELERATE;
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED_DECELERATE;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.ACCELERATED_EASE;
+import static com.android.app.animation.Interpolators.DECELERATED_EASE;
+import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE;
+import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_MATERIAL_U_POPUP;
 
 import android.animation.Animator;
@@ -673,10 +673,6 @@
         }
         mIsOpen = false;
 
-        mOpenCloseAnimator = getOpenCloseAnimator(false, mCloseDuration, mCloseFadeStartDelay,
-                mCloseFadeDuration, mCloseChildFadeStartDelay, mCloseChildFadeDuration,
-                ACCELERATED_EASE);
-
         mOpenCloseAnimator = ENABLE_MATERIAL_U_POPUP.get()
                 ? getMaterialUOpenCloseAnimator(
                         false,
diff --git a/src/com/android/launcher3/popup/RemoteActionShortcut.java b/src/com/android/launcher3/popup/RemoteActionShortcut.java
index 7c9ab87..eab0969 100644
--- a/src/com/android/launcher3/popup/RemoteActionShortcut.java
+++ b/src/com/android/launcher3/popup/RemoteActionShortcut.java
@@ -16,10 +16,12 @@
 
 package com.android.launcher3.popup;
 
+import static com.android.launcher3.Utilities.allowBGLaunch;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PAUSE_TAP;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.annotation.TargetApi;
+import android.app.ActivityOptions;
 import android.app.PendingIntent;
 import android.app.RemoteAction;
 import android.content.Context;
@@ -84,6 +86,8 @@
         final WeakReference<BaseDraggingActivity> weakTarget = new WeakReference<>(mTarget);
         final String actionIdentity = mAction.getTitle() + ", "
                 + mItemInfo.getTargetComponent().getPackageName();
+
+        ActivityOptions options = allowBGLaunch(ActivityOptions.makeBasic());
         try {
             if (DEBUG) Log.d(TAG, "Sending action: " + actionIdentity);
             mAction.getActionIntent().send(
@@ -103,7 +107,9 @@
                             }
                         }
                     },
-                    MAIN_EXECUTOR.getHandler());
+                    MAIN_EXECUTOR.getHandler(),
+                    null,
+                    options.toBundle());
         } catch (PendingIntent.CanceledException e) {
             Log.e(TAG, "Remote action canceled: " + actionIdentity, e);
             Toast.makeText(mTarget, mTarget.getString(
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
index c718dcc..575551b 100644
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.java
@@ -28,7 +28,6 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.drawable.Icon;
-import android.os.Binder;
 import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.UserManager;
@@ -50,6 +49,13 @@
  */
 public class LauncherDbUtils {
 
+    /**
+     * Returns a string which can be used as a where clause for DB query to match the given itemId
+     */
+    public static String itemIdMatch(int itemId) {
+        return "_id=" + itemId;
+    }
+
     public static IntArray queryIntArray(boolean distinct, SQLiteDatabase db, String tableName,
             String columnName, String selection, String groupBy, String orderBy) {
         IntArray out = new IntArray();
@@ -114,6 +120,10 @@
                 deletedShortcuts.add(lc.id);
                 continue;
             }
+            if (TextUtils.isEmpty(lc.getTitle())) {
+                deletedShortcuts.add(lc.id);
+                continue;
+            }
 
             // Make sure the target intent can be launched without any permissions. Otherwise remove
             // the shortcut
@@ -166,7 +176,7 @@
     /**
      * Utility class to simplify managing sqlite transactions
      */
-    public static class SQLiteTransaction extends Binder implements AutoCloseable {
+    public static class SQLiteTransaction implements AutoCloseable {
         private final SQLiteDatabase mDb;
 
         public SQLiteTransaction(SQLiteDatabase db) {
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index c554def..4725dd1 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -82,11 +82,15 @@
     public static final String APPWIDGET_OLD_IDS = "appwidget_old_ids";
     public static final String APPWIDGET_IDS = "appwidget_ids";
 
+    private static final String[] DB_COLUMNS_TO_LOG = {"profileId", "title", "itemType", "screen",
+            "container", "cellX", "cellY", "spanX", "spanY", "intent"};
+
     /**
      * Tries to restore the backup DB if needed
      */
     public static void restoreIfNeeded(Context context, ModelDbController dbController) {
         if (!isPending(context)) {
+            Log.d(TAG, "No restore task pending, exiting RestoreDbTask");
             return;
         }
         if (!performRestore(context, dbController)) {
@@ -106,6 +110,7 @@
 
     private static boolean performRestore(Context context, ModelDbController controller) {
         SQLiteDatabase db = controller.getDb();
+        FileLog.d(TAG, "performRestore: starting restore from db");
         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
             RestoreDbTask task = new RestoreDbTask();
             task.sanitizeDB(context, controller, db, new BackupManager(context));
@@ -133,10 +138,11 @@
     @VisibleForTesting
     protected int sanitizeDB(Context context, ModelDbController controller, SQLiteDatabase db,
             BackupManager backupManager) throws Exception {
+        FileLog.d(TAG, "Old Launcher Database before sanitizing:");
         // Primary user ids
         long myProfileId = controller.getSerialNumberForUser(myUserHandle());
         long oldProfileId = getDefaultProfileId(db);
-        Log.d(TAG, "sanitizeDB: myProfileId=" + myProfileId + " oldProfileId=" + oldProfileId);
+        FileLog.d(TAG, "sanitizeDB: myProfileId=" + myProfileId + " oldProfileId=" + oldProfileId);
         LongSparseArray<Long> oldManagedProfileIds = getManagedProfileIds(db, oldProfileId);
         LongSparseArray<Long> profileMapping = new LongSparseArray<>(oldManagedProfileIds.size()
                 + 1);
@@ -149,8 +155,11 @@
             if (user != null) {
                 long newManagedProfileId = controller.getSerialNumberForUser(user);
                 profileMapping.put(oldManagedProfileId, newManagedProfileId);
-                Log.d(TAG, "sanitizeDB: managed profile id=" + oldManagedProfileId
+                FileLog.d(TAG, "sanitizeDB: managed profile id=" + oldManagedProfileId
                         + " should be mapped to new id=" + newManagedProfileId);
+            } else {
+                FileLog.e(TAG, "sanitizeDB: No User found for old profileId, Ancestral Serial "
+                        + "Number: " + oldManagedProfileId);
             }
         }
 
@@ -161,11 +170,13 @@
         for (int i = numProfiles - 1; i >= 1; --i) {
             profileIds[i] = Long.toString(profileMapping.keyAt(i));
         }
+
         final String[] args = new String[profileIds.length];
         Arrays.fill(args, "?");
         final String where = "profileId NOT IN (" + TextUtils.join(", ", Arrays.asList(args)) + ")";
-        int itemsDeleted = db.delete(Favorites.TABLE_NAME, where, profileIds);
-        FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted");
+        logUnrestoredItems(db, where, profileIds);
+        int itemsDeletedCount = db.delete(Favorites.TABLE_NAME, where, profileIds);
+        FileLog.d(TAG, itemsDeletedCount + " total items from unrestored user(s) were deleted");
 
         // Mark all items as restored.
         boolean keepAllIcons = Utilities.isPropertyEnabled(LogConfig.KEEP_ALL_ICONS);
@@ -219,8 +230,48 @@
 
         // Override shortcuts
         maybeOverrideShortcuts(context, controller, db, myProfileId);
+        return itemsDeletedCount;
+    }
 
-        return itemsDeleted;
+    /**
+     * Queries and logs the items we will delete from unrestored profiles in the launcher db.
+     * This is to understand why items might be missing during the restore process for Launcher.
+     * @param database the Launcher db to query from.
+     * @param where the SELECT statement to query items that will be deleted.
+     * @param profileIds the profile ID's the user will be migrating to.
+     */
+    private void logUnrestoredItems(SQLiteDatabase database, String where, String[] profileIds) {
+        try (Cursor itemsToDelete = database.query(
+                /* table */ Favorites.TABLE_NAME,
+                /* columns */ DB_COLUMNS_TO_LOG,
+                /* selection */ where,
+                /* selection args */ profileIds,
+                /* groupBy */ null,
+                /* having */ null,
+                /* orderBy */ null
+        )) {
+            if (itemsToDelete.moveToFirst()) {
+                String[] columnNames = itemsToDelete.getColumnNames();
+                StringBuilder stringBuilder = new StringBuilder(
+                        "items to be deleted from the Favorites Table during restore:\n"
+                );
+                do {
+                    for (String columnName : columnNames) {
+                        stringBuilder.append(columnName)
+                            .append("=")
+                            .append(itemsToDelete.getString(
+                                        itemsToDelete.getColumnIndex(columnName)))
+                            .append(" ");
+                    }
+                    stringBuilder.append("\n");
+                } while (itemsToDelete.moveToNext());
+                FileLog.d(TAG, stringBuilder.toString());
+            } else {
+                FileLog.d(TAG, "logDeletedItems: No items found to delete");
+            }
+        } catch (Exception e) {
+            FileLog.e(TAG, "logDeletedItems: Error reading from database", e);
+        }
     }
 
     /**
@@ -321,7 +372,7 @@
      * Marks the DB state as pending restoration
      */
     public static void setPending(Context context) {
-        FileLog.d(TAG, "Restore data received through full backup ");
+        FileLog.d(TAG, "Restore data received through full backup");
         LauncherPrefs.get(context)
                 .putSync(RESTORE_DEVICE.to(new DeviceGridState(context).getDeviceType()));
     }
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
new file mode 100644
index 0000000..3c59c1d
--- /dev/null
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.recyclerview
+
+import android.content.Context
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import com.android.launcher3.BubbleTextView
+import com.android.launcher3.allapps.BaseAllAppsAdapter
+import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR
+import com.android.launcher3.views.ActivityContext
+import java.util.concurrent.Future
+
+const val PREINFLATE_ICONS_ROW_COUNT = 4
+const val EXTRA_ICONS_COUNT = 2
+
+/**
+ * An [RecycledViewPool] that preinflates app icons ([ViewHolder] of [BubbleTextView]) of all apps
+ * [RecyclerView]. The view inflation will happen on background thread and inflated [ViewHolder]s
+ * will be added to [RecycledViewPool] on main thread.
+ */
+class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
+
+    private var future: Future<Void>? = null
+
+    /**
+     * Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
+     */
+    fun <T> preInflateAllAppsViewHolders(context: T) where T : Context, T : ActivityContext {
+        val appsView = context.appsView ?: return
+        val activeRv: RecyclerView = appsView.activeRecyclerView ?: return
+        val preInflateCount = getPreinflateCount(context)
+        if (preInflateCount <= 0) {
+            return
+        }
+
+        // Because we perform onCreateViewHolder() on worker thread, we need a separate
+        // adapter/inflator object as they are not thread-safe. Note that the adapter
+        // just need to perform onCreateViewHolder(parent, VIEW_TYPE_ICON) so it doesn't need
+        // data source information.
+        val adapter: RecyclerView.Adapter<BaseAllAppsAdapter.ViewHolder> =
+            object : BaseAllAppsAdapter<T>(context, context.appsView.layoutInflater, null, null) {
+                override fun setAppsPerRow(appsPerRow: Int) = Unit
+                override fun getLayoutManager(): RecyclerView.LayoutManager? = null
+            }
+
+        // Inflate view holders on background thread, and added to view pool on main thread.
+        future?.cancel(true)
+        future =
+            VIEW_PREINFLATION_EXECUTOR.submit<Void> {
+                val viewHolders =
+                    Array(preInflateCount) {
+                        adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
+                    }
+                MAIN_EXECUTOR.execute {
+                    for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
+                        putRecycledView(viewHolders[i])
+                    }
+                }
+                null
+            }
+    }
+
+    /**
+     * After testing on phone, foldable and tablet, we found [PREINFLATE_ICONS_ROW_COUNT] rows of
+     * app icons plus [EXTRA_ICONS_COUNT] is the magic minimal count of app icons to preinflate to
+     * suffice fast scrolling.
+     *
+     * Note that if [FeatureFlags.ALL_APPS_GONE_VISIBILITY] is enabled, we need to preinfate extra
+     * app icons in size of one all apps pages, so that opening all apps don't need to inflate app
+     * icons.
+     */
+    fun <T> getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
+        var targetPreinflateCount =
+            PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
+                EXTRA_ICONS_COUNT
+        if (FeatureFlags.ALL_APPS_GONE_VISIBILITY.get()) {
+            val grid = ActivityContext.lookupContext<T>(context).deviceProfile
+            val approxRows =
+                Math.ceil((grid.availableHeightPx / grid.allAppsIconSizePx).toDouble()).toInt()
+            targetPreinflateCount += (approxRows + 1) * grid.numShownAllAppsColumns
+        }
+        val existingPreinflateCount = getRecycledViewCount(BaseAllAppsAdapter.VIEW_TYPE_ICON)
+        return targetPreinflateCount - existingPreinflateCount
+    }
+}
diff --git a/src/com/android/launcher3/responsive/AllAppsSpecs.kt b/src/com/android/launcher3/responsive/AllAppsSpecs.kt
new file mode 100644
index 0000000..8ed3ffc
--- /dev/null
+++ b/src/com/android/launcher3/responsive/AllAppsSpecs.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.res.TypedArray
+import com.android.launcher3.R
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
+import com.android.launcher3.util.ResourceHelper
+
+class AllAppsSpecs(widthSpecs: List<AllAppsSpec>, heightSpecs: List<AllAppsSpec>) :
+    ResponsiveSpecs<AllAppsSpec>(widthSpecs, heightSpecs) {
+
+    fun getCalculatedWidthSpec(
+        columns: Int,
+        availableWidth: Int,
+        calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+    ): CalculatedAllAppsSpec {
+        check(calculatedWorkspaceSpec.spec.specType == SpecType.WIDTH) {
+            "Invalid specType for CalculatedWorkspaceSpec. " +
+                "Expected: ${SpecType.WIDTH} - " +
+                "Found: ${calculatedWorkspaceSpec.spec.specType}}"
+        }
+
+        val spec = getWidthSpec(availableWidth)
+        return CalculatedAllAppsSpec(availableWidth, columns, spec, calculatedWorkspaceSpec)
+    }
+
+    fun getCalculatedHeightSpec(
+        rows: Int,
+        availableHeight: Int,
+        calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+    ): CalculatedAllAppsSpec {
+        check(calculatedWorkspaceSpec.spec.specType == SpecType.HEIGHT) {
+            "Invalid specType for CalculatedWorkspaceSpec. " +
+                "Expected: ${SpecType.HEIGHT} - " +
+                "Found: ${calculatedWorkspaceSpec.spec.specType}}"
+        }
+
+        val spec = getHeightSpec(availableHeight)
+        return CalculatedAllAppsSpec(availableHeight, rows, spec, calculatedWorkspaceSpec)
+    }
+
+    companion object {
+        private const val XML_ALL_APPS_SPEC = "allAppsSpec"
+
+        @JvmStatic
+        fun create(resourceHelper: ResourceHelper): AllAppsSpecs {
+            val parser = ResponsiveSpecsParser(resourceHelper)
+            val specs = parser.parseXML(XML_ALL_APPS_SPEC, ::AllAppsSpec)
+            val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+            return AllAppsSpecs(widthSpecs, heightSpecs)
+        }
+    }
+}
+
+data class AllAppsSpec(
+    override val maxAvailableSize: Int,
+    override val specType: SpecType,
+    override val startPadding: SizeSpec,
+    override val endPadding: SizeSpec,
+    override val gutter: SizeSpec,
+    override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
+
+    init {
+        check(isValid()) { "Invalid AllAppsSpec found." }
+    }
+
+    constructor(
+        attrs: TypedArray,
+        specs: Map<String, SizeSpec>
+    ) : this(
+        maxAvailableSize =
+            attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+        specType =
+            SpecType.values()[
+                    attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+        startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+        endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+        gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+        cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
+    )
+}
+
+class CalculatedAllAppsSpec(
+    availableSpace: Int,
+    cells: Int,
+    spec: AllAppsSpec,
+    calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+) : CalculatedResponsiveSpec(availableSpace, cells, spec, calculatedWorkspaceSpec)
diff --git a/src/com/android/launcher3/responsive/FolderSpecs.kt b/src/com/android/launcher3/responsive/FolderSpecs.kt
new file mode 100644
index 0000000..bc2db28
--- /dev/null
+++ b/src/com/android/launcher3/responsive/FolderSpecs.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.res.TypedArray
+import com.android.launcher3.R
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
+import com.android.launcher3.util.ResourceHelper
+
+class FolderSpecs(widthSpecs: List<FolderSpec>, heightSpecs: List<FolderSpec>) :
+    ResponsiveSpecs<FolderSpec>(widthSpecs, heightSpecs) {
+
+    fun getCalculatedWidthSpec(
+        columns: Int,
+        availableWidth: Int,
+        calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+    ): CalculatedFolderSpec {
+        check(calculatedWorkspaceSpec.spec.specType == SpecType.WIDTH) {
+            "Invalid specType for CalculatedWorkspaceSpec. " +
+                "Expected: ${SpecType.WIDTH} - " +
+                "Found: ${calculatedWorkspaceSpec.spec.specType}}"
+        }
+
+        val spec = getWidthSpec(availableWidth)
+        return CalculatedFolderSpec(availableWidth, columns, spec, calculatedWorkspaceSpec)
+    }
+
+    fun getCalculatedHeightSpec(
+        rows: Int,
+        availableHeight: Int,
+        calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+    ): CalculatedFolderSpec {
+        check(calculatedWorkspaceSpec.spec.specType == SpecType.HEIGHT) {
+            "Invalid specType for CalculatedWorkspaceSpec. " +
+                "Expected: ${SpecType.HEIGHT} - " +
+                "Found: ${calculatedWorkspaceSpec.spec.specType}}"
+        }
+
+        val spec = getHeightSpec(availableHeight)
+        return CalculatedFolderSpec(availableHeight, rows, spec, calculatedWorkspaceSpec)
+    }
+
+    companion object {
+
+        private const val XML_FOLDER_SPEC = "folderSpec"
+
+        @JvmStatic
+        fun create(resourceHelper: ResourceHelper): FolderSpecs {
+            val parser = ResponsiveSpecsParser(resourceHelper)
+            val specs = parser.parseXML(XML_FOLDER_SPEC, ::FolderSpec)
+            val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+            return FolderSpecs(widthSpecs, heightSpecs)
+        }
+    }
+}
+
+data class FolderSpec(
+    override val maxAvailableSize: Int,
+    override val specType: SpecType,
+    override val startPadding: SizeSpec,
+    override val endPadding: SizeSpec,
+    override val gutter: SizeSpec,
+    override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
+
+    init {
+        check(isValid()) { "Invalid FolderSpec found." }
+    }
+
+    constructor(
+        attrs: TypedArray,
+        specs: Map<String, SizeSpec>
+    ) : this(
+        maxAvailableSize =
+            attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+        specType =
+            SpecType.values()[
+                    attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+        startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+        endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+        gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+        cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
+    )
+}
+
+class CalculatedFolderSpec(
+    availableSpace: Int,
+    cells: Int,
+    spec: FolderSpec,
+    calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+) : CalculatedResponsiveSpec(availableSpace, cells, spec, calculatedWorkspaceSpec)
diff --git a/src/com/android/launcher3/responsive/HotseatSpecs.kt b/src/com/android/launcher3/responsive/HotseatSpecs.kt
new file mode 100644
index 0000000..482508d
--- /dev/null
+++ b/src/com/android/launcher3/responsive/HotseatSpecs.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.res.TypedArray
+import android.util.Log
+import com.android.launcher3.R
+import com.android.launcher3.util.ResourceHelper
+
+class HotseatSpecs(val specs: List<HotseatSpec>) {
+
+    fun getCalculatedHeightSpec(availableHeight: Int): CalculatedHotseatSpec {
+        val spec = specs.firstOrNull { availableHeight <= it.maxAvailableSize }
+        check(spec != null) { "No available height spec found within $availableHeight." }
+        return CalculatedHotseatSpec(availableHeight, spec)
+    }
+
+    companion object {
+        private const val XML_HOTSEAT_SPEC = "hotseatSpec"
+
+        @JvmStatic
+        fun create(resourceHelper: ResourceHelper): HotseatSpecs {
+            val parser = ResponsiveSpecsParser(resourceHelper)
+            val specs = parser.parseXML(XML_HOTSEAT_SPEC, ::HotseatSpec)
+            return HotseatSpecs(specs.filter { it.specType == ResponsiveSpec.SpecType.HEIGHT })
+        }
+    }
+}
+
+data class HotseatSpec(
+    val maxAvailableSize: Int,
+    val specType: ResponsiveSpec.SpecType,
+    val hotseatQsbSpace: SizeSpec
+) {
+
+    init {
+        check(isValid()) { "Invalid HotseatSpec found." }
+    }
+
+    constructor(
+        attrs: TypedArray,
+        specs: Map<String, SizeSpec>
+    ) : this(
+        maxAvailableSize =
+            attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+        specType =
+            ResponsiveSpec.SpecType.values()[
+                    attrs.getInt(
+                        R.styleable.ResponsiveSpec_specType,
+                        ResponsiveSpec.SpecType.HEIGHT.ordinal
+                    )],
+        hotseatQsbSpace = specs.getOrError(SizeSpec.XmlTags.HOTSEAT_QSB_SPACE)
+    )
+
+    fun isValid(): Boolean {
+        if (maxAvailableSize <= 0) {
+            Log.e(LOG_TAG, "${this::class.simpleName}#isValid - maxAvailableSize <= 0")
+            return false
+        }
+
+        // All specs need to be individually valid
+        if (!allSpecsAreValid()) {
+            Log.e(LOG_TAG, "${this::class.simpleName}#isValid - !allSpecsAreValid()")
+            return false
+        }
+
+        return true
+    }
+
+    private fun allSpecsAreValid(): Boolean {
+        return hotseatQsbSpace.isValid() && hotseatQsbSpace.onlyFixedSize()
+    }
+
+    companion object {
+        private const val LOG_TAG = "HotseatSpec"
+    }
+}
+
+class CalculatedHotseatSpec(val availableSpace: Int, val spec: HotseatSpec) {
+
+    var hotseatQsbSpace: Int = 0
+        private set
+
+    init {
+        hotseatQsbSpace = spec.hotseatQsbSpace.getCalculatedValue(availableSpace)
+    }
+
+    override fun hashCode(): Int {
+        var result = availableSpace.hashCode()
+        result = 31 * result + hotseatQsbSpace.hashCode()
+        result = 31 * result + spec.hashCode()
+        return result
+    }
+
+    override fun equals(other: Any?): Boolean {
+        return other is CalculatedHotseatSpec &&
+            availableSpace == other.availableSpace &&
+            hotseatQsbSpace == other.hotseatQsbSpace &&
+            spec == other.spec
+    }
+
+    override fun toString(): String {
+        return "${this::class.simpleName}(" +
+            "availableSpace=$availableSpace, hotseatQsbSpace=$hotseatQsbSpace, " +
+            "${spec::class.simpleName}.maxAvailableSize=${spec.maxAvailableSize}" +
+            ")"
+    }
+}
diff --git a/src/com/android/launcher3/responsive/ResponsiveSpecs.kt b/src/com/android/launcher3/responsive/ResponsiveSpecs.kt
new file mode 100644
index 0000000..72a0ea4
--- /dev/null
+++ b/src/com/android/launcher3/responsive/ResponsiveSpecs.kt
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.util.Log
+
+/**
+ * Base class for responsive specs that holds a list of width and height specs.
+ *
+ * @param widthSpecs List of width responsive specifications
+ * @param heightSpecs List of height responsive specifications
+ */
+abstract class ResponsiveSpecs<T : ResponsiveSpec>(
+    val widthSpecs: List<T>,
+    val heightSpecs: List<T>
+) {
+
+    init {
+        check(widthSpecs.isNotEmpty() && heightSpecs.isNotEmpty()) {
+            "${this::class.simpleName} is incomplete - " +
+                "width list size = ${widthSpecs.size}; " +
+                "height list size = ${heightSpecs.size}."
+        }
+    }
+
+    /**
+     * Get a [ResponsiveSpec] for width within the breakpoint.
+     *
+     * @param availableWidth The width breakpoint for the spec
+     * @return A [ResponsiveSpec] for width.
+     */
+    fun getWidthSpec(availableWidth: Int): T {
+        val spec = widthSpecs.firstOrNull { availableWidth <= it.maxAvailableSize }
+        check(spec != null) { "No available width spec found within $availableWidth." }
+        return spec
+    }
+
+    /**
+     * Get a [ResponsiveSpec] for height within the breakpoint.
+     *
+     * @param availableHeight The height breakpoint for the spec
+     * @return A [ResponsiveSpec] for height.
+     */
+    fun getHeightSpec(availableHeight: Int): T {
+        val spec = heightSpecs.firstOrNull { availableHeight <= it.maxAvailableSize }
+        check(spec != null) { "No available height spec found within $availableHeight." }
+        return spec
+    }
+}
+
+/**
+ * Base class for a responsive specification that is used to calculate the paddings, gutter and cell
+ * size.
+ *
+ * @param maxAvailableSize indicates the breakpoint to use this specification.
+ * @param specType indicates whether the paddings and gutters will be applied vertically or
+ *   horizontally.
+ * @param startPadding padding used at the top or left (right in RTL) in the workspace folder.
+ * @param endPadding padding used at the bottom or right (left in RTL) in the workspace folder.
+ * @param gutter the space between the cells vertically or horizontally depending on the [specType].
+ * @param cellSize height or width of the cell depending on the [specType].
+ */
+abstract class ResponsiveSpec(
+    open val maxAvailableSize: Int,
+    open val specType: SpecType,
+    open val startPadding: SizeSpec,
+    open val endPadding: SizeSpec,
+    open val gutter: SizeSpec,
+    open val cellSize: SizeSpec
+) {
+    open fun isValid(): Boolean {
+        if (maxAvailableSize <= 0) {
+            Log.e(LOG_TAG, "${this::class.simpleName}#isValid - maxAvailableSize <= 0")
+            return false
+        }
+
+        // All specs need to be individually valid
+        if (!allSpecsAreValid()) {
+            Log.e(LOG_TAG, "${this::class.simpleName}#isValid - !allSpecsAreValid()")
+            return false
+        }
+
+        return true
+    }
+
+    private fun allSpecsAreValid(): Boolean {
+        return startPadding.isValid() &&
+            endPadding.isValid() &&
+            gutter.isValid() &&
+            cellSize.isValid()
+    }
+
+    enum class SpecType {
+        HEIGHT,
+        WIDTH
+    }
+
+    companion object {
+        private const val LOG_TAG = "ResponsiveSpec"
+    }
+}
+
+/**
+ * Calculated responsive specs contains the final paddings, gutter and cell size in pixels after
+ * they are calculated from the available space, cells and workspace specs.
+ */
+sealed class CalculatedResponsiveSpec {
+    var availableSpace: Int = 0
+        private set
+
+    var cells: Int = 0
+        private set
+
+    var startPaddingPx: Int = 0
+        private set
+
+    var endPaddingPx: Int = 0
+        private set
+
+    var gutterPx: Int = 0
+        private set
+
+    var cellSizePx: Int = 0
+        private set
+
+    var spec: ResponsiveSpec
+        private set
+
+    constructor(
+        availableSpace: Int,
+        cells: Int,
+        spec: ResponsiveSpec,
+        calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+    ) {
+        this.availableSpace = availableSpace
+        this.cells = cells
+        this.spec = spec
+
+        // Map if is fixedSize, ofAvailableSpace or matchWorkspace
+        startPaddingPx =
+            spec.startPadding.getCalculatedValue(
+                availableSpace,
+                calculatedWorkspaceSpec.startPaddingPx
+            )
+        endPaddingPx =
+            spec.endPadding.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.endPaddingPx)
+        gutterPx = spec.gutter.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.gutterPx)
+        cellSizePx =
+            spec.cellSize.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.cellSizePx)
+
+        updateRemainderSpaces(availableSpace, cells, spec)
+    }
+
+    constructor(availableSpace: Int, cells: Int, spec: ResponsiveSpec) {
+        this.availableSpace = availableSpace
+        this.cells = cells
+        this.spec = spec
+
+        // Map if is fixedSize or ofAvailableSpace
+        startPaddingPx = spec.startPadding.getCalculatedValue(availableSpace)
+        endPaddingPx = spec.endPadding.getCalculatedValue(availableSpace)
+        gutterPx = spec.gutter.getCalculatedValue(availableSpace)
+        cellSizePx = spec.cellSize.getCalculatedValue(availableSpace)
+
+        updateRemainderSpaces(availableSpace, cells, spec)
+    }
+
+    private fun updateRemainderSpaces(availableSpace: Int, cells: Int, spec: ResponsiveSpec) {
+        val gutters = cells - 1
+        val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
+        val remainderSpace = availableSpace - usedSpace
+
+        startPaddingPx = spec.startPadding.getRemainderSpaceValue(remainderSpace, startPaddingPx)
+        endPaddingPx = spec.endPadding.getRemainderSpaceValue(remainderSpace, endPaddingPx)
+        gutterPx = spec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx)
+        cellSizePx = spec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx)
+    }
+
+    override fun hashCode(): Int {
+        var result = availableSpace.hashCode()
+        result = 31 * result + cells.hashCode()
+        result = 31 * result + startPaddingPx.hashCode()
+        result = 31 * result + endPaddingPx.hashCode()
+        result = 31 * result + gutterPx.hashCode()
+        result = 31 * result + cellSizePx.hashCode()
+        result = 31 * result + spec.hashCode()
+        return result
+    }
+
+    override fun equals(other: Any?): Boolean {
+        return other is CalculatedResponsiveSpec &&
+            availableSpace == other.availableSpace &&
+            cells == other.cells &&
+            startPaddingPx == other.startPaddingPx &&
+            endPaddingPx == other.endPaddingPx &&
+            gutterPx == other.gutterPx &&
+            cellSizePx == other.cellSizePx &&
+            spec == other.spec
+    }
+
+    override fun toString(): String {
+        return "${this::class.simpleName}(" +
+            "availableSpace=$availableSpace, cells=$cells, startPaddingPx=$startPaddingPx, " +
+            "endPaddingPx=$endPaddingPx, gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
+            "${spec::class.simpleName}.maxAvailableSize=${spec.maxAvailableSize}" +
+            ")"
+    }
+}
diff --git a/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt b/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt
new file mode 100644
index 0000000..a89b619
--- /dev/null
+++ b/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.res.TypedArray
+import android.content.res.XmlResourceParser
+import android.util.Xml
+import com.android.launcher3.R
+import com.android.launcher3.util.ResourceHelper
+import java.io.IOException
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserException
+
+class ResponsiveSpecsParser(private val resourceHelper: ResourceHelper) {
+
+    private fun parseSizeSpecs(parser: XmlResourceParser): Map<String, SizeSpec> {
+        val parentName = parser.name
+        parser.next()
+
+        val result = mutableMapOf<String, SizeSpec>()
+        while (parser.eventType != XmlPullParser.END_DOCUMENT && parser.name != parentName) {
+            if (parser.eventType == XmlResourceParser.START_TAG) {
+                result[parser.name] = SizeSpec.create(resourceHelper, Xml.asAttributeSet(parser))
+            }
+            parser.next()
+        }
+
+        return result
+    }
+
+    fun <T> parseXML(
+        tagName: String,
+        map: (attributes: TypedArray, sizeSpecs: Map<String, SizeSpec>) -> T
+    ): List<T> {
+        val parser: XmlResourceParser = resourceHelper.getXml()
+
+        try {
+            val list = mutableListOf<T>()
+
+            var eventType = parser.eventType
+            while (eventType != XmlPullParser.END_DOCUMENT) {
+                if (eventType == XmlResourceParser.START_TAG && parser.name == tagName) {
+                    val attrs =
+                        resourceHelper.obtainStyledAttributes(
+                            Xml.asAttributeSet(parser),
+                            R.styleable.ResponsiveSpec
+                        )
+
+                    val sizeSpecs = parseSizeSpecs(parser)
+                    list += map(attrs, sizeSpecs)
+                    attrs.recycle()
+                }
+
+                eventType = parser.next()
+            }
+
+            parser.close()
+
+            return list
+        } catch (e: Exception) {
+            when (e) {
+                is NoSuchFieldException,
+                is IOException,
+                is XmlPullParserException ->
+                    throw RuntimeException("Failure parsing specs file.", e)
+                else -> throw e
+            }
+        } finally {
+            parser.close()
+        }
+    }
+}
+
+fun Map<String, SizeSpec>.getOrError(key: String): SizeSpec {
+    return this.getOrElse(key) { error("Attr '$key' must be defined.") }
+}
diff --git a/src/com/android/launcher3/responsive/SizeSpec.kt b/src/com/android/launcher3/responsive/SizeSpec.kt
new file mode 100644
index 0000000..c868c9f
--- /dev/null
+++ b/src/com/android/launcher3/responsive/SizeSpec.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import android.util.Log
+import android.util.TypedValue
+import com.android.launcher3.R
+import com.android.launcher3.util.ResourceHelper
+import kotlin.math.roundToInt
+
+/**
+ * [SizeSpec] is an attribute used to represent a property in the responsive grid specs.
+ *
+ * @param fixedSize a fixed size in dp to be used
+ * @param ofAvailableSpace a percentage of the available space
+ * @param ofRemainderSpace a percentage of the remaining space (available space minus used space)
+ * @param matchWorkspace indicates whether the workspace value will be used or not.
+ * @param maxSize restricts the maximum value allowed for the [SizeSpec].
+ */
+data class SizeSpec(
+    val fixedSize: Float = 0f,
+    val ofAvailableSpace: Float = 0f,
+    val ofRemainderSpace: Float = 0f,
+    val matchWorkspace: Boolean = false,
+    val maxSize: Int = Int.MAX_VALUE
+) {
+
+    /** Retrieves the correct value for [SizeSpec]. */
+    fun getCalculatedValue(availableSpace: Int, workspaceValue: Int = 0): Int {
+        val calculatedValue =
+            when {
+                fixedSize > 0 -> fixedSize.roundToInt()
+                matchWorkspace -> workspaceValue
+                ofAvailableSpace > 0 -> (ofAvailableSpace * availableSpace).roundToInt()
+                else -> 0
+            }
+
+        return calculatedValue.coerceAtMost(maxSize)
+    }
+
+    /**
+     * Calculates the [SizeSpec] value when remainder space value is defined. If no remainderSpace
+     * is 0, returns a default value.
+     */
+    fun getRemainderSpaceValue(remainderSpace: Int, defaultValue: Int): Int {
+        val remainderSpaceValue =
+            if (ofRemainderSpace > 0) {
+                (ofRemainderSpace * remainderSpace).roundToInt()
+            } else {
+                defaultValue
+            }
+
+        return remainderSpaceValue.coerceAtMost(maxSize)
+    }
+
+    fun isValid(): Boolean {
+        // All attributes are empty
+        if (fixedSize < 0f && ofAvailableSpace <= 0f && ofRemainderSpace <= 0f && !matchWorkspace) {
+            Log.e(TAG, "SizeSpec#isValid - all attributes are empty")
+            return false
+        }
+
+        // More than one attribute is filled
+        val attrCount =
+            (if (fixedSize > 0) 1 else 0) +
+                (if (ofAvailableSpace > 0) 1 else 0) +
+                (if (ofRemainderSpace > 0) 1 else 0) +
+                (if (matchWorkspace) 1 else 0)
+        if (attrCount > 1) {
+            Log.e(TAG, "SizeSpec#isValid - more than one attribute is filled")
+            return false
+        }
+
+        // Values should be between 0 and 1
+        if (ofAvailableSpace !in 0f..1f || ofRemainderSpace !in 0f..1f) {
+            Log.e(TAG, "SizeSpec#isValid - values should be between 0 and 1")
+            return false
+        }
+
+        // Invalid fixed or max size
+        if (fixedSize < 0f || maxSize < 0f) {
+            Log.e(TAG, "SizeSpec#isValid - values should be bigger or equal to zero.")
+            return false
+        }
+
+        if (fixedSize > 0f && fixedSize > maxSize) {
+            Log.e(TAG, "SizeSpec#isValid - fixed size should be smaller than the max size.")
+            return false
+        }
+
+        return true
+    }
+
+    fun onlyFixedSize(): Boolean {
+        if (ofAvailableSpace > 0 || ofRemainderSpace > 0 || matchWorkspace) {
+            Log.e(TAG, "SizeSpec#onlyFixedSize - only fixed size allowed for this tag")
+            return false
+        }
+        return true
+    }
+
+    object XmlTags {
+        const val START_PADDING = "startPadding"
+        const val END_PADDING = "endPadding"
+        const val GUTTER = "gutter"
+        const val CELL_SIZE = "cellSize"
+        const val HOTSEAT_QSB_SPACE = "hotseatQsbSpace"
+    }
+
+    companion object {
+        private const val TAG = "SizeSpec"
+
+        private fun getValue(a: TypedArray, index: Int): Float {
+            return when (a.getType(index)) {
+                TypedValue.TYPE_DIMENSION -> a.getDimensionPixelSize(index, 0).toFloat()
+                TypedValue.TYPE_FLOAT -> a.getFloat(index, 0f)
+                else -> 0f
+            }
+        }
+
+        fun create(resourceHelper: ResourceHelper, attrs: AttributeSet): SizeSpec {
+            val styledAttrs = resourceHelper.obtainStyledAttributes(attrs, R.styleable.SizeSpec)
+
+            val fixedSize = getValue(styledAttrs, R.styleable.SizeSpec_fixedSize)
+            val ofAvailableSpace = getValue(styledAttrs, R.styleable.SizeSpec_ofAvailableSpace)
+            val ofRemainderSpace = getValue(styledAttrs, R.styleable.SizeSpec_ofRemainderSpace)
+            val matchWorkspace = styledAttrs.getBoolean(R.styleable.SizeSpec_matchWorkspace, false)
+            val maxSize =
+                styledAttrs.getDimensionPixelSize(R.styleable.SizeSpec_maxSize, Int.MAX_VALUE)
+
+            styledAttrs.recycle()
+
+            return SizeSpec(fixedSize, ofAvailableSpace, ofRemainderSpace, matchWorkspace, maxSize)
+        }
+    }
+}
diff --git a/src/com/android/launcher3/responsive/WorkspaceSpecs.kt b/src/com/android/launcher3/responsive/WorkspaceSpecs.kt
new file mode 100644
index 0000000..0da7026
--- /dev/null
+++ b/src/com/android/launcher3/responsive/WorkspaceSpecs.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.res.TypedArray
+import android.util.Log
+import com.android.launcher3.R
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
+import com.android.launcher3.util.ResourceHelper
+
+private const val TAG = "WorkspaceSpecs"
+
+class WorkspaceSpecs(widthSpecs: List<WorkspaceSpec>, heightSpecs: List<WorkspaceSpec>) :
+    ResponsiveSpecs<WorkspaceSpec>(widthSpecs, heightSpecs) {
+
+    fun getCalculatedWidthSpec(columns: Int, availableWidth: Int): CalculatedWorkspaceSpec {
+        val spec = getWidthSpec(availableWidth)
+        return CalculatedWorkspaceSpec(availableWidth, columns, spec)
+    }
+
+    fun getCalculatedHeightSpec(rows: Int, availableHeight: Int): CalculatedWorkspaceSpec {
+        val spec = getHeightSpec(availableHeight)
+        return CalculatedWorkspaceSpec(availableHeight, rows, spec)
+    }
+
+    companion object {
+        private const val XML_WORKSPACE_SPEC = "workspaceSpec"
+
+        @JvmStatic
+        fun create(resourceHelper: ResourceHelper): WorkspaceSpecs {
+            val parser = ResponsiveSpecsParser(resourceHelper)
+            val specs = parser.parseXML(XML_WORKSPACE_SPEC, ::WorkspaceSpec)
+            val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+            return WorkspaceSpecs(widthSpecs, heightSpecs)
+        }
+    }
+}
+
+data class WorkspaceSpec(
+    override val maxAvailableSize: Int,
+    override val specType: SpecType,
+    override val startPadding: SizeSpec,
+    override val endPadding: SizeSpec,
+    override val gutter: SizeSpec,
+    override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
+
+    init {
+        check(isValid()) { "Invalid WorkspaceSpec found." }
+    }
+
+    constructor(
+        attrs: TypedArray,
+        specs: Map<String, SizeSpec>
+    ) : this(
+        maxAvailableSize =
+            attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+        specType =
+            SpecType.values()[
+                    attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+        startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+        endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+        gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+        cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
+    )
+
+    override fun isValid(): Boolean {
+        // Workspace spec should not match workspace
+        if (
+            startPadding.matchWorkspace ||
+                endPadding.matchWorkspace ||
+                gutter.matchWorkspace ||
+                cellSize.matchWorkspace
+        ) {
+            Log.e(TAG, "WorkspaceSpec#isValid - workspace shouldn't contain matchWorkspace!")
+            return false
+        }
+
+        return super.isValid()
+    }
+}
+
+class CalculatedWorkspaceSpec(availableSpace: Int, cells: Int, spec: WorkspaceSpec) :
+    CalculatedResponsiveSpec(availableSpace, cells, spec)
diff --git a/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java b/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java
index f03c62a..2d69bfa 100644
--- a/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java
+++ b/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java
@@ -60,13 +60,15 @@
     private final OnClickListener mOnClickListener;
     private final OnLongClickListener mOnLongClickListener;
     private final SharedPreferences mPrefs;
-    private final AllAppsStore mAllAppsList;
+    private final AllAppsStore<SecondaryDisplayLauncher> mAllAppsList;
     private final AppInfoComparator mAppNameComparator;
 
     private final Set<ComponentKey> mPinnedApps = new HashSet<>();
     private final ArrayList<AppInfo> mItems = new ArrayList<>();
 
-    public PinnedAppsAdapter(SecondaryDisplayLauncher launcher, AllAppsStore allAppsStore,
+    public PinnedAppsAdapter(
+            SecondaryDisplayLauncher launcher,
+            AllAppsStore<SecondaryDisplayLauncher> allAppsStore,
             OnLongClickListener onLongClickListener) {
         mLauncher = launcher;
         mOnClickListener = launcher.getItemOnClickListener();
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index 458f137..a10c0ad 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -72,7 +72,7 @@
         implements BgDataModel.Callbacks, DragController.DragListener {
 
     private LauncherModel mModel;
-    private BaseDragLayer mDragLayer;
+    private SecondaryDragLayer mDragLayer;
     private SecondaryDragController mDragController;
     private ActivityAllAppsContainerView<SecondaryDisplayLauncher> mAppsView;
     private View mAppsButton;
@@ -302,7 +302,7 @@
     public void bindAllApplications(AppInfo[] apps, int flags,
             Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
         Preconditions.assertUIThread();
-        AllAppsStore appsStore = mAppsView.getAppsStore();
+        AllAppsStore<SecondaryDisplayLauncher> appsStore = mAppsView.getAppsStore();
         appsStore.setApps(apps, flags, packageUserKeytoUidMap);
         PopupContainerWithArrow.dismissInvalidPopup(this);
     }
@@ -314,10 +314,6 @@
         }
     }
 
-    public SecondaryDisplayPredictions getSecondaryDisplayPredictions() {
-        return mSecondaryDisplayPredictions;
-    }
-
     @Override
     public StringCache getStringCache() {
         return mStringCache;
@@ -337,6 +333,11 @@
         return this::onIconClicked;
     }
 
+    @Override
+    public View.OnLongClickListener getAllAppsItemLongClickListener() {
+        return v -> mDragLayer.onIconLongClicked(v);
+    }
+
     private void onIconClicked(View v) {
         // Make sure that rogue clicks don't get through while allapps is launching, or after the
         // view has detached (it's possible for this to happen if the view is removed mid touch).
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayPredictions.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayPredictions.java
index 21c50d3..a58916a 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayPredictions.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayPredictions.java
@@ -16,10 +16,8 @@
 package com.android.launcher3.secondarydisplay;
 
 import android.content.Context;
-import android.view.View;
 
 import com.android.launcher3.R;
-import com.android.launcher3.allapps.ActivityAllAppsContainerView;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.util.ResourceBasedOverride;
 
@@ -47,12 +45,4 @@
      */
     public void setPredictedApps(BgDataModel.FixedContainerItems item) {
     }
-
-    /**
-     * Set long click listener for predicted apps in top of app drawer.
-     */
-    public void setLongClickListener(
-            ActivityAllAppsContainerView<?> appsView,
-            View.OnLongClickListener onIconLongClickListener) {
-    }
 }
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
index 87afcab..e8be12c 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
@@ -79,9 +79,6 @@
         mAllAppsButton = findViewById(R.id.all_apps_button);
 
         mAppsView = findViewById(R.id.apps_view);
-        mAppsView.setOnIconLongClickListener(this::onIconLongClicked);
-        mActivity.getSecondaryDisplayPredictions()
-                .setLongClickListener(mAppsView, this::onIconLongClicked);
         // Setup workspace
         mWorkspace = findViewById(R.id.workspace_grid);
         mPinnedAppsAdapter = new PinnedAppsAdapter(mActivity, mAppsView.getAppsStore(),
@@ -179,7 +176,7 @@
         return mPinnedAppsAdapter;
     }
 
-    private boolean onIconLongClicked(View v) {
+    boolean onIconLongClicked(View v) {
         if (!(v instanceof BubbleTextView)) {
             return false;
         }
@@ -243,9 +240,8 @@
                 public void onPreDragStart(DropTarget.DragObject dragObject) {
                     mDragView = dragObject.dragView;
                     if (!shouldStartDrag(0)) {
-                        mDragView.setOnAnimationEndCallback(() -> {
-                            mActivity.beginDragShared(v, mActivity.getAppsView(), options);
-                        });
+                        mDragView.setOnScaleAnimEndCallback(() ->
+                                mActivity.beginDragShared(v, mActivity.getAppsView(), options));
                     }
                 }
 
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index 198dad3..360ff7e 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -27,6 +27,7 @@
 import android.animation.AnimatorSet;
 import android.os.Handler;
 import android.os.Looper;
+import android.util.Log;
 
 import androidx.annotation.FloatRange;
 
@@ -35,6 +36,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
+import com.android.launcher3.testing.shared.TestProtocol;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -76,6 +78,10 @@
         return mState;
     }
 
+    public STATE_TYPE getTargetState() {
+        return (STATE_TYPE) mConfig.targetState;
+    }
+
     public STATE_TYPE getCurrentStableState() {
         return mCurrentStableState;
     }
@@ -221,6 +227,8 @@
 
     private void goToState(
             STATE_TYPE state, boolean animated, long delay, AnimatorListener listener) {
+        Log.d(TestProtocol.OVERVIEW_OVER_HOME, "go to state " + state);
+
         animated &= areAnimatorsEnabled();
         if (mActivity.isInState(state)) {
             if (mConfig.currentAnimation == null) {
@@ -375,6 +383,8 @@
         mState = state;
         mActivity.onStateSetStart(mState);
 
+        Log.d(TestProtocol.OVERVIEW_OVER_HOME, "Notifying listeners for state transition start"
+                + " to state: " + state.toString());
         for (int i = mListeners.size() - 1; i >= 0; i--) {
             mListeners.get(i).onStateTransitionStart(state);
         }
@@ -392,6 +402,8 @@
             setRestState(null);
         }
 
+        Log.d(TestProtocol.OVERVIEW_OVER_HOME, "Notifying " + mListeners.size() + " listeners "
+                + "for end transition for state: " + state.toString());
         for (int i = mListeners.size() - 1; i >= 0; i--) {
             mListeners.get(i).onStateTransitionComplete(state);
         }
@@ -429,6 +441,7 @@
      * Cancels the current animation.
      */
     public void cancelAnimation() {
+        Log.d(TestProtocol.OVERVIEW_OVER_HOME, "current animation cancelled");
         mConfig.reset();
         // It could happen that a new animation is set as a result of an endListener on the
         // existing animation.
@@ -452,6 +465,7 @@
      * @param toState The state we are animating towards.
      */
     public void setCurrentAnimation(AnimatorSet anim, STATE_TYPE toState) {
+        Log.d(TestProtocol.OVERVIEW_OVER_HOME, "setting animation to " + toState.toString());
         cancelAnimation();
         setCurrentAnimation(anim);
         anim.addListener(createStateAnimationListener(toState));
diff --git a/src/com/android/launcher3/states/StateAnimationConfig.java b/src/com/android/launcher3/states/StateAnimationConfig.java
index d1e816b..0d9e010 100644
--- a/src/com/android/launcher3/states/StateAnimationConfig.java
+++ b/src/com/android/launcher3/states/StateAnimationConfig.java
@@ -66,7 +66,8 @@
             ANIM_WORKSPACE_PAGE_TRANSLATE_X,
             ANIM_OVERVIEW_SPLIT_SELECT_FLOATING_TASK_TRANSLATE_OFFSCREEN,
             ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE,
-            ANIM_ALL_APPS_BOTTOM_SHEET_FADE
+            ANIM_ALL_APPS_BOTTOM_SHEET_FADE,
+            ANIM_ALL_APPS_KEYBOARD_FADE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AnimType {}
@@ -90,8 +91,9 @@
     public static final int ANIM_OVERVIEW_SPLIT_SELECT_FLOATING_TASK_TRANSLATE_OFFSCREEN = 17;
     public static final int ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE = 18;
     public static final int ANIM_ALL_APPS_BOTTOM_SHEET_FADE = 19;
+    public static final int ANIM_ALL_APPS_KEYBOARD_FADE = 20;
 
-    private static final int ANIM_TYPES_COUNT = 20;
+    private static final int ANIM_TYPES_COUNT = 21;
 
     protected final Interpolator[] mInterpolators = new Interpolator[ANIM_TYPES_COUNT];
 
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index e62ccbc..07a1b82 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.testing;
 
 import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_GRID_ONLY_OVERVIEW;
 import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
@@ -203,10 +204,11 @@
             }
 
             case TestProtocol.REQUEST_WORKSPACE_COLUMNS_ROWS: {
+                InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mContext);
                 return getLauncherUIProperty(Bundle::putParcelable, launcher -> new Point(
-                        InvariantDeviceProfile.INSTANCE.get(mContext).numColumns,
-                        InvariantDeviceProfile.INSTANCE.get(mContext).numRows)
-                );
+                        idp.getDeviceProfile(mContext).getPanelCount() * idp.numColumns,
+                        idp.numRows
+                ));
             }
 
             case TestProtocol.REQUEST_WORKSPACE_CURRENT_PAGE_INDEX: {
@@ -229,7 +231,7 @@
             }
 
             case TestProtocol.REQUEST_HAS_TIS: {
-                response.putBoolean(TestProtocol.REQUEST_HAS_TIS, false);
+                response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, false);
                 return response;
             }
 
@@ -245,6 +247,12 @@
                                 + l.getAppsView().getActiveRecyclerView().getPaddingBottom());
             }
 
+            case TestProtocol.REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW: {
+                response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                        ENABLE_GRID_ONLY_OVERVIEW.get());
+                return response;
+            }
+
             default:
                 return null;
         }
diff --git a/src/com/android/launcher3/testing/TestLogging.java b/src/com/android/launcher3/testing/TestLogging.java
index f95548d..70691f8 100644
--- a/src/com/android/launcher3/testing/TestLogging.java
+++ b/src/com/android/launcher3/testing/TestLogging.java
@@ -27,26 +27,29 @@
 import java.util.function.BiConsumer;
 
 public final class TestLogging {
+    private static final String TAPL_EVENTS_TAG = "TaplEvents";
+    private static final String LAUNCHER_EVENTS_TAG = "LauncherEvents";
     private static BiConsumer<String, String> sEventConsumer;
     public static boolean sHadEventsNotFromTest;
 
-    private static void recordEventSlow(String sequence, String event) {
-        Log.d(TestProtocol.TAPL_EVENTS_TAG, sequence + " / " + event);
+    private static void recordEventSlow(String sequence, String event, boolean reportToTapl) {
+        Log.d(reportToTapl ? TAPL_EVENTS_TAG : LAUNCHER_EVENTS_TAG,
+                sequence + " / " + event);
         final BiConsumer<String, String> eventConsumer = sEventConsumer;
-        if (eventConsumer != null) {
+        if (reportToTapl && eventConsumer != null) {
             eventConsumer.accept(sequence, event);
         }
     }
 
     public static void recordEvent(String sequence, String event) {
         if (Utilities.isRunningInTestHarness()) {
-            recordEventSlow(sequence, event);
+            recordEventSlow(sequence, event, true);
         }
     }
 
     public static void recordEvent(String sequence, String message, Object parameter) {
         if (Utilities.isRunningInTestHarness()) {
-            recordEventSlow(sequence, message + ": " + parameter);
+            recordEventSlow(sequence, message + ": " + parameter, true);
         }
     }
 
@@ -59,14 +62,20 @@
 
     public static void recordKeyEvent(String sequence, String message, KeyEvent event) {
         if (Utilities.isRunningInTestHarness()) {
-            recordEventSlow(sequence, message + ": " + event);
+            recordEventSlow(sequence, message + ": " + event, true);
             registerEventNotFromTest(event);
         }
     }
 
     public static void recordMotionEvent(String sequence, String message, MotionEvent event) {
-        if (Utilities.isRunningInTestHarness() && event.getAction() != MotionEvent.ACTION_MOVE) {
-            recordEventSlow(sequence, message + ": " + event);
+        final int action = event.getAction();
+        if (Utilities.isRunningInTestHarness() && action != MotionEvent.ACTION_MOVE) {
+            // "Expecting" in TAPL motion events was thought to be producing considerable noise in
+            // tests due to failed checks for expected events. So we are not sending them to TAPL.
+            // Other events, such as EVENT_PILFER_POINTERS produce less noise and are thought to
+            // be more useful.
+            // That's why we pass false as the value for the 'reportToTapl' parameter.
+            recordEventSlow(sequence, message + ": " + event, false);
             registerEventNotFromTest(event);
         }
     }
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index c499e35..cec4574 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.touch;
 
+import static com.android.app.animation.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.LauncherAnimUtils.TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
@@ -22,7 +23,6 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
-import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
@@ -384,8 +384,8 @@
         } else {
             logReachedState(targetState);
         }
-        mLauncher.getRootView().getSysUiScrim().createSysuiMultiplierAnim(
-                1f).setDuration(0).start();
+        mLauncher.getRootView().getSysUiScrim().getSysUIMultiplier().animateToValue(1f)
+                .setDuration(0).start();
     }
 
     private void logReachedState(LauncherState targetState) {
diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java
index d028f24..447d22b 100644
--- a/src/com/android/launcher3/touch/AllAppsSwipeController.java
+++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java
@@ -15,17 +15,20 @@
  */
 package com.android.launcher3.touch;
 
+import static com.android.app.animation.Interpolators.DECELERATED_EASE;
+import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE;
+import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
+import static com.android.app.animation.Interpolators.FINAL_FRAME;
+import static com.android.app.animation.Interpolators.INSTANT;
+import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.clampToProgress;
+import static com.android.app.animation.Interpolators.mapToProgress;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED;
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED_ACCELERATE;
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED_DECELERATE;
-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.clampToProgress;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_BOTTOM_SHEET_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_KEYBOARD_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_SCALE;
@@ -40,11 +43,11 @@
 import android.view.MotionEvent;
 import android.view.animation.Interpolator;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.states.StateAnimationConfig;
 
 /**
@@ -209,8 +212,8 @@
             if (!config.userControlled) {
                 config.setInterpolator(ANIM_VERTICAL_PROGRESS, EMPHASIZED);
             }
-            config.setInterpolator(ANIM_WORKSPACE_SCALE, EMPHASIZED);
-            config.setInterpolator(ANIM_DEPTH, EMPHASIZED);
+            config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE);
+            config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE);
         } else {
             if (config.userControlled) {
                 config.setInterpolator(ANIM_DEPTH, Interpolators.reverse(BLUR_MANUAL));
@@ -250,8 +253,8 @@
             if (!config.userControlled) {
                 config.setInterpolator(ANIM_VERTICAL_PROGRESS, EMPHASIZED);
             }
-            config.setInterpolator(ANIM_WORKSPACE_SCALE, EMPHASIZED);
-            config.setInterpolator(ANIM_DEPTH, EMPHASIZED);
+            config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE);
+            config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE);
         } else {
             config.setInterpolator(ANIM_DEPTH, config.userControlled ? BLUR_MANUAL : BLUR_ATOMIC);
             config.setInterpolator(ANIM_WORKSPACE_FADE,
@@ -293,20 +296,15 @@
             config.setInterpolator(ANIM_WORKSPACE_SCALE, INSTANT);
             config.setInterpolator(ANIM_WORKSPACE_TRANSLATE, INSTANT);
         } else {
-            // Remove scrim for this transition.
-            config.setInterpolator(ANIM_SCRIM_FADE, progress -> 0);
-
-            // For now, pop the background panel in at full opacity at the threshold.
+            // Pop the background panel, keyboard, and content in at full opacity at the threshold.
             config.setInterpolator(ANIM_ALL_APPS_BOTTOM_SHEET_FADE,
                     thresholdInterpolator(threshold, INSTANT));
-
-            // Fade the apps in when the scrim normally does, so it's apparent sooner what is
-            // happening (in this case we are fading them on top of the background panel).
-            config.setInterpolator(ANIM_ALL_APPS_FADE,
-                    thresholdInterpolator(threshold, SCRIM_FADE_MANUAL));
+            config.setInterpolator(ANIM_ALL_APPS_KEYBOARD_FADE,
+                    thresholdInterpolator(threshold, INSTANT));
+            config.setInterpolator(ANIM_ALL_APPS_FADE, thresholdInterpolator(threshold, INSTANT));
 
             config.setInterpolator(ANIM_VERTICAL_PROGRESS,
-                    thresholdInterpolator(threshold, ALL_APPS_VERTICAL_PROGRESS_MANUAL));
+                    thresholdInterpolator(threshold, mapToProgress(LINEAR, threshold, 1f)));
         }
     }
 
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index 790c226..8c12547 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -42,6 +42,7 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.logging.InstanceId;
@@ -95,6 +96,8 @@
         } else if (tag instanceof FolderInfo) {
             if (v instanceof FolderIcon) {
                 onClickFolderIcon(v);
+            } else if (v instanceof AppPairIcon) {
+                onClickAppPairIcon(v);
             }
         } else if (tag instanceof AppInfo) {
             startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher);
@@ -123,6 +126,17 @@
     }
 
     /**
+     * Event handler for an app pair icon click.
+     *
+     * @param v The view that was clicked. Must be an instance of {@link AppPairIcon}.
+     */
+    private static void onClickAppPairIcon(View v) {
+        Launcher launcher = Launcher.getLauncher(v.getContext());
+        FolderInfo folderInfo = ((AppPairIcon) v).getInfo();
+        launcher.launchAppPair(folderInfo.contents.get(0), folderInfo.contents.get(1));
+    }
+
+    /**
      * Event handler for the app widget view which has not fully restored.
      */
     private static void onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher) {
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index 9cba19d..122b1e0 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -67,7 +67,7 @@
         if (!(v.getTag() instanceof ItemInfo)) return false;
 
         launcher.setWaitingForResult(null);
-        beginDrag(v, launcher, (ItemInfo) v.getTag(), launcher.getDefaultWorkspaceDragOptions());
+        beginDrag(v, launcher, (ItemInfo) v.getTag(), new DragOptions());
         return true;
     }
 
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index 6a972eb..dc4621e 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -582,7 +582,8 @@
                 ? splitInfo.dividerHeightPercent
                 : splitInfo.dividerWidthPercent;
 
-        float scale = (float) outRect.height() / dp.availableHeightPx;
+        int taskbarHeight = dp.isTransientTaskbar ? 0 : dp.taskbarHeight;
+        float scale = (float) outRect.height() / (dp.availableHeightPx - taskbarHeight);
         float topTaskHeight = dp.availableHeightPx * topLeftTaskPercent;
         float scaledTopTaskHeight = topTaskHeight * scale;
         float dividerHeight = dp.availableHeightPx * dividerBarPercent;
@@ -639,7 +640,8 @@
             // Reset unused translations
             primarySnapshot.setTranslationY(0);
         } else {
-            float scale = (float) totalThumbnailHeight / dp.availableHeightPx;
+            int taskbarHeight = dp.isTransientTaskbar ? 0 : dp.taskbarHeight;
+            float scale = (float) totalThumbnailHeight / (dp.availableHeightPx - taskbarHeight);
             float topTaskHeight = dp.availableHeightPx * taskPercent;
             float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale);
             float scaledTopTaskHeight = topTaskHeight * scale;
diff --git a/src/com/android/launcher3/util/CannedAnimationCoordinator.kt b/src/com/android/launcher3/util/CannedAnimationCoordinator.kt
new file mode 100644
index 0000000..18f8339
--- /dev/null
+++ b/src/com/android/launcher3/util/CannedAnimationCoordinator.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.launcher3.util
+
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.util.Log
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
+import androidx.core.view.OneShotPreDrawListener
+import com.android.app.animation.Interpolators.LINEAR
+import com.android.launcher3.anim.AnimatorListeners
+import com.android.launcher3.anim.AnimatorPlaybackController
+import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.statemanager.StatefulActivity
+import java.util.function.Consumer
+
+private const val TAG = "CannedAnimCoordinator"
+
+/**
+ * Utility class to run a canned animation on Launcher.
+ *
+ * This class takes care to registering animations with stateManager and ensures that only one
+ * animation is playing at a time.
+ */
+class CannedAnimationCoordinator(private val activity: StatefulActivity<*>) {
+
+    private val launcherLayoutListener = OnGlobalLayoutListener { scheduleRecreateAnimOnPreDraw() }
+    private var recreatePending = false
+
+    private var animationProvider: Any? = null
+
+    private var animationDuration: Long = 0L
+    private var animationFactory: Consumer<PendingAnimation>? = null
+    private var animationController: AnimatorPlaybackController? = null
+
+    private var currentAnim: AnimatorPlaybackController? = null
+
+    /**
+     * Sets the current animation cancelling any previously set animation.
+     *
+     * Callers can control the animation using {@link #getPlaybackController}. The state is
+     * automatically cleared when the playback controller ends. The animation is automatically
+     * recreated when any layout change happens. Callers can also ask for recreation by calling
+     * {@link #recreateAnimation}
+     */
+    fun setAnimation(provider: Any, factory: Consumer<PendingAnimation>, duration: Long) {
+        if (provider != animationProvider) {
+            Log.e(TAG, "Trying to play two animations together, $provider and $animationProvider")
+        }
+
+        // Cancel any previously running animation
+        endCurrentAnimation(false)
+        animationController?.dispatchOnCancel()?.dispatchOnEnd()
+
+        animationProvider = provider
+        animationFactory = factory
+        animationDuration = duration
+
+        // Setup a new controller and link it with launcher state animation
+        val anim = AnimatorSet()
+        anim.play(
+            ValueAnimator.ofFloat(0f, 1f).apply {
+                interpolator = LINEAR
+                this.duration = duration
+                addUpdateListener { anim -> currentAnim?.setPlayFraction(anim.animatedFraction) }
+            }
+        )
+        val controller = AnimatorPlaybackController.wrap(anim, duration)
+        anim.addListener(
+            AnimatorListeners.forEndCallback { success ->
+                if (animationController != controller) {
+                    return@forEndCallback
+                }
+
+                endCurrentAnimation(success)
+                animationController = null
+                animationFactory = null
+                animationProvider = null
+
+                activity.rootView.viewTreeObserver.apply {
+                    if (isAlive) {
+                        removeOnGlobalLayoutListener(launcherLayoutListener)
+                    }
+                }
+            }
+        )
+
+        // Recreate animation whenever layout happens in case transforms change during layout
+        activity.rootView.viewTreeObserver.apply {
+            if (isAlive) {
+                addOnGlobalLayoutListener(launcherLayoutListener)
+            }
+        }
+        // Link this to the state manager so that it auto-cancels when state changes
+        recreatePending = false
+        animationController =
+            controller.apply { activity.stateManager.setCurrentUserControlledAnimation(this) }
+        recreateAnimation(provider)
+    }
+
+    private fun endCurrentAnimation(success: Boolean) {
+        currentAnim?.apply {
+            // When cancelling an animation, apply final progress so that all transformations
+            // are restored
+            setPlayFraction(1f)
+            if (!success) dispatchOnCancel()
+            dispatchOnEnd()
+        }
+        currentAnim = null
+    }
+
+    /** Returns the current animation controller to control the animation */
+    fun getPlaybackController(provider: Any): AnimatorPlaybackController? {
+        return if (provider == animationProvider) animationController
+        else {
+            Log.d(TAG, "Wrong controller access from $provider, actual provider $animationProvider")
+            null
+        }
+    }
+
+    private fun scheduleRecreateAnimOnPreDraw() {
+        if (!recreatePending) {
+            recreatePending = true
+            OneShotPreDrawListener.add(activity.rootView) {
+                if (recreatePending) {
+                    recreatePending = false
+                    animationProvider?.apply { recreateAnimation(this) }
+                }
+            }
+        }
+    }
+
+    /** Notify the controller to recreate the animation. The animation progress is preserved */
+    fun recreateAnimation(provider: Any) {
+        if (provider != animationProvider) {
+            Log.e(TAG, "Ignore recreate request from $provider, actual provider $animationProvider")
+            return
+        }
+        endCurrentAnimation(false /* success */)
+
+        if (animationFactory == null || animationController == null) {
+            return
+        }
+        currentAnim =
+            PendingAnimation(animationDuration)
+                .apply { animationFactory?.accept(this) }
+                .createPlaybackController()
+                .apply { setPlayFraction(animationController!!.progressFraction) }
+    }
+}
diff --git a/src/com/android/launcher3/util/DimensionUtils.kt b/src/com/android/launcher3/util/DimensionUtils.kt
index 9188c2e..0eb0e08 100644
--- a/src/com/android/launcher3/util/DimensionUtils.kt
+++ b/src/com/android/launcher3/util/DimensionUtils.kt
@@ -29,9 +29,9 @@
      */
     @JvmStatic
     fun getTaskbarPhoneDimensions(
-        deviceProfile: DeviceProfile,
-        res: Resources,
-        isPhoneMode: Boolean
+            deviceProfile: DeviceProfile,
+            res: Resources,
+            isPhoneMode: Boolean
     ): Point {
         val p = Point()
         // Taskbar for large screen
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 6647d0d..a7c94bb 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -54,8 +54,8 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -106,7 +106,8 @@
 
     private final LauncherPrefs mPrefs;
 
-    private DisplayController(Context context) {
+    @VisibleForTesting
+    protected DisplayController(Context context) {
         mContext = context;
         mDM = context.getSystemService(DisplayManager.class);
         mPrefs = LauncherPrefs.get(context);
@@ -127,8 +128,7 @@
         Context displayInfoContext = getDisplayInfoContext(display);
         mInfo = new Info(displayInfoContext, wmProxy,
                 wmProxy.estimateInternalDisplayBounds(displayInfoContext));
-        mInfo.mPerDisplayBounds.forEach((key, value) -> FileLog.i(TAG,
-                "(CTOR) perDisplayBounds - " + key + ": " + Arrays.deepToString(value)));
+        FileLog.i(TAG, "(CTOR) perDisplayBounds: " + mInfo.mPerDisplayBounds);
     }
 
     /**
@@ -286,9 +286,8 @@
         if (!newInfo.supportedBounds.equals(oldInfo.supportedBounds)
                 || !newInfo.mPerDisplayBounds.equals(oldInfo.mPerDisplayBounds)) {
             change |= CHANGE_SUPPORTED_BOUNDS;
-            newInfo.mPerDisplayBounds.forEach((key, value) -> FileLog.w(TAG,
-                    "(CHANGE_SUPPORTED_BOUNDS) perDisplayBounds - " + key + ": "
-                            + Arrays.deepToString(value)));
+            FileLog.w(TAG,
+                    "(CHANGE_SUPPORTED_BOUNDS) perDisplayBounds: " + newInfo.mPerDisplayBounds);
         }
         if (DEBUG) {
             Log.d(TAG, "handleInfoChange - change: " + getChangeFlagsString(change));
@@ -329,7 +328,7 @@
         // WindowBounds
         public final WindowBounds realBounds;
         public final Set<WindowBounds> supportedBounds = new ArraySet<>();
-        private final ArrayMap<CachedDisplayInfo, WindowBounds[]> mPerDisplayBounds =
+        private final ArrayMap<CachedDisplayInfo, List<WindowBounds>> mPerDisplayBounds =
                 new ArrayMap<>();
 
         public Info(Context displayInfoContext) {
@@ -340,7 +339,7 @@
         // Used for testing
         public Info(Context displayInfoContext,
                 WindowManagerProxy wmProxy,
-                Map<CachedDisplayInfo, WindowBounds[]> perDisplayBoundsCache) {
+                Map<CachedDisplayInfo, List<WindowBounds>> perDisplayBoundsCache) {
             CachedDisplayInfo displayInfo = wmProxy.getDisplayInfo(displayInfoContext);
             normalizedDisplayInfo = displayInfo.normalize();
             rotation = displayInfo.rotation;
@@ -354,16 +353,14 @@
             navigationMode = wmProxy.getNavigationMode(displayInfoContext);
 
             mPerDisplayBounds.putAll(perDisplayBoundsCache);
-            WindowBounds[] cachedValue = mPerDisplayBounds.get(normalizedDisplayInfo);
+            List<WindowBounds> cachedValue = mPerDisplayBounds.get(normalizedDisplayInfo);
 
             realBounds = wmProxy.getRealBounds(displayInfoContext, displayInfo);
             if (cachedValue == null) {
                 // Unexpected normalizedDisplayInfo is found, recreate the cache
                 FileLog.e(TAG, "Unexpected normalizedDisplayInfo found, invalidating cache: "
                         + normalizedDisplayInfo);
-                mPerDisplayBounds.forEach((key, value) -> FileLog.e(TAG,
-                        "(Invalid Cache) perDisplayBounds - " + key + ": " + Arrays.deepToString(
-                                value)));
+                FileLog.e(TAG, "(Invalid Cache) perDisplayBounds : " + mPerDisplayBounds);
                 mPerDisplayBounds.clear();
                 mPerDisplayBounds.putAll(wmProxy.estimateInternalDisplayBounds(displayInfoContext));
                 cachedValue = mPerDisplayBounds.get(normalizedDisplayInfo);
@@ -376,22 +373,19 @@
 
             if (cachedValue != null) {
                 // Verify that the real bounds are a match
-                WindowBounds expectedBounds = cachedValue[displayInfo.rotation];
+                WindowBounds expectedBounds = cachedValue.get(displayInfo.rotation);
                 if (!realBounds.equals(expectedBounds)) {
-                    WindowBounds[] clone = new WindowBounds[4];
-                    System.arraycopy(cachedValue, 0, clone, 0, 4);
-                    clone[displayInfo.rotation] = realBounds;
+                    List<WindowBounds> clone = new ArrayList<>(cachedValue);
+                    clone.set(displayInfo.rotation, realBounds);
                     mPerDisplayBounds.put(normalizedDisplayInfo, clone);
                 }
             }
-            mPerDisplayBounds.values().forEach(
-                    windowBounds -> Collections.addAll(supportedBounds, windowBounds));
+            mPerDisplayBounds.values().forEach(supportedBounds::addAll);
             if (DEBUG) {
                 Log.d(TAG, "displayInfo: " + displayInfo);
                 Log.d(TAG, "realBounds: " + realBounds);
                 Log.d(TAG, "normalizedDisplayInfo: " + normalizedDisplayInfo);
-                mPerDisplayBounds.forEach((key, value) -> Log.d(TAG,
-                        "perDisplayBounds - " + key + ": " + Arrays.deepToString(value)));
+                Log.d(TAG, "perDisplayBounds: " + mPerDisplayBounds);
             }
         }
 
@@ -448,7 +442,7 @@
         pw.println("  navigationMode=" + info.navigationMode.name());
         pw.println("  currentSize=" + info.currentSize);
         info.mPerDisplayBounds.forEach((key, value) -> pw.println(
-                "  perDisplayBounds - " + key + ": " + Arrays.deepToString(value)));
+                "  perDisplayBounds - " + key + ": " + value));
     }
 
     /**
diff --git a/src/com/android/launcher3/util/Executors.java b/src/com/android/launcher3/util/Executors.java
index 6978e0c..dec4b5c 100644
--- a/src/com/android/launcher3/util/Executors.java
+++ b/src/com/android/launcher3/util/Executors.java
@@ -21,6 +21,7 @@
 
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
@@ -58,6 +59,11 @@
             new LooperExecutor(
                     createAndStartNewLooper("UiThreadHelper", Process.THREAD_PRIORITY_FOREGROUND));
 
+
+    /** A background executor to preinflate views. */
+    public static final ExecutorService VIEW_PREINFLATION_EXECUTOR =
+            java.util.concurrent.Executors.newSingleThreadExecutor();
+
     /**
      * Utility method to get a started handler thread statically
      */
diff --git a/src/com/android/launcher3/util/IconSizeSteps.kt b/src/com/android/launcher3/util/IconSizeSteps.kt
new file mode 100644
index 0000000..2a5afe0
--- /dev/null
+++ b/src/com/android/launcher3/util/IconSizeSteps.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.res.Resources
+import androidx.core.content.res.getDimensionOrThrow
+import androidx.core.content.res.use
+import com.android.launcher3.R
+import kotlin.math.max
+
+class IconSizeSteps(res: Resources) {
+    private val steps: List<Int>
+
+    init {
+        steps =
+            res.obtainTypedArray(R.array.icon_size_steps).use {
+                (0 until it.length()).map { step -> it.getDimensionOrThrow(step).toInt() }.sorted()
+            }
+    }
+
+    fun minimumIconSize(): Int = steps[0]
+
+    fun getNextLowerIconSize(iconSizePx: Int): Int {
+        return steps[max(0, getIndexForIconSize(iconSizePx) - 1)]
+    }
+
+    fun getIconSmallerThan(cellWidth: Int): Int {
+        return steps.lastOrNull { it <= cellWidth } ?: steps[0]
+    }
+
+    private fun getIndexForIconSize(iconSizePx: Int): Int {
+        return max(0, steps.indexOfFirst { iconSizePx <= it })
+    }
+}
diff --git a/src/com/android/launcher3/util/InstantAppResolver.java b/src/com/android/launcher3/util/InstantAppResolver.java
index 6f706d2..bdb5e77 100644
--- a/src/com/android/launcher3/util/InstantAppResolver.java
+++ b/src/com/android/launcher3/util/InstantAppResolver.java
@@ -42,14 +42,7 @@
         return false;
     }
 
-    public boolean isInstantApp(Context context, String packageName) {
-        PackageManager packageManager = context.getPackageManager();
-        try {
-            return isInstantApp(packageManager.getPackageInfo(packageName, 0).applicationInfo);
-        } catch (PackageManager.NameNotFoundException e) {
-            Log.e("InstantAppResolver", "Failed to determine whether package is instant app "
-                    + packageName, e);
-        }
+    public boolean isInstantApp(String packageName, int userId) {
         return false;
     }
 }
diff --git a/src/com/android/launcher3/util/IntArray.java b/src/com/android/launcher3/util/IntArray.java
index 1c78795..2498242 100644
--- a/src/com/android/launcher3/util/IntArray.java
+++ b/src/com/android/launcher3/util/IntArray.java
@@ -250,6 +250,11 @@
         return b.toString();
     }
 
+    @Override
+    public String toString() {
+        return "IntArray [" + toConcatString() + "]";
+    }
+
     public static IntArray fromConcatString(String concatString) {
         StringTokenizer tokenizer = new StringTokenizer(concatString, ",");
         int[] array = new int[tokenizer.countTokens()];
diff --git a/src/com/android/launcher3/util/LockedUserState.kt b/src/com/android/launcher3/util/LockedUserState.kt
index 1231604..0a87594 100644
--- a/src/com/android/launcher3/util/LockedUserState.kt
+++ b/src/com/android/launcher3/util/LockedUserState.kt
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.launcher3.util
 
 import android.content.Context
diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java
index 6a4e528..1cb9994 100644
--- a/src/com/android/launcher3/util/MainThreadInitializedObject.java
+++ b/src/com/android/launcher3/util/MainThreadInitializedObject.java
@@ -48,8 +48,8 @@
     }
 
     public T get(Context context) {
-        if (context instanceof SandboxContext) {
-            return ((SandboxContext) context).getObject(this, mProvider);
+        if (context instanceof SandboxContext sc) {
+            return sc.getObject(this);
         }
 
         if (mValue == null) {
@@ -131,23 +131,22 @@
          * Find a cached object from mObjectMap if we have already created one. If not, generate
          * an object using the provider.
          */
-        private <T> T getObject(MainThreadInitializedObject<T> object, ObjectProvider<T> provider) {
+        protected <T> T getObject(MainThreadInitializedObject<T> object) {
             synchronized (mDestroyLock) {
                 if (mDestroyed) {
                     Log.e(TAG, "Static object access with a destroyed context");
                 }
-
                 T t = (T) mObjectMap.get(object);
                 if (t != null) {
                     return t;
                 }
                 if (Looper.myLooper() == Looper.getMainLooper()) {
-                    t = createObject(provider);
+                    t = createObject(object);
                     // Check if we've explicitly allowed the object or if it's a SafeCloseable,
                     // it will get destroyed in onDestroy()
                     if (!mAllowedObjects.contains(object) && !(t instanceof SafeCloseable)) {
-                        throw new IllegalStateException(
-                                "Leaking unknown objects " + object + "  " + provider + " " + t);
+                        throw new IllegalStateException("Leaking unknown objects "
+                                + object + "  " + object.mProvider + " " + t);
                     }
                     mObjectMap.put(object, t);
                     mOrderedObjects.add(t);
@@ -156,15 +155,15 @@
             }
 
             try {
-                return MAIN_EXECUTOR.submit(() -> getObject(object, provider)).get();
+                return MAIN_EXECUTOR.submit(() -> getObject(object)).get();
             } catch (InterruptedException | ExecutionException e) {
                 throw new RuntimeException(e);
             }
         }
 
         @UiThread
-        protected <T> T createObject(ObjectProvider<T> provider) {
-            return provider.get(this);
+        protected <T> T createObject(MainThreadInitializedObject<T> object) {
+            return object.mProvider.get(this);
         }
     }
 }
diff --git a/src/com/android/launcher3/util/MultiScalePropertyFactory.java b/src/com/android/launcher3/util/MultiScalePropertyFactory.java
index a7e6cc8..cf8d6cc 100644
--- a/src/com/android/launcher3/util/MultiScalePropertyFactory.java
+++ b/src/com/android/launcher3/util/MultiScalePropertyFactory.java
@@ -40,8 +40,7 @@
     private static final boolean DEBUG = false;
     private static final String TAG = "MultiScaleProperty";
     private final String mName;
-    private final ArrayMap<Integer, MultiScaleProperty> mProperties =
-            new ArrayMap<Integer, MultiScaleProperty>();
+    private final ArrayMap<Integer, MultiScaleProperty> mProperties = new ArrayMap<>();
 
     // This is an optimization for cases when set is called repeatedly with the same setterIndex.
     private float mMinOfOthers = 0;
@@ -55,7 +54,7 @@
     }
 
     /** Returns the [MultiFloatProperty] associated with [inx], creating it if not present. */
-    public MultiScaleProperty get(Integer index) {
+    public FloatProperty<T> get(Integer index) {
         return mProperties.computeIfAbsent(index,
                 (k) -> new MultiScaleProperty(index, mName + "_" + index));
     }
diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java
index ac016a8..a66a9d2 100644
--- a/src/com/android/launcher3/util/MultiValueAlpha.java
+++ b/src/com/android/launcher3/util/MultiValueAlpha.java
@@ -32,8 +32,15 @@
     // Whether we should change from INVISIBLE to VISIBLE and vice versa at low alpha values.
     private boolean mUpdateVisibility;
 
+    private final int mHiddenVisibility;
+
     public MultiValueAlpha(View view, int size) {
+        this(view, size, View.INVISIBLE);
+    }
+
+    public MultiValueAlpha(View view, int size, int hiddenVisibility) {
         super(view, VIEW_ALPHA, size, ALPHA_AGGREGATOR, 1f);
+        this.mHiddenVisibility = hiddenVisibility;
     }
 
     /** Sets whether we should update between INVISIBLE and VISIBLE based on alpha. */
@@ -45,7 +52,7 @@
     protected void apply(float value) {
         super.apply(value);
         if (mUpdateVisibility) {
-            AlphaUpdateListener.updateVisibility(mTarget);
+            AlphaUpdateListener.updateVisibility(mTarget, mHiddenVisibility);
         }
     }
 }
diff --git a/src/com/android/launcher3/util/OnboardingPrefs.java b/src/com/android/launcher3/util/OnboardingPrefs.java
index 348c8d8..f8f4b5f 100644
--- a/src/com/android/launcher3/util/OnboardingPrefs.java
+++ b/src/com/android/launcher3/util/OnboardingPrefs.java
@@ -38,8 +38,6 @@
     public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_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";
-    public static final String SEARCH_KEYBOARD_EDU_SEEN = "launcher.search_edu_seen";
-    public static final String SEARCH_SNACKBAR_COUNT = "launcher.keyboard_snackbar_count";
     public static final String ALL_APPS_VISITED_COUNT = "launcher.all_apps_visited_count";
     public static final String TASKBAR_EDU_TOOLTIP_STEP = "launcher.taskbar_edu_tooltip_step";
     // When adding a new key, add it here as well, to be able to reset it from Developer Options.
@@ -47,7 +45,6 @@
             "All Apps Bounce", new String[] { HOME_BOUNCE_SEEN, HOME_BOUNCE_COUNT },
             "Hybrid Hotseat Education", new String[] { HOTSEAT_DISCOVERY_TIP_COUNT,
                     HOTSEAT_LONGPRESS_TIP_SEEN },
-            "Search Education", new String[] { SEARCH_KEYBOARD_EDU_SEEN, SEARCH_SNACKBAR_COUNT},
             "Taskbar Education", new String[] { TASKBAR_EDU_TOOLTIP_STEP },
             "All Apps Visited Count", new String[] {ALL_APPS_VISITED_COUNT}
     );
@@ -58,7 +55,6 @@
     @StringDef(value = {
             HOME_BOUNCE_SEEN,
             HOTSEAT_LONGPRESS_TIP_SEEN,
-            SEARCH_KEYBOARD_EDU_SEEN,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface EventBoolKey {}
@@ -69,7 +65,6 @@
     @StringDef(value = {
             HOME_BOUNCE_COUNT,
             HOTSEAT_DISCOVERY_TIP_COUNT,
-            SEARCH_SNACKBAR_COUNT,
             ALL_APPS_VISITED_COUNT,
             TASKBAR_EDU_TOOLTIP_STEP,
     })
@@ -82,7 +77,6 @@
         Map<String, Integer> maxCounts = new ArrayMap<>(5);
         maxCounts.put(HOME_BOUNCE_COUNT, 3);
         maxCounts.put(HOTSEAT_DISCOVERY_TIP_COUNT, 5);
-        maxCounts.put(SEARCH_SNACKBAR_COUNT, 3);
         maxCounts.put(ALL_APPS_VISITED_COUNT, 20);
         maxCounts.put(TASKBAR_EDU_TOOLTIP_STEP, 2);
         MAX_COUNTS = Collections.unmodifiableMap(maxCounts);
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 1d6bc25..91203a7 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -164,13 +164,6 @@
         }
     }
 
-    public static Intent getStyleWallpapersIntent(Context context) {
-        return new Intent(Intent.ACTION_SET_WALLPAPER).setComponent(
-                new ComponentName(context.getString(R.string.wallpaper_picker_package),
-                    context.getString(R.string.custom_activity_picker)
-                ));
-    }
-
     /**
      * Starts the details activity for {@code info}
      */
diff --git a/src/com/android/launcher3/util/SettingsCache.java b/src/com/android/launcher3/util/SettingsCache.java
index 0c5b722..06cb00e 100644
--- a/src/com/android/launcher3/util/SettingsCache.java
+++ b/src/com/android/launcher3/util/SettingsCache.java
@@ -154,7 +154,7 @@
      */
     public void unregister(Uri uri, OnChangeListener listener) {
         List<OnChangeListener> listenersToRemoveFrom = mListenerMap.get(uri);
-        if (!listenersToRemoveFrom.contains(listener)) {
+        if (listenersToRemoveFrom == null) {
             return;
         }
 
diff --git a/src/com/android/launcher3/util/StartActivityParams.java b/src/com/android/launcher3/util/StartActivityParams.java
index 6e60b2a..b48562f 100644
--- a/src/com/android/launcher3/util/StartActivityParams.java
+++ b/src/com/android/launcher3/util/StartActivityParams.java
@@ -20,7 +20,10 @@
 import static android.app.PendingIntent.FLAG_ONE_SHOT;
 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
 
+import static com.android.launcher3.Utilities.allowBGLaunch;
+
 import android.app.Activity;
+import android.app.ActivityOptions;
 import android.app.PendingIntent;
 import android.app.PendingIntent.CanceledException;
 import android.content.Context;
@@ -95,9 +98,10 @@
 
     /** Perform the operation on the pendingIntent. */
     public void deliverResult(Context context, int resultCode, Intent data) {
+        ActivityOptions options = allowBGLaunch(ActivityOptions.makeBasic());
         try {
             if (mPICallback != null) {
-                mPICallback.send(context, resultCode, data);
+                mPICallback.send(context, resultCode, data, null, null, null, options.toBundle());
             }
         } catch (CanceledException e) {
             Log.e(TAG, "Unable to send back result", e);
diff --git a/src/com/android/launcher3/util/Themes.java b/src/com/android/launcher3/util/Themes.java
index a5c663f..60951ba 100644
--- a/src/com/android/launcher3/util/Themes.java
+++ b/src/com/android/launcher3/util/Themes.java
@@ -21,8 +21,6 @@
 
 import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
 
-import android.app.WallpaperColors;
-import android.app.WallpaperManager;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Color;
@@ -48,16 +46,9 @@
 
     public static final String KEY_THEMED_ICONS = "themed_icons";
 
+    /** Gets the WallpaperColorHints and then uses those to get the correct activity theme res. */
     public static int getActivityThemeRes(Context context) {
-        final int colorHints;
-        if (Utilities.ATLEAST_P) {
-            WallpaperColors colors = context.getSystemService(WallpaperManager.class)
-                    .getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
-            colorHints = colors == null ? 0 : colors.getColorHints();
-        } else {
-            colorHints = 0;
-        }
-        return getActivityThemeRes(context, colorHints);
+        return getActivityThemeRes(context, WallpaperColorHints.get(context).getHints());
     }
 
     public static int getActivityThemeRes(Context context, int wallpaperColorHints) {
diff --git a/src/com/android/launcher3/util/TraceHelper.java b/src/com/android/launcher3/util/TraceHelper.java
index c23df77..138cc4a 100644
--- a/src/com/android/launcher3/util/TraceHelper.java
+++ b/src/com/android/launcher3/util/TraceHelper.java
@@ -15,12 +15,17 @@
  */
 package com.android.launcher3.util;
 
+import android.annotation.SuppressLint;
 import android.os.Trace;
 
 import androidx.annotation.MainThread;
 
+import com.android.launcher3.Utilities;
+
 import java.util.function.Supplier;
 
+import kotlin.random.Random;
+
 /**
  * A wrapper around {@link Trace} to allow better testing.
  *
@@ -36,54 +41,63 @@
     // Temporarily ignore blocking binder calls for this trace.
     public static final int FLAG_IGNORE_BINDERS = 1 << 1;
 
-    public static final int FLAG_CHECK_FOR_RACE_CONDITIONS = 1 << 2;
-
-    public static final int FLAG_UI_EVENT =
-            FLAG_ALLOW_BINDER_TRACKING | FLAG_CHECK_FOR_RACE_CONDITIONS;
-
     /**
      * Static instance of Trace helper, overridden in tests.
      */
     public static TraceHelper INSTANCE = new TraceHelper();
 
     /**
-     * @return a token to pass into {@link #endSection(Object)}.
+     * @see Trace#beginSection(String)
      */
-    public Object beginSection(String sectionName) {
-        return beginSection(sectionName, 0);
-    }
-
-    public Object beginSection(String sectionName, int flags) {
+    public void beginSection(String sectionName) {
         Trace.beginSection(sectionName);
-        return null;
     }
 
     /**
-     * @param token the token returned from {@link #beginSection(String, int)}
+     * @see Trace#endSection()
      */
-    public void endSection(Object token) {
+    public void endSection() {
         Trace.endSection();
     }
 
     /**
-     * Similar to {@link #beginSection} but doesn't add a trace section.
+     * @see Trace#beginAsyncSection(String, int)
+     * @return a SafeCloseable that can be used to end the session
      */
-    public Object beginFlagsOverride(int flags) {
-        return null;
+    @SuppressWarnings("NewApi")
+    @SuppressLint("NewApi")
+    public SafeCloseable beginAsyncSection(String sectionName) {
+        if (!Utilities.ATLEAST_Q) {
+            return () -> { };
+        }
+        int cookie = Random.Default.nextInt();
+        Trace.beginAsyncSection(sectionName, cookie);
+        return () -> Trace.endAsyncSection(sectionName, cookie);
     }
 
-    public void endFlagsOverride(Object token) { }
+    /**
+     * Returns a SafeCloseable to temporarily ignore blocking binder calls.
+     */
+    @SuppressWarnings("NewApi")
+    @SuppressLint("NewApi")
+    public SafeCloseable allowIpcs(String rpcName) {
+        if (!Utilities.ATLEAST_Q) {
+            return () -> { };
+        }
+        int cookie = Random.Default.nextInt();
+        Trace.beginAsyncSection(rpcName, cookie);
+        return () -> Trace.endAsyncSection(rpcName, cookie);
+    }
 
     /**
      * Temporarily ignore blocking binder calls for the duration of this {@link Supplier}.
+     *
+     * Note, new features should be designed to not rely on mainThread RPCs.
      */
     @MainThread
     public static <T> T allowIpcs(String rpcName, Supplier<T> supplier) {
-        Object traceToken = INSTANCE.beginSection(rpcName, FLAG_IGNORE_BINDERS);
-        try {
+        try (SafeCloseable c = INSTANCE.allowIpcs(rpcName)) {
             return supplier.get();
-        } finally {
-            INSTANCE.endSection(traceToken);
         }
     }
 }
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index ceba0db..91945ca 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -17,6 +17,7 @@
 
 import static android.os.VibrationEffect.createPredefined;
 import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED;
+
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
@@ -68,6 +69,9 @@
     @Nullable
     private final VibrationEffect mBumpEffect;
 
+    @Nullable
+    private final VibrationEffect mAssistEffect;
+
     private long mLastDragTime;
     private final int mThresholdUntilNextDragCallMillis;
 
@@ -125,12 +129,25 @@
             mBumpEffect = null;
             mThresholdUntilNextDragCallMillis = 0;
         }
+
+        if (Utilities.ATLEAST_R && mVibrator.areAllPrimitivesSupported(
+                VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
+                VibrationEffect.Composition.PRIMITIVE_TICK)) {
+            // quiet ramp, short pause, then sharp tick
+            mAssistEffect = VibrationEffect.startComposition()
+                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.25f)
+                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f, 50)
+                    .compose();
+        } else {
+            // fallback for devices without composition support
+            mAssistEffect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK);
+        }
     }
 
     /**
-     *  This is called when the user swipes to/from all apps. This is meant to be used in between
-     *  long animation progresses so that it gives a dragging texture effect. For a better
-     *  experience, this should be used in combination with vibrateForDragCommit().
+     * This is called when the user swipes to/from all apps. This is meant to be used in between
+     * long animation progresses so that it gives a dragging texture effect. For a better
+     * experience, this should be used in combination with vibrateForDragCommit().
      */
     public void vibrateForDragTexture() {
         if (mDragEffect == null) {
@@ -145,7 +162,7 @@
     }
 
     /**
-     *  This is used when user reaches the commit threshold when swiping to/from from all apps.
+     * This is used when user reaches the commit threshold when swiping to/from from all apps.
      */
     public void vibrateForDragCommit() {
         if (mCommitEffect != null) {
@@ -156,9 +173,9 @@
     }
 
     /**
-     *  The bump haptic is used to be called at the end of a swipe and only if it the gesture is a
-     *  FLING going to/from all apps. Client can just call this method elsewhere just for the
-     *  effect.
+     * The bump haptic is used to be called at the end of a swipe and only if it the gesture is a
+     * FLING going to/from all apps. Client can just call this method elsewhere just for the
+     * effect.
      */
     public void vibrateForDragBump() {
         if (mBumpEffect != null) {
@@ -167,6 +184,15 @@
     }
 
     /**
+     * The assist haptic is used to be called when an assistant is invoked
+     */
+    public void vibrateForAssist() {
+        if (mAssistEffect != null) {
+            vibrate(mAssistEffect);
+        }
+    }
+
+    /**
      * This should be used to cancel a haptic in case where the haptic shouldn't be vibrating. For
      * example, when no animation is happening but a vibrator happens to be vibrating still. Need
      * boolean parameter for {@link PendingAnimation#addEndListener(Consumer)}.
@@ -176,6 +202,7 @@
         // reset dragTexture timestamp to be able to play dragTexture again whenever cancelled
         mLastDragTime = 0;
     }
+
     private boolean isHapticFeedbackEnabled(ContentResolver resolver) {
         return Settings.System.getInt(resolver, HAPTIC_FEEDBACK_ENABLED, 0) == 1;
     }
diff --git a/src/com/android/launcher3/util/WallpaperColorHints.kt b/src/com/android/launcher3/util/WallpaperColorHints.kt
new file mode 100644
index 0000000..1361c1e
--- /dev/null
+++ b/src/com/android/launcher3/util/WallpaperColorHints.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.app.WallpaperColors
+import android.app.WallpaperManager
+import android.app.WallpaperManager.FLAG_SYSTEM
+import android.app.WallpaperManager.OnColorsChangedListener
+import android.content.Context
+import androidx.annotation.MainThread
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+
+/**
+ * This class caches the system's wallpaper color hints for use by other classes as a performance
+ * enhancer. It also centralizes all the WallpaperManager color hint code in one location.
+ */
+class WallpaperColorHints(private val context: Context) : SafeCloseable {
+    var hints: Int = 0
+        private set
+    private val wallpaperManager
+        get() = context.getSystemService(WallpaperManager::class.java)!!
+    private val onColorHintsChangedListeners = mutableListOf<OnColorHintListener>()
+    private val onClose: SafeCloseable
+
+    init {
+        if (Utilities.ATLEAST_S) {
+            hints = wallpaperManager.getWallpaperColors(FLAG_SYSTEM)?.colorHints ?: 0
+            val onColorsChangedListener = OnColorsChangedListener { colors, which ->
+                onColorsChanged(colors, which)
+            }
+            UI_HELPER_EXECUTOR.execute {
+                wallpaperManager.addOnColorsChangedListener(
+                    onColorsChangedListener,
+                    MAIN_EXECUTOR.handler
+                )
+            }
+            onClose = SafeCloseable {
+                UI_HELPER_EXECUTOR.execute {
+                    wallpaperManager.removeOnColorsChangedListener(onColorsChangedListener)
+                }
+            }
+        } else {
+            onClose = SafeCloseable {}
+        }
+    }
+
+    @MainThread
+    private fun onColorsChanged(colors: WallpaperColors?, which: Int) {
+        if ((which and FLAG_SYSTEM) != 0 && Utilities.ATLEAST_S) {
+            val newHints = colors?.colorHints ?: 0
+            if (newHints != hints) {
+                hints = newHints
+                onColorHintsChangedListeners.forEach { it.onColorHintsChanged(newHints) }
+            }
+        }
+    }
+
+    override fun close() = onClose.close()
+
+    fun registerOnColorHintsChangedListener(listener: OnColorHintListener) {
+        onColorHintsChangedListeners.add(listener)
+    }
+
+    fun unregisterOnColorsChangedListener(listener: OnColorHintListener) {
+        onColorHintsChangedListeners.remove(listener)
+    }
+
+    companion object {
+        @VisibleForTesting
+        @JvmField
+        val INSTANCE = MainThreadInitializedObject { WallpaperColorHints(it) }
+        @JvmStatic fun get(context: Context): WallpaperColorHints = INSTANCE.get(context)
+    }
+}
+
+interface OnColorHintListener {
+    fun onColorHintsChanged(colorHints: Int)
+}
diff --git a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
index 4ac6bc4..b97b889 100644
--- a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
+++ b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
@@ -15,9 +15,9 @@
 
 import androidx.annotation.AnyThread;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.anim.Interpolators;
 
 /**
  * Utility class to handle wallpaper scrolling along with workspace.
@@ -237,7 +237,7 @@
 
         public OffsetHandler(Context context) {
             super(UI_HELPER_EXECUTOR.getLooper());
-            mInterpolator = Interpolators.DEACCEL_1_5;
+            mInterpolator = Interpolators.DECELERATE_1_5;
             mWM = WallpaperManager.getInstance(context);
         }
 
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index 4093bc9..278a37e 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -57,6 +57,9 @@
 import com.android.launcher3.util.ResourceBasedOverride;
 import com.android.launcher3.util.WindowBounds;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Utility class for mocking some window manager behaviours
  */
@@ -90,11 +93,11 @@
      * Returns a map of normalized info of internal displays to estimated window bounds
      * for that display
      */
-    public ArrayMap<CachedDisplayInfo, WindowBounds[]> estimateInternalDisplayBounds(
+    public ArrayMap<CachedDisplayInfo, List<WindowBounds>> estimateInternalDisplayBounds(
             Context displayInfoContext) {
         CachedDisplayInfo info = getDisplayInfo(displayInfoContext).normalize();
-        WindowBounds[] bounds = estimateWindowBounds(displayInfoContext, info);
-        ArrayMap<CachedDisplayInfo, WindowBounds[]> result = new ArrayMap<>();
+        List<WindowBounds> bounds = estimateWindowBounds(displayInfoContext, info);
+        ArrayMap<CachedDisplayInfo, List<WindowBounds>> result = new ArrayMap<>();
         result.put(info, bounds);
         return result;
     }
@@ -200,7 +203,8 @@
     /**
      * Returns a list of possible WindowBounds for the display keyed on the 4 surface rotations
      */
-    protected WindowBounds[] estimateWindowBounds(Context context, CachedDisplayInfo displayInfo) {
+    protected List<WindowBounds> estimateWindowBounds(Context context,
+            CachedDisplayInfo displayInfo) {
         int densityDpi = context.getResources().getConfiguration().densityDpi;
         int rotation = displayInfo.rotation;
         Rect safeCutout = displayInfo.cutout;
@@ -243,7 +247,7 @@
                 ? 0
                 : getDimenByName(systemRes, NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
 
-        WindowBounds[] result = new WindowBounds[4];
+        List<WindowBounds> result = new ArrayList<>(4);
         Point tempSize = new Point();
         for (int i = 0; i < 4; i++) {
             int rotationChange = deltaRotation(rotation, i);
@@ -274,7 +278,7 @@
             } else {
                 insets.right = Math.max(insets.right, navbarWidth);
             }
-            result[i] = new WindowBounds(bounds, insets, i);
+            result.add(new WindowBounds(bounds, insets, i));
         }
         return result;
     }
diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index ec7ec0b..30e0971 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -17,24 +17,22 @@
 
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 
+import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.app.animation.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.LauncherAnimUtils.TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.allapps.AllAppsTransitionController.REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS;
-import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Outline;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.util.AttributeSet;
-import android.util.Property;
+import android.util.FloatProperty;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -47,10 +45,13 @@
 import androidx.annotation.Px;
 import androidx.annotation.RequiresApi;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.BaseSwipeDetector;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 
@@ -66,8 +67,8 @@
 public abstract class AbstractSlideInView<T extends Context & ActivityContext>
         extends AbstractFloatingView implements SingleAxisSwipeDetector.Listener {
 
-    protected static final Property<AbstractSlideInView, Float> TRANSLATION_SHIFT =
-            new Property<AbstractSlideInView, Float>(Float.class, "translationShift") {
+    protected static final FloatProperty<AbstractSlideInView<?>> TRANSLATION_SHIFT =
+            new FloatProperty<>("translationShift") {
 
                 @Override
                 public Float get(AbstractSlideInView view) {
@@ -75,25 +76,54 @@
                 }
 
                 @Override
-                public void set(AbstractSlideInView view, Float value) {
+                public void setValue(AbstractSlideInView view, float value) {
                     view.setTranslationShift(value);
                 }
             };
     protected static final float TRANSLATION_SHIFT_CLOSED = 1f;
     protected static final float TRANSLATION_SHIFT_OPENED = 0f;
     private static final float VIEW_NO_SCALE = 1f;
+    private static final int DEFAULT_DURATION = 300;
 
     protected final T mActivityContext;
 
     protected final SingleAxisSwipeDetector mSwipeDetector;
-    protected final ObjectAnimator mOpenCloseAnimator;
+    protected @NonNull AnimatorPlaybackController mOpenCloseAnimation;
 
     protected ViewGroup mContent;
     protected final View mColorScrim;
-    protected Interpolator mScrollInterpolator;
+
+    /**
+     * Interpolator for {@link #mOpenCloseAnimation} when we are closing due to dragging downwards.
+     */
+    private Interpolator mScrollInterpolator;
+    private long mScrollDuration;
+    /**
+     * End progress for {@link #mOpenCloseAnimation} when we are closing due to dragging downloads.
+     * <p>
+     * There are two cases that determine this value:
+     * <ol>
+     *     <li>
+     *         If the drag interrupts the opening transition (i.e. {@link #mToTranslationShift}
+     *         is {@link #TRANSLATION_SHIFT_OPENED}), we need to animate back to {@code 0} to
+     *         reverse the animation that was paused at {@link #onDragStart(boolean, float)}.
+     *     </li>
+     *     <li>
+     *         If the drag started after the view is fully opened (i.e.
+     *         {@link #mToTranslationShift} is {@link #TRANSLATION_SHIFT_CLOSED}), the animation
+     *         that was set up at {@link #onDragStart(boolean, float)} for closing the view
+     *         should go forward to {@code 1}.
+     *     </li>
+     * </ol>
+     */
+    private float mScrollEndProgress;
 
     // range [0, 1], 0=> completely open, 1=> completely closed
     protected float mTranslationShift = TRANSLATION_SHIFT_CLOSED;
+    protected float mFromTranslationShift;
+    protected float mToTranslationShift;
+    /** {@link #mOpenCloseAnimation} progress at {@link #onDragStart(boolean, float)}. */
+    private float mDragStartProgress;
 
     protected boolean mNoIntercept;
     protected @Nullable OnCloseListener mOnCloseBeginListener;
@@ -102,8 +132,8 @@
     protected final AnimatedFloat mSlideInViewScale =
             new AnimatedFloat(this::onScaleProgressChanged, VIEW_NO_SCALE);
     protected boolean mIsBackProgressing;
-    @Nullable private Drawable mContentBackground;
-    @Nullable private View mContentBackgroundParentView;
+    private @Nullable Drawable mContentBackground;
+    private @Nullable View mContentBackgroundParentView;
 
     protected final ViewOutlineProvider mViewOutlineProvider = new ViewOutlineProvider() {
         @Override
@@ -122,21 +152,78 @@
         mActivityContext = ActivityContext.lookupContext(context);
 
         mScrollInterpolator = Interpolators.SCROLL_CUBIC;
+        mScrollDuration = DEFAULT_DURATION;
         mSwipeDetector = new SingleAxisSwipeDetector(context, this,
                 SingleAxisSwipeDetector.VERTICAL);
 
-        mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
-        mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mSwipeDetector.finishedScrolling();
-                announceAccessibilityChanges();
-            }
-        });
+        mOpenCloseAnimation = new PendingAnimation(0).createPlaybackController();
+
         int scrimColor = getScrimColor(context);
         mColorScrim = scrimColor != -1 ? createColorScrim(context, scrimColor) : null;
     }
 
+    /**
+     * Sets up a {@link #mOpenCloseAnimation} for opening with default parameters.
+     *
+     * @see #setUpOpenCloseAnimation(float, float, long)
+     */
+    protected final AnimatorPlaybackController setUpDefaultOpenAnimation() {
+        AnimatorPlaybackController animation = setUpOpenCloseAnimation(
+                TRANSLATION_SHIFT_CLOSED, TRANSLATION_SHIFT_OPENED, DEFAULT_DURATION);
+        animation.getAnimationPlayer().setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        return animation;
+    }
+
+    /**
+     * Sets up a {@link #mOpenCloseAnimation} for opening with a given duration.
+     *
+     * @see #setUpOpenCloseAnimation(float, float, long)
+     */
+    protected final AnimatorPlaybackController setUpOpenAnimation(long duration) {
+        return setUpOpenCloseAnimation(
+                TRANSLATION_SHIFT_CLOSED, TRANSLATION_SHIFT_OPENED, duration);
+    }
+
+    private AnimatorPlaybackController setUpCloseAnimation(long duration) {
+        return setUpOpenCloseAnimation(
+                TRANSLATION_SHIFT_OPENED, TRANSLATION_SHIFT_CLOSED, duration);
+    }
+
+    /**
+     * Initializes a new {@link #mOpenCloseAnimation}.
+     *
+     * @param fromTranslationShift translation shift to animate from.
+     * @param toTranslationShift   translation shift to animate to.
+     * @param duration             animation duration.
+     * @return {@link #mOpenCloseAnimation}
+     */
+    private AnimatorPlaybackController setUpOpenCloseAnimation(
+            float fromTranslationShift, float toTranslationShift, long duration) {
+        mFromTranslationShift = fromTranslationShift;
+        mToTranslationShift = toTranslationShift;
+
+        PendingAnimation animation = new PendingAnimation(duration);
+        animation.addEndListener(b -> {
+            mSwipeDetector.finishedScrolling();
+            announceAccessibilityChanges();
+        });
+
+        animation.addFloat(
+                this, TRANSLATION_SHIFT, fromTranslationShift, toTranslationShift, LINEAR);
+        onOpenCloseAnimationPending(animation);
+
+        mOpenCloseAnimation = animation.createPlaybackController();
+        return mOpenCloseAnimation;
+    }
+
+    /**
+     * Invoked when a {@link #mOpenCloseAnimation} is being set up.
+     * <p>
+     * Subclasses can override this method to modify the animation before it's used to create a
+     * {@link AnimatorPlaybackController}.
+     */
+    protected void onOpenCloseAnimationPending(PendingAnimation animation) {}
+
     protected void attachToContainer() {
         if (mColorScrim != null) {
             getPopupContainer().addView(mColorScrim);
@@ -279,19 +366,28 @@
     }
 
     private boolean isOpeningAnimationRunning() {
-        return mIsOpen && mOpenCloseAnimator.isRunning();
+        return mIsOpen && mOpenCloseAnimation.getAnimationPlayer().isRunning();
     }
 
     /* SingleAxisSwipeDetector.Listener */
 
     @Override
-    public void onDragStart(boolean start, float startDisplacement) { }
+    public void onDragStart(boolean start, float startDisplacement) {
+        if (mOpenCloseAnimation.getAnimationPlayer().isRunning()) {
+            mOpenCloseAnimation.pause();
+            mDragStartProgress = mOpenCloseAnimation.getProgressFraction();
+        } else {
+            setUpCloseAnimation(DEFAULT_DURATION);
+            mDragStartProgress = 0;
+        }
+    }
 
     @Override
     public boolean onDrag(float displacement) {
-        float range = getShiftRange();
-        displacement = Utilities.boundToRange(displacement, 0, range);
-        setTranslationShift(displacement / range);
+        float progress = mDragStartProgress
+                + Math.signum(mToTranslationShift - mFromTranslationShift)
+                * (displacement / getShiftRange());
+        mOpenCloseAnimation.setPlayFraction(Utilities.boundToRange(progress, 0, 1));
         return true;
     }
 
@@ -302,16 +398,18 @@
         if ((mSwipeDetector.isFling(velocity) && velocity > 0)
                 || mTranslationShift > successfulShiftThreshold) {
             mScrollInterpolator = scrollInterpolatorForVelocity(velocity);
-            mOpenCloseAnimator.setDuration(BaseSwipeDetector.calculateDuration(
-                    velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift));
+            mScrollDuration = BaseSwipeDetector.calculateDuration(
+                    velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift);
+            mScrollEndProgress = mToTranslationShift == TRANSLATION_SHIFT_OPENED ? 0 : 1;
             close(true);
         } else {
-            mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat(
-                    TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-            mOpenCloseAnimator.setDuration(
-                    BaseSwipeDetector.calculateDuration(velocity, mTranslationShift))
-                    .setInterpolator(Interpolators.DEACCEL);
-            mOpenCloseAnimator.start();
+            ValueAnimator animator = mOpenCloseAnimation.getAnimationPlayer();
+            animator.setInterpolator(Interpolators.DECELERATE);
+            animator.setFloatValues(
+                    mOpenCloseAnimation.getProgressFraction(),
+                    mToTranslationShift == TRANSLATION_SHIFT_OPENED ? 1 : 0);
+            animator.setDuration(BaseSwipeDetector.calculateDuration(velocity, mTranslationShift))
+                    .start();
         }
     }
 
@@ -332,32 +430,31 @@
         Optional.ofNullable(mOnCloseBeginListener).ifPresent(OnCloseListener::onSlideInViewClosed);
 
         if (!animate) {
-            mOpenCloseAnimator.cancel();
+            mOpenCloseAnimation.pause();
             setTranslationShift(TRANSLATION_SHIFT_CLOSED);
             onCloseComplete();
             return;
         }
-        mOpenCloseAnimator.setValues(
-                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_CLOSED));
-        mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mOpenCloseAnimator.removeListener(this);
-                onCloseComplete();
-            }
-        });
+
+        final ValueAnimator animator;
         if (mSwipeDetector.isIdleState()) {
-            mOpenCloseAnimator
-                    .setDuration(defaultDuration)
-                    .setInterpolator(getIdleInterpolator());
+            setUpCloseAnimation(defaultDuration);
+            animator = mOpenCloseAnimation.getAnimationPlayer();
+            animator.setInterpolator(getIdleInterpolator());
         } else {
-            mOpenCloseAnimator.setInterpolator(mScrollInterpolator);
+            animator = mOpenCloseAnimation.getAnimationPlayer();
+            animator.setInterpolator(mScrollInterpolator);
+            animator.setDuration(mScrollDuration);
+            mOpenCloseAnimation.getAnimationPlayer().setFloatValues(
+                    mOpenCloseAnimation.getProgressFraction(), mScrollEndProgress);
         }
-        mOpenCloseAnimator.start();
+
+        animator.addListener(AnimatorListeners.forEndCallback(this::onCloseComplete));
+        animator.start();
     }
 
     protected Interpolator getIdleInterpolator() {
-        return Interpolators.ACCEL;
+        return Interpolators.ACCELERATE;
     }
 
     protected void onCloseComplete() {
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index 4b319e5..84ea871 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -18,6 +18,7 @@
 import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR;
 
 import static com.android.launcher3.LauncherSettings.Animation.DEFAULT_NO_ICON;
+import static com.android.launcher3.Utilities.allowBGLaunch;
 import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_KEYBOARD_CLOSED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_PENDING_INTENT;
@@ -38,7 +39,6 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Process;
-import android.os.StrictMode;
 import android.os.UserHandle;
 import android.util.Log;
 import android.view.Display;
@@ -239,6 +239,11 @@
         };
     }
 
+    /** Long-click callback used for All Apps items. */
+    default View.OnLongClickListener getAllAppsItemLongClickListener() {
+        return v -> false;
+    }
+
     @Nullable
     default PopupDataProvider getPopupDataProvider() {
         return null;
@@ -338,6 +343,12 @@
             return null;
         }
 
+        boolean isShortcut = (item instanceof WorkspaceItemInfo)
+                && item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+                && !((WorkspaceItemInfo) item).isPromise();
+        if (isShortcut && GO_DISABLE_WIDGETS) {
+            return null;
+        }
         ActivityOptionsWrapper options = v != null ? getActivityLaunchOptions(v, item)
                 : makeDefaultActivityOptions(item != null && item.animationType == DEFAULT_NO_ICON
                         ? SPLASH_SCREEN_STYLE_SOLID_COLOR : -1 /* SPLASH_SCREEN_STYLE_UNDEFINED */);
@@ -349,13 +360,11 @@
             intent.setSourceBounds(Utilities.getViewBounds(v));
         }
         try {
-            boolean isShortcut = (item instanceof WorkspaceItemInfo)
-                    && (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT
-                    || item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)
-                    && !((WorkspaceItemInfo) item).isPromise();
             if (isShortcut) {
-                // Shortcuts need some special checks due to legacy reasons.
-                startShortcutIntentSafely(intent, optsBundle, item);
+                String id = ((WorkspaceItemInfo) item).getDeepShortcutId();
+                String packageName = intent.getPackage();
+                ((Context) this).getSystemService(LauncherApps.class).startShortcut(
+                        packageName, id, intent.getSourceBounds(), optsBundle, user);
             } else if (user == null || user.equals(Process.myUserHandle())) {
                 // Could be launching some bookkeeping activity
                 context.startActivity(intent, optsBundle);
@@ -410,8 +419,7 @@
             }
         }
         ActivityOptions options =
-                ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
-
+                allowBGLaunch(ActivityOptions.makeClipRevealAnimation(v, left, top, width, height));
         options.setLaunchDisplayId(
                 (v != null && v.getDisplay() != null) ? v.getDisplay().getDisplayId()
                         : Display.DEFAULT_DISPLAY);
@@ -423,62 +431,13 @@
      * Creates a default activity option and we do not want association with any launcher element.
      */
     default ActivityOptionsWrapper makeDefaultActivityOptions(int splashScreenStyle) {
-        ActivityOptions options = ActivityOptions.makeBasic();
+        ActivityOptions options = allowBGLaunch(ActivityOptions.makeBasic());
         if (Utilities.ATLEAST_T) {
             options.setSplashScreenStyle(splashScreenStyle);
         }
         return new ActivityOptionsWrapper(options, new RunnableList());
     }
 
-    /**
-     * Safely launches an intent for a shortcut.
-     *
-     * @param intent Intent to start.
-     * @param optsBundle Optional launch arguments.
-     * @param info Shortcut information.
-     */
-    default void startShortcutIntentSafely(Intent intent, Bundle optsBundle, ItemInfo info) {
-        try {
-            StrictMode.VmPolicy oldPolicy = StrictMode.getVmPolicy();
-            try {
-                // Temporarily disable deathPenalty on all default checks. For eg, shortcuts
-                // containing file Uri's would cause a crash as penaltyDeathOnFileUriExposure
-                // is enabled by default on NYC.
-                StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll()
-                        .penaltyLog().build());
-
-                if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
-                    String id = ((WorkspaceItemInfo) info).getDeepShortcutId();
-                    String packageName = intent.getPackage();
-                    startShortcut(packageName, id, intent.getSourceBounds(), optsBundle, info.user);
-                } else {
-                    // Could be launching some bookkeeping activity
-                    ((Context) this).startActivity(intent, optsBundle);
-                }
-            } finally {
-                StrictMode.setVmPolicy(oldPolicy);
-            }
-        } catch (SecurityException e) {
-            throw e;
-        }
-    }
-
-    /**
-     * A wrapper around the platform method with Launcher specific checks.
-     */
-    default void startShortcut(String packageName, String id, Rect sourceBounds,
-            Bundle startActivityOptions, UserHandle user) {
-        if (GO_DISABLE_WIDGETS) {
-            return;
-        }
-        try {
-            ((Context) this).getSystemService(LauncherApps.class).startShortcut(packageName, id,
-                    sourceBounds, startActivityOptions, user);
-        } catch (SecurityException | IllegalStateException e) {
-            Log.e(TAG, "Failed to start shortcut", e);
-        }
-    }
-
     default CellPosMapper getCellPosMapper() {
         return CellPosMapper.DEFAULT;
     }
diff --git a/src/com/android/launcher3/views/ArrowTipView.java b/src/com/android/launcher3/views/ArrowTipView.java
index 73c5ad4..b44dbeb 100644
--- a/src/com/android/launcher3/views/ArrowTipView.java
+++ b/src/com/android/launcher3/views/ArrowTipView.java
@@ -16,14 +16,21 @@
 
 package com.android.launcher3.views;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.content.res.TypedArray;
 import android.graphics.CornerPathEffect;
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.drawable.ShapeDrawable;
 import android.os.Handler;
+import android.util.IntProperty;
 import android.util.Log;
+import android.view.ContextThemeWrapper;
 import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.View;
@@ -33,18 +40,17 @@
 
 import androidx.annotation.Nullable;
 import androidx.annotation.Px;
-import androidx.core.content.ContextCompat;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
-import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.graphics.TriangleShape;
 
 /**
- * A base class for arrow tip view in launcher
+ * A base class for arrow tip view in launcher.
  */
 public class ArrowTipView extends AbstractFloatingView {
 
@@ -54,33 +60,66 @@
     private static final long SHOW_DURATION_MS = 300;
     private static final long HIDE_DURATION_MS = 100;
 
-    protected final BaseDraggingActivity mActivity;
+    public static final IntProperty<ArrowTipView> TEXT_ALPHA =
+            new IntProperty<>("textAlpha") {
+                @Override
+                public void setValue(ArrowTipView view, int v) {
+                    view.setTextAlpha(v);
+                }
+
+                @Override
+                public Integer get(ArrowTipView view) {
+                    return view.getTextAlpha();
+                }
+            };
+
+    private final ActivityContext mActivityContext;
     private final Handler mHandler = new Handler();
-    private final int mArrowWidth;
-    private final int mArrowMinOffset;
     private boolean mIsPointingUp;
     private Runnable mOnClosed;
     private View mArrowView;
+    private final int mArrowWidth;
+    private final int mArrowMinOffset;
+    private final int mArrowViewPaintColor;
+
+    private AnimatorSet mOpenAnimator = new AnimatorSet();
+    private AnimatorSet mCloseAnimator = new AnimatorSet();
+
+    private int mTextAlpha;
 
     public ArrowTipView(Context context) {
         this(context, false);
     }
 
     public ArrowTipView(Context context, boolean isPointingUp) {
+        this(context, isPointingUp, R.layout.arrow_toast);
+    }
+
+    public ArrowTipView(Context context, boolean isPointingUp, int layoutId) {
         super(context, null, 0);
-        mActivity = BaseDraggingActivity.fromContext(context);
+        mActivityContext = ActivityContext.lookupContext(context);
         mIsPointingUp = isPointingUp;
-        mArrowWidth = context.getResources().getDimensionPixelSize(R.dimen.arrow_toast_arrow_width);
+        mArrowWidth = context.getResources().getDimensionPixelSize(
+                R.dimen.arrow_toast_arrow_width);
         mArrowMinOffset = context.getResources().getDimensionPixelSize(
                 R.dimen.dynamic_grid_cell_border_spacing);
-        init(context);
+        TypedArray ta = context.obtainStyledAttributes(R.styleable.ArrowTipView);
+        // Set style to default to avoid inflation issues with missing attributes.
+        if (!ta.hasValue(R.styleable.ArrowTipView_arrowTipBackground)
+                || !ta.hasValue(R.styleable.ArrowTipView_arrowTipTextColor)) {
+            context = new ContextThemeWrapper(context, R.style.ArrowTipStyle);
+        }
+        mArrowViewPaintColor = ta.getColor(R.styleable.ArrowTipView_arrowTipBackground,
+                context.getColor(R.color.arrow_tip_view_bg));
+        ta.recycle();
+        init(context, layoutId);
     }
 
     @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             close(true);
-            if (mActivity.getDragLayer().isEventOverView(this, ev)) {
+            if (mActivityContext.getDragLayer().isEventOverView(this, ev)) {
                 return true;
             }
         }
@@ -89,18 +128,17 @@
 
     @Override
     protected void handleClose(boolean animate) {
+        if (mOpenAnimator.isStarted()) {
+            mOpenAnimator.cancel();
+        }
         if (mIsOpen) {
             if (animate) {
-                animate().alpha(0f)
-                        .withLayer()
-                        .setStartDelay(0)
-                        .setDuration(HIDE_DURATION_MS)
-                        .setInterpolator(Interpolators.ACCEL)
-                        .withEndAction(() -> mActivity.getDragLayer().removeView(this))
-                        .start();
+                mCloseAnimator.addListener(AnimatorListeners.forSuccessCallback(
+                        () -> mActivityContext.getDragLayer().removeView(this)));
+                mCloseAnimator.start();
             } else {
-                animate().cancel();
-                mActivity.getDragLayer().removeView(this);
+                mCloseAnimator.cancel();
+                mActivityContext.getDragLayer().removeView(this);
             }
             if (mOnClosed != null) mOnClosed.run();
             mIsOpen = false;
@@ -112,12 +150,31 @@
         return (type & TYPE_ON_BOARD_POPUP) != 0;
     }
 
-    private void init(Context context) {
-        inflate(context, R.layout.arrow_toast, this);
+    private void init(Context context, int layoutId) {
+        inflate(context, layoutId, this);
         setOrientation(LinearLayout.VERTICAL);
 
         mArrowView = findViewById(R.id.arrow);
         updateArrowTipInView();
+        setAlpha(0);
+
+        // Create default open animator.
+        mOpenAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, 1f));
+        mOpenAnimator.setStartDelay(SHOW_DELAY_MS);
+        mOpenAnimator.setDuration(SHOW_DURATION_MS);
+        mOpenAnimator.setInterpolator(Interpolators.DECELERATE);
+
+        // Create default close animator.
+        mCloseAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, 0));
+        mCloseAnimator.setStartDelay(0);
+        mCloseAnimator.setDuration(HIDE_DURATION_MS);
+        mCloseAnimator.setInterpolator(Interpolators.ACCELERATE);
+        mCloseAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mActivityContext.getDragLayer().removeView(ArrowTipView.this);
+            }
+        });
     }
 
     /**
@@ -153,10 +210,10 @@
     public ArrowTipView show(
             String text, int gravity, int arrowMarginStart, int top, boolean shouldAutoClose) {
         ((TextView) findViewById(R.id.text)).setText(text);
-        ViewGroup parent = mActivity.getDragLayer();
+        ViewGroup parent = mActivityContext.getDragLayer();
         parent.addView(this);
 
-        DeviceProfile grid = mActivity.getDeviceProfile();
+        DeviceProfile grid = mActivityContext.getDeviceProfile();
 
         DragLayer.LayoutParams params = (DragLayer.LayoutParams) getLayoutParams();
         params.gravity = gravity;
@@ -185,14 +242,8 @@
         if (shouldAutoClose) {
             mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
         }
-        setAlpha(0);
-        animate()
-                .alpha(1f)
-                .withLayer()
-                .setStartDelay(SHOW_DELAY_MS)
-                .setDuration(SHOW_DURATION_MS)
-                .setInterpolator(Interpolators.DEACCEL)
-                .start();
+
+        mOpenAnimator.start();
         return this;
     }
 
@@ -273,7 +324,7 @@
      */
     @Nullable private ArrowTipView showAtLocation(String text, @Px int arrowXCoord,
             @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip, boolean shouldAutoClose) {
-        ViewGroup parent = mActivity.getDragLayer();
+        ViewGroup parent = mActivityContext.getDragLayer();
         @Px int parentViewWidth = parent.getWidth();
         @Px int parentViewHeight = parent.getHeight();
         @Px int maxTextViewWidth = getContext().getResources()
@@ -288,8 +339,10 @@
         TextView textView = findViewById(R.id.text);
         textView.setText(text);
         textView.setMaxWidth(maxTextViewWidth);
-        parent.addView(this);
-        requestLayout();
+        if (parent.indexOfChild(this) < 0) {
+            parent.addView(this);
+            requestLayout();
+        }
 
         post(() -> {
             // Adjust the tooltip horizontally.
@@ -333,14 +386,8 @@
         if (shouldAutoClose) {
             mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
         }
-        setAlpha(0);
-        animate()
-                .alpha(1f)
-                .withLayer()
-                .setStartDelay(SHOW_DELAY_MS)
-                .setDuration(SHOW_DURATION_MS)
-                .setInterpolator(Interpolators.DEACCEL)
-                .start();
+
+        mOpenAnimator.start();
         return this;
     }
 
@@ -351,7 +398,7 @@
         Paint arrowPaint = arrowDrawable.getPaint();
         @Px int arrowTipRadius = getContext().getResources()
                 .getDimensionPixelSize(R.dimen.arrow_toast_corner_radius);
-        arrowPaint.setColor(ContextCompat.getColor(getContext(), R.color.arrow_tip_view_bg));
+        arrowPaint.setColor(mArrowViewPaintColor);
         arrowPaint.setPathEffect(new CornerPathEffect(arrowTipRadius));
         mArrowView.setBackground(arrowDrawable);
         // Add negative margin so that the rounded corners on base of arrow are not visible.
@@ -378,4 +425,30 @@
         super.onConfigurationChanged(newConfig);
         close(/* animate= */ false);
     }
+
+    /**
+     * Sets a custom animation to run on open of the ArrowTipView.
+     */
+    public void setCustomOpenAnimation(AnimatorSet animator) {
+        mOpenAnimator = animator;
+    }
+
+    /**
+     * Sets a custom animation to run on close of the ArrowTipView.
+     */
+    public void setCustomCloseAnimation(AnimatorSet animator) {
+        mCloseAnimator = animator;
+    }
+
+    private void setTextAlpha(int textAlpha) {
+        if (mTextAlpha != textAlpha) {
+            mTextAlpha = textAlpha;
+            TextView textView = findViewById(R.id.text);
+            textView.setTextColor(textView.getTextColors().withAlpha(mTextAlpha));
+        }
+    }
+
+    private int getTextAlpha() {
+        return mTextAlpha;
+    }
 }
diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
index 694dead..87e496e 100644
--- a/src/com/android/launcher3/views/ClipIconView.java
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -15,9 +15,9 @@
  */
 package com.android.launcher3.views;
 
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.Utilities.boundToRange;
 import static com.android.launcher3.Utilities.mapToRange;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 
 import static java.lang.Math.max;
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 6b5c8df..41b98c7 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -17,10 +17,10 @@
 
 import static android.view.Gravity.LEFT;
 
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.Utilities.getBadge;
 import static com.android.launcher3.Utilities.getFullDrawable;
 import static com.android.launcher3.Utilities.mapToRange;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible;
 
@@ -289,12 +289,14 @@
             int width = (int) pos.width();
             int height = (int) pos.height();
             Object[] tmpObjArray = new Object[1];
+            boolean[] outIsIconThemed = new boolean[1];
             if (supportsAdaptiveIcons) {
                 boolean shouldThemeIcon = btvIcon instanceof FastBitmapDrawable
                         && ((FastBitmapDrawable) btvIcon).isThemed();
-                drawable = getFullDrawable(l, info, width, height, shouldThemeIcon, tmpObjArray);
+                drawable = getFullDrawable(
+                        l, info, width, height, shouldThemeIcon, tmpObjArray, outIsIconThemed);
                 if (drawable instanceof AdaptiveIconDrawable) {
-                    badge = getBadge(l, info, tmpObjArray[0]);
+                    badge = getBadge(l, info, tmpObjArray[0], outIsIconThemed[0]);
                 } else {
                     // The drawable we get back is not an adaptive icon, so we need to use the
                     // BubbleTextView icon that is already legacy treated.
@@ -306,7 +308,7 @@
                     drawable = btvIcon;
                 } else {
                     drawable = getFullDrawable(l, info, width, height, true /* shouldThemeIcon */,
-                            tmpObjArray);
+                            tmpObjArray, outIsIconThemed);
                 }
             }
         }
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 4641e31..b62f60d 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -55,7 +55,6 @@
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 
 import java.util.ArrayList;
@@ -63,8 +62,10 @@
 
 /**
  * Popup shown on long pressing an empty space in launcher
+ *
+ * @param <T> The context showing this popup.
  */
-public class OptionsPopupView extends ArrowPopup<Launcher>
+public class OptionsPopupView<T extends Context & ActivityContext> extends ArrowPopup<T>
         implements OnClickListener, OnLongClickListener {
 
     // An intent extra to indicate the horizontal scroll of the wallpaper.
@@ -156,21 +157,27 @@
         }
     }
 
-    public static OptionsPopupView show(ActivityContext launcher, RectF targetRect,
-            List<OptionItem> items, boolean shouldAddArrow) {
-        return show(launcher, targetRect, items, shouldAddArrow, 0 /* width */);
+    public static <T extends Context & ActivityContext> OptionsPopupView<T> show(
+            ActivityContext activityContext,
+            RectF targetRect,
+            List<OptionItem> items,
+            boolean shouldAddArrow) {
+        return show(activityContext, targetRect, items, shouldAddArrow, 0 /* width */);
     }
 
-    public static OptionsPopupView show(ActivityContext launcher, RectF targetRect,
-            List<OptionItem> items, boolean shouldAddArrow, int width) {
-        OptionsPopupView popup = (OptionsPopupView) launcher.getLayoutInflater()
-                .inflate(R.layout.longpress_options_menu, launcher.getDragLayer(), false);
+    public static <T extends Context & ActivityContext> OptionsPopupView<T> show(
+            ActivityContext activityContext,
+            RectF targetRect,
+            List<OptionItem> items,
+            boolean shouldAddArrow,
+            int width) {
+        OptionsPopupView<T> popup = (OptionsPopupView<T>) activityContext.getLayoutInflater()
+                .inflate(R.layout.longpress_options_menu, activityContext.getDragLayer(), false);
         popup.mTargetRect = targetRect;
         popup.setShouldAddArrow(shouldAddArrow);
 
         for (OptionItem item : items) {
-            DeepShortcutView view =
-                    (DeepShortcutView) popup.inflateAndAdd(R.layout.system_shortcut, popup);
+            DeepShortcutView view = popup.inflateAndAdd(R.layout.system_shortcut, popup);
             if (width > 0) {
                 view.getLayoutParams().width = width;
             }
@@ -190,14 +197,9 @@
      */
     public static ArrayList<OptionItem> getOptions(Launcher launcher) {
         ArrayList<OptionItem> options = new ArrayList<>();
-        boolean styleWallpaperExists = styleWallpapersExists(launcher);
-        int resString = styleWallpaperExists
-                ? R.string.styles_wallpaper_button_text : R.string.wallpaper_button_text;
-        int resDrawable = styleWallpaperExists
-                ? R.drawable.ic_palette : R.drawable.ic_wallpaper;
         options.add(new OptionItem(launcher,
-                resString,
-                resDrawable,
+                R.string.styles_wallpaper_button_text,
+                R.drawable.ic_palette,
                 IGNORE,
                 OptionsPopupView::startWallpaperPicker));
         if (!WidgetsModel.GO_DISABLE_WIDGETS) {
@@ -274,12 +276,8 @@
                 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
                 .putExtra(EXTRA_WALLPAPER_OFFSET,
                         launcher.getWorkspace().getWallpaperOffsetForCenterPage())
-                .putExtra(EXTRA_WALLPAPER_LAUNCH_SOURCE, "app_launched_launcher");
-        if (!styleWallpapersExists(launcher)) {
-            intent.putExtra(EXTRA_WALLPAPER_FLAVOR, "wallpaper_only");
-        } else {
-            intent.putExtra(EXTRA_WALLPAPER_FLAVOR, "focus_wallpaper");
-        }
+                .putExtra(EXTRA_WALLPAPER_LAUNCH_SOURCE, "app_launched_launcher")
+                .putExtra(EXTRA_WALLPAPER_FLAVOR, "focus_wallpaper");
         String pickerPackage = launcher.getString(R.string.wallpaper_picker_package);
         if (!TextUtils.isEmpty(pickerPackage)) {
             intent.setPackage(pickerPackage);
@@ -290,7 +288,6 @@
     static WorkspaceItemInfo placeholderInfo(Intent intent) {
         WorkspaceItemInfo placeholderInfo = new WorkspaceItemInfo();
         placeholderInfo.intent = intent;
-        placeholderInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
         placeholderInfo.container = LauncherSettings.Favorites.CONTAINER_SETTINGS;
         return placeholderInfo;
     }
@@ -323,9 +320,4 @@
             this.clickListener = clickListener;
         }
     }
-
-    private static boolean styleWallpapersExists(Context context) {
-        return context.getPackageManager().resolveActivity(
-                PackageManagerHelper.getStyleWallpapersIntent(context), 0) != null;
-    }
 }
diff --git a/src/com/android/launcher3/views/Snackbar.java b/src/com/android/launcher3/views/Snackbar.java
index 2460be1..99040ff 100644
--- a/src/com/android/launcher3/views/Snackbar.java
+++ b/src/com/android/launcher3/views/Snackbar.java
@@ -30,10 +30,10 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.dragndrop.DragLayer;
 
@@ -175,7 +175,7 @@
                 .scaleX(1)
                 .scaleY(1)
                 .setDuration(SHOW_DURATION_MS)
-                .setInterpolator(Interpolators.ACCEL_DEACCEL)
+                .setInterpolator(Interpolators.ACCELERATE_DECELERATE)
                 .start();
         int timeout = AccessibilityManagerCompat.getRecommendedTimeoutMillis(activity,
                 TIMEOUT_DURATION_MS, FLAG_CONTENT_TEXT | FLAG_CONTENT_CONTROLS);
@@ -190,7 +190,7 @@
                         .withLayer()
                         .setStartDelay(0)
                         .setDuration(HIDE_DURATION_MS)
-                        .setInterpolator(Interpolators.ACCEL)
+                        .setInterpolator(Interpolators.ACCELERATE)
                         .withEndAction(this::onClosed)
                         .start();
             } else {
diff --git a/src/com/android/launcher3/views/WidgetsEduView.java b/src/com/android/launcher3/views/WidgetsEduView.java
index c2947c7..e70b1cb 100644
--- a/src/com/android/launcher3/views/WidgetsEduView.java
+++ b/src/com/android/launcher3/views/WidgetsEduView.java
@@ -15,9 +15,6 @@
  */
 package com.android.launcher3.views;
 
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
-
-import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
@@ -119,14 +116,11 @@
     }
 
     private void animateOpen() {
-        if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+        if (mIsOpen || mOpenCloseAnimation.getAnimationPlayer().isRunning()) {
             return;
         }
         mIsOpen = true;
-        mOpenCloseAnimator.setValues(
-                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-        mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN);
-        mOpenCloseAnimator.start();
+        setUpDefaultOpenAnimation().start();
     }
 
     /** Shows widget education dialog. */
diff --git a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java
index 9442734..80b1cdd 100644
--- a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java
@@ -17,9 +17,7 @@
 package com.android.launcher3.widget;
 
 import static com.android.launcher3.Utilities.ATLEAST_R;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 
-import android.animation.PropertyValuesHolder;
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.Insets;
@@ -130,14 +128,11 @@
     }
 
     private void animateOpen() {
-        if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+        if (mIsOpen || mOpenCloseAnimation.getAnimationPlayer().isRunning()) {
             return;
         }
         mIsOpen = true;
-        mOpenCloseAnimator.setValues(
-                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-        mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN);
-        mOpenCloseAnimator.start();
+        setUpDefaultOpenAnimation().start();
     }
 
     @Override
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 049131e..dcc86a1 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -15,7 +15,7 @@
  */
 package com.android.launcher3.widget;
 
-import static com.android.launcher3.anim.Interpolators.EMPHASIZED;
+import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.launcher3.config.FeatureFlags.LARGE_SCREEN_WIDGET_PICKER;
 
 import android.content.Context;
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index bc3889f..340a61e 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -95,6 +95,8 @@
 
     private boolean mTrackingWidgetUpdate = false;
 
+    private boolean mIsWidgetCachingDisabled = false;
+
     public LauncherAppWidgetHostView(Context context) {
         super(context);
         mLauncher = Launcher.getLauncher(context);
@@ -105,7 +107,7 @@
         if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
             setOnLightBackground(true);
         }
-        mColorExtractor = LocalColorExtractor.newInstance(getContext());
+        mColorExtractor = new LocalColorExtractor(); // no-op
     }
 
     @Override
@@ -138,6 +140,10 @@
         }
     }
 
+    public void setIsWidgetCachingDisabled(boolean isWidgetCachingDisabled) {
+        mIsWidgetCachingDisabled = isWidgetCachingDisabled;
+    }
+
     @Override
     @TargetApi(Build.VERSION_CODES.Q)
     public void updateAppWidget(RemoteViews remoteViews) {
@@ -147,7 +153,8 @@
                     TRACE_METHOD_NAME + getAppWidgetInfo().provider, getAppWidgetId());
             mTrackingWidgetUpdate = false;
         }
-        if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) {
+        if (FeatureFlags.ENABLE_CACHED_WIDGET.get()
+                && !mIsWidgetCachingDisabled) {
             mLastRemoteViews = remoteViews;
             if (isDeferringUpdates()) {
                 return;
diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
index 2ca825c..6acc83d 100644
--- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java
+++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
@@ -275,9 +275,15 @@
     protected Bundle getConfigurationActivityOptions(@NonNull BaseDraggingActivity activity,
             int widgetId) {
         LauncherAppWidgetHostView view = mViews.get(widgetId);
-        if (view == null) return null;
+        if (view == null) {
+            return activity.makeDefaultActivityOptions(
+                    -1 /* SPLASH_SCREEN_STYLE_UNDEFINED */).toBundle();
+        }
         Object tag = view.getTag();
-        if (!(tag instanceof ItemInfo)) return null;
+        if (!(tag instanceof ItemInfo)) {
+            return activity.makeDefaultActivityOptions(
+                    -1 /* SPLASH_SCREEN_STYLE_UNDEFINED */).toBundle();
+        }
         Bundle bundle = activity.getActivityLaunchOptions(view, (ItemInfo) tag).toBundle();
         bundle.putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_EMPTY);
         return bundle;
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index 846dafd..c347939 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -17,9 +17,7 @@
 package com.android.launcher3.widget;
 
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 
-import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
@@ -226,15 +224,12 @@
     }
 
     private void animateOpen() {
-        if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+        if (mIsOpen || mOpenCloseAnimation.getAnimationPlayer().isRunning()) {
             return;
         }
         mIsOpen = true;
         setupNavBarColor();
-        mOpenCloseAnimator.setValues(
-                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-        mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN);
-        mOpenCloseAnimator.start();
+        setUpDefaultOpenAnimation().start();
     }
 
     @Override
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 33c4f8d..a4b605c 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -23,8 +23,6 @@
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 
 import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.content.pm.LauncherApps;
 import android.content.res.Configuration;
@@ -627,20 +625,13 @@
                 mContent.setAlpha(0);
                 setTranslationShift(VERTICAL_START_POSITION);
             }
-            mOpenCloseAnimator.setValues(
-                    PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-            mOpenCloseAnimator
-                    .setDuration(mActivityContext.getDeviceProfile().bottomSheetOpenDuration)
-                    .setInterpolator(AnimationUtils.loadInterpolator(
-                            getContext(), android.R.interpolator.linear_out_slow_in));
-            mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    mOpenCloseAnimator.removeListener(this);
-                }
-            });
+            setUpOpenAnimation(mActivityContext.getDeviceProfile().bottomSheetOpenDuration);
+            Animator animator = mOpenCloseAnimation.getAnimationPlayer();
+            animator.setInterpolator(AnimationUtils.loadInterpolator(
+                    getContext(), android.R.interpolator.linear_out_slow_in));
             post(() -> {
-                mOpenCloseAnimator.start();
+                animator.setDuration(mActivityContext.getDeviceProfile().bottomSheetOpenDuration)
+                        .start();
                 mContent.animate().alpha(1).setDuration(FADE_IN_DURATION);
             });
         } else {
@@ -774,7 +765,7 @@
         super.onCloseComplete();
         removeCallbacks(mShowEducationTipTask);
         if (mLatestEducationalTip != null) {
-            mLatestEducationalTip.close(false);
+            mLatestEducationalTip.close(true);
         }
         AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL);
     }
@@ -803,13 +794,15 @@
         }
 
         // Checks the orientation of the screen
-        if (LARGE_SCREEN_WIDGET_PICKER.get()
-                && mOrientation != newConfig.orientation
-                && mDeviceProfile.isTablet
-                && !mDeviceProfile.isTwoPanels) {
+        if (mOrientation != newConfig.orientation) {
             mOrientation = newConfig.orientation;
-            handleClose(false);
-            show(Launcher.getLauncher(getContext()), false);
+            if (LARGE_SCREEN_WIDGET_PICKER.get()
+                    && mDeviceProfile.isTablet && !mDeviceProfile.isTwoPanels) {
+                handleClose(false);
+                show(Launcher.getLauncher(getContext()), false);
+            } else {
+                reset();
+            }
         }
     }
 
@@ -891,6 +884,19 @@
         return false;
     }
 
+    /** Gets the sheet for widget picker, which is used for testing. */
+    @VisibleForTesting
+    public View getSheet() {
+        return mContent;
+    }
+
+    /** Opens the first header in widget picker and scrolls to the top of the RecyclerView. */
+    @VisibleForTesting
+    public void openFirstHeader() {
+        mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.selectFirstHeaderEntry();
+        mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop();
+    }
+
     /** A holder class for holding adapters & their corresponding recycler view. */
     final class AdapterHolder {
         static final int PRIMARY = 0;
diff --git a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
deleted file mode 100644
index ac0a166..0000000
--- a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.workspace
-
-import android.content.res.TypedArray
-import android.content.res.XmlResourceParser
-import android.util.AttributeSet
-import android.util.Log
-import android.util.TypedValue
-import android.util.Xml
-import com.android.launcher3.R
-import com.android.launcher3.util.ResourceHelper
-import java.io.IOException
-import kotlin.math.roundToInt
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
-
-private const val TAG = "WorkspaceSpecs"
-
-class WorkspaceSpecs(resourceHelper: ResourceHelper) {
-    object XmlTags {
-        const val WORKSPACE_SPECS = "workspaceSpecs"
-
-        const val WORKSPACE_SPEC = "workspaceSpec"
-        const val START_PADDING = "startPadding"
-        const val END_PADDING = "endPadding"
-        const val GUTTER = "gutter"
-        const val CELL_SIZE = "cellSize"
-    }
-
-    val workspaceHeightSpecList = mutableListOf<WorkspaceSpec>()
-    val workspaceWidthSpecList = mutableListOf<WorkspaceSpec>()
-
-    init {
-        try {
-            val parser: XmlResourceParser = resourceHelper.getXml()
-            val depth = parser.depth
-            var type: Int
-            while (
-                (parser.next().also { type = it } != XmlPullParser.END_TAG ||
-                    parser.depth > depth) && type != XmlPullParser.END_DOCUMENT
-            ) {
-                if (type == XmlPullParser.START_TAG && XmlTags.WORKSPACE_SPECS == parser.name) {
-                    val displayDepth = parser.depth
-                    while (
-                        (parser.next().also { type = it } != XmlPullParser.END_TAG ||
-                            parser.depth > displayDepth) && type != XmlPullParser.END_DOCUMENT
-                    ) {
-                        if (
-                            type == XmlPullParser.START_TAG && XmlTags.WORKSPACE_SPEC == parser.name
-                        ) {
-                            val attrs =
-                                resourceHelper.obtainStyledAttributes(
-                                    Xml.asAttributeSet(parser),
-                                    R.styleable.WorkspaceSpec
-                                )
-                            val maxAvailableSize =
-                                attrs.getDimensionPixelSize(
-                                    R.styleable.WorkspaceSpec_maxAvailableSize,
-                                    0
-                                )
-                            val specType =
-                                WorkspaceSpec.SpecType.values()[
-                                        attrs.getInt(
-                                            R.styleable.WorkspaceSpec_specType,
-                                            WorkspaceSpec.SpecType.HEIGHT.ordinal
-                                        )]
-                            attrs.recycle()
-
-                            var startPadding: SizeSpec? = null
-                            var endPadding: SizeSpec? = null
-                            var gutter: SizeSpec? = null
-                            var cellSize: SizeSpec? = null
-
-                            val limitDepth = parser.depth
-                            while (
-                                (parser.next().also { type = it } != XmlPullParser.END_TAG ||
-                                    parser.depth > limitDepth) && type != XmlPullParser.END_DOCUMENT
-                            ) {
-                                val attr: AttributeSet = Xml.asAttributeSet(parser)
-                                if (type == XmlPullParser.START_TAG) {
-                                    when (parser.name) {
-                                        XmlTags.START_PADDING -> {
-                                            startPadding = SizeSpec(resourceHelper, attr)
-                                        }
-                                        XmlTags.END_PADDING -> {
-                                            endPadding = SizeSpec(resourceHelper, attr)
-                                        }
-                                        XmlTags.GUTTER -> {
-                                            gutter = SizeSpec(resourceHelper, attr)
-                                        }
-                                        XmlTags.CELL_SIZE -> {
-                                            cellSize = SizeSpec(resourceHelper, attr)
-                                        }
-                                    }
-                                }
-                            }
-
-                            if (
-                                startPadding == null ||
-                                    endPadding == null ||
-                                    gutter == null ||
-                                    cellSize == null
-                            ) {
-                                throw IllegalStateException(
-                                    "All attributes in workspaceSpec must be defined"
-                                )
-                            }
-
-                            val workspaceSpec =
-                                WorkspaceSpec(
-                                    maxAvailableSize,
-                                    specType,
-                                    startPadding,
-                                    endPadding,
-                                    gutter,
-                                    cellSize
-                                )
-                            if (workspaceSpec.isValid()) {
-                                if (workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT)
-                                    workspaceHeightSpecList.add(workspaceSpec)
-                                else workspaceWidthSpecList.add(workspaceSpec)
-                            } else {
-                                throw IllegalStateException("Invalid workspaceSpec found.")
-                            }
-                        }
-                    }
-
-                    if (workspaceWidthSpecList.isEmpty() || workspaceHeightSpecList.isEmpty()) {
-                        throw IllegalStateException(
-                            "WorkspaceSpecs is incomplete - " +
-                                "height list size = ${workspaceHeightSpecList.size}; " +
-                                "width list size = ${workspaceWidthSpecList.size}."
-                        )
-                    }
-                }
-            }
-            parser.close()
-        } catch (e: Exception) {
-            when (e) {
-                is IOException,
-                is XmlPullParserException -> {
-                    throw RuntimeException("Failure parsing workspaces specs file.", e)
-                }
-                else -> throw e
-            }
-        }
-    }
-
-    /**
-     * Returns the CalculatedWorkspaceSpec for width, based on the available width and the
-     * WorkspaceSpecs.
-     */
-    fun getCalculatedWidthSpec(columns: Int, availableWidth: Int): CalculatedWorkspaceSpec {
-        val widthSpec = workspaceWidthSpecList.first { availableWidth <= it.maxAvailableSize }
-
-        return CalculatedWorkspaceSpec(availableWidth, columns, widthSpec)
-    }
-
-    /**
-     * Returns the CalculatedWorkspaceSpec for height, based on the available height and the
-     * WorkspaceSpecs.
-     */
-    fun getCalculatedHeightSpec(rows: Int, availableHeight: Int): CalculatedWorkspaceSpec {
-        val heightSpec = workspaceHeightSpecList.first { availableHeight <= it.maxAvailableSize }
-
-        return CalculatedWorkspaceSpec(availableHeight, rows, heightSpec)
-    }
-}
-
-class CalculatedWorkspaceSpec(
-    val availableSpace: Int,
-    val cells: Int,
-    val workspaceSpec: WorkspaceSpec
-) {
-    var startPaddingPx: Int = 0
-        private set
-    var endPaddingPx: Int = 0
-        private set
-    var gutterPx: Int = 0
-        private set
-    var cellSizePx: Int = 0
-        private set
-    init {
-        // Calculate all fixed size first
-        if (workspaceSpec.startPadding.fixedSize > 0)
-            startPaddingPx = workspaceSpec.startPadding.fixedSize.roundToInt()
-        if (workspaceSpec.endPadding.fixedSize > 0)
-            endPaddingPx = workspaceSpec.endPadding.fixedSize.roundToInt()
-        if (workspaceSpec.gutter.fixedSize > 0)
-            gutterPx = workspaceSpec.gutter.fixedSize.roundToInt()
-        if (workspaceSpec.cellSize.fixedSize > 0)
-            cellSizePx = workspaceSpec.cellSize.fixedSize.roundToInt()
-
-        // Calculate all available space next
-        if (workspaceSpec.startPadding.ofAvailableSpace > 0)
-            startPaddingPx =
-                (workspaceSpec.startPadding.ofAvailableSpace * availableSpace).roundToInt()
-        if (workspaceSpec.endPadding.ofAvailableSpace > 0)
-            endPaddingPx = (workspaceSpec.endPadding.ofAvailableSpace * availableSpace).roundToInt()
-        if (workspaceSpec.gutter.ofAvailableSpace > 0)
-            gutterPx = (workspaceSpec.gutter.ofAvailableSpace * availableSpace).roundToInt()
-        if (workspaceSpec.cellSize.ofAvailableSpace > 0)
-            cellSizePx = (workspaceSpec.cellSize.ofAvailableSpace * availableSpace).roundToInt()
-
-        // Calculate remainder space last
-        val gutters = cells - 1
-        val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
-        val remainderSpace = availableSpace - usedSpace
-        if (workspaceSpec.startPadding.ofRemainderSpace > 0)
-            startPaddingPx =
-                (workspaceSpec.startPadding.ofRemainderSpace * remainderSpace).roundToInt()
-        if (workspaceSpec.endPadding.ofRemainderSpace > 0)
-            endPaddingPx = (workspaceSpec.endPadding.ofRemainderSpace * remainderSpace).roundToInt()
-        if (workspaceSpec.gutter.ofRemainderSpace > 0)
-            gutterPx = (workspaceSpec.gutter.ofRemainderSpace * remainderSpace).roundToInt()
-        if (workspaceSpec.cellSize.ofRemainderSpace > 0)
-            cellSizePx = (workspaceSpec.cellSize.ofRemainderSpace * remainderSpace).roundToInt()
-    }
-}
-
-data class WorkspaceSpec(
-    val maxAvailableSize: Int,
-    val specType: SpecType,
-    val startPadding: SizeSpec,
-    val endPadding: SizeSpec,
-    val gutter: SizeSpec,
-    val cellSize: SizeSpec
-) {
-
-    enum class SpecType {
-        HEIGHT,
-        WIDTH
-    }
-
-    fun isValid(): Boolean {
-        if (maxAvailableSize <= 0) {
-            Log.e(TAG, "WorkspaceSpec#isValid - maxAvailableSize <= 0")
-            return false
-        }
-
-        // All specs need to be individually valid
-        if (!allSpecsAreValid()) {
-            Log.e(TAG, "WorkspaceSpec#isValid - !allSpecsAreValid()")
-            return false
-        }
-
-        return true
-    }
-
-    private fun allSpecsAreValid(): Boolean =
-        startPadding.isValid() && endPadding.isValid() && gutter.isValid() && cellSize.isValid()
-}
-
-class SizeSpec(resourceHelper: ResourceHelper, attrs: AttributeSet) {
-    val fixedSize: Float
-    val ofAvailableSpace: Float
-    val ofRemainderSpace: Float
-
-    init {
-        val styledAttrs = resourceHelper.obtainStyledAttributes(attrs, R.styleable.SpecSize)
-
-        fixedSize = getValue(styledAttrs, R.styleable.SpecSize_fixedSize)
-        ofAvailableSpace = getValue(styledAttrs, R.styleable.SpecSize_ofAvailableSpace)
-        ofRemainderSpace = getValue(styledAttrs, R.styleable.SpecSize_ofRemainderSpace)
-
-        styledAttrs.recycle()
-    }
-
-    private fun getValue(a: TypedArray, index: Int): Float {
-        if (a.getType(index) == TypedValue.TYPE_DIMENSION) {
-            return a.getDimensionPixelSize(index, 0).toFloat()
-        } else if (a.getType(index) == TypedValue.TYPE_FLOAT) {
-            return a.getFloat(index, 0f)
-        }
-        return 0f
-    }
-
-    fun isValid(): Boolean {
-        // All attributes are empty
-        if (fixedSize < 0f && ofAvailableSpace <= 0f && ofRemainderSpace <= 0f) {
-            Log.e(TAG, "SizeSpec#isValid - all attributes are empty")
-            return false
-        }
-
-        // More than one attribute is filled
-        val attrCount =
-            (if (fixedSize > 0) 1 else 0) +
-                (if (ofAvailableSpace > 0) 1 else 0) +
-                (if (ofRemainderSpace > 0) 1 else 0)
-        if (attrCount > 1) {
-            Log.e(TAG, "SizeSpec#isValid - more than one attribute is filled")
-            return false
-        }
-
-        // Values should be between 0 and 1
-        if (ofAvailableSpace !in 0f..1f || ofRemainderSpace !in 0f..1f) {
-            Log.e(TAG, "SizeSpec#isValid - values should be between 0 and 1")
-            return false
-        }
-
-        return true
-    }
-
-    override fun toString(): String {
-        return "SizeSpec(fixedSize=$fixedSize, ofAvailableSpace=$ofAvailableSpace, " +
-            "ofRemainderSpace=$ofRemainderSpace)"
-    }
-}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
index d6b41c9..599a591 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.uioverrides;
 
+import android.app.ActivityOptions;
 import android.app.Person;
 import android.content.Context;
 import android.content.pm.LauncherActivityInfo;
@@ -40,4 +41,11 @@
     public static Map<String, LauncherActivityInfo> getActivityOverrides(Context context) {
         return Collections.emptyMap();
     }
+
+    /**
+     * Creates an ActivityOptions to play fade-out animation on closing targets
+     */
+    public static ActivityOptions createFadeOutAnimOptions(Context context) {
+        return ActivityOptions.makeCustomAnimation(context, 0, android.R.anim.fade_out);
+    }
 }
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 772a995..b62dbd1 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -15,7 +15,7 @@
  */
 package com.android.launcher3.uioverrides.states;
 
-import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.app.animation.Interpolators.DECELERATE;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS;
 
 import android.content.Context;
@@ -80,7 +80,7 @@
     @Override
     public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
         PageAlphaProvider superPageAlphaProvider = super.getWorkspacePageAlphaProvider(launcher);
-        return new PageAlphaProvider(DEACCEL_2) {
+        return new PageAlphaProvider(DECELERATE) {
             @Override
             public float getPageAlpha(int pageIndex) {
                 return launcher.getDeviceProfile().isTablet
diff --git a/tests/Android.bp b/tests/Android.bp
index e7f4084..5a52440 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -50,14 +50,13 @@
       "src/com/android/launcher3/util/Wait.java",
       "src/com/android/launcher3/util/WidgetUtils.java",
       "src/com/android/launcher3/util/rule/FailureWatcher.java",
-      "src/com/android/launcher3/util/rule/LauncherActivityRule.java",
       "src/com/android/launcher3/util/rule/ViewCaptureRule.kt",
       "src/com/android/launcher3/util/rule/SamplerRule.java",
       "src/com/android/launcher3/util/rule/ScreenRecordRule.java",
       "src/com/android/launcher3/util/rule/ShellCommandRule.java",
-      "src/com/android/launcher3/util/rule/SimpleActivityRule.java",
       "src/com/android/launcher3/util/rule/TestStabilityRule.java",
       "src/com/android/launcher3/util/rule/TISBindRule.java",
+      "src/com/android/launcher3/util/viewcapture_analysis/*.java",
       "src/com/android/launcher3/testcomponent/BaseTestingActivity.java",
       "src/com/android/launcher3/testcomponent/OtherBaseTestingActivity.java",
       "src/com/android/launcher3/testcomponent/CustomShortcutConfigActivity.java",
@@ -87,6 +86,7 @@
         "launcher_log_protos_lite",
         "truth-prebuilt",
         "platform-test-rules",
+        "testables",
     ],
     manifest: "AndroidManifest-common.xml",
     platform_apis: true,
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index b170061..ee151bb 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -32,6 +32,7 @@
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWidgetNoConfig"
             android:exported="true"
+            android:icon="@drawable/test_widget_no_config_icon"
             android:label="No Config">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -41,6 +42,17 @@
         </receiver>
 
         <receiver
+            android:name="com.android.launcher3.testcomponent.AppWidgetNoConfigLarge"
+            android:exported="true"
+            android:label="No Config Large">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+            </intent-filter>
+            <meta-data android:name="android.appwidget.provider"
+                android:resource="@xml/appwidget_no_config_large"/>
+        </receiver>
+
+        <receiver
             android:name="com.android.launcher3.testcomponent.AppWdigetHidden"
             android:exported="true"
             android:label="Hidden widget">
@@ -54,6 +66,7 @@
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWidgetWithConfig"
             android:exported="true"
+            android:icon="@drawable/test_widget_with_config_icon"
             android:label="With Config">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -65,6 +78,7 @@
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWidgetWithDialog"
             android:exported="true"
+            android:icon="@drawable/test_widget_with_dialog_icon"
             android:label="With Dialog">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -76,6 +90,7 @@
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWidgetDynamicColors"
             android:exported="true"
+            android:icon="@drawable/test_widget_dynamic_colors_icon"
             android:label="Dynamic Colors">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -288,6 +303,28 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+        <activity-alias android:name="MaxShortcutsActivity"
+            android:label="TestActivityMaxShortcuts"
+            android:exported="true"
+            android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+            <meta-data android:name="android.app.shortcuts"
+                android:resource="@xml/max_shortcuts"/>
+        </activity-alias>
+        <activity-alias android:name="SingleShortcutActivity"
+            android:label="TestActivitySingleShortcut"
+            android:exported="true"
+            android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+            <meta-data android:name="android.app.shortcuts"
+                android:resource="@xml/single_shortcut"/>
+        </activity-alias>
         <activity
             android:name="com.android.launcher3.testcomponent.DialogTestActivity"
             android:label="Dialog Activity"
@@ -308,6 +345,15 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <activity-alias android:name="WebSearchActivity"
+            android:label="WebSearchActivity"
+            android:exported="true"
+            android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.WEB_SEARCH" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity-alias>
 
         <!-- [b/197780098] Disable eager initialization of Jetpack libraries. -->
         <provider
diff --git a/tests/assets/ReorderWidgets/multiple_cell_layouts_no_space_reorder b/tests/assets/ReorderWidgets/multiple_cell_layouts_no_space_reorder
new file mode 100644
index 0000000..c6d65f8
--- /dev/null
+++ b/tests/assets/ReorderWidgets/multiple_cell_layouts_no_space_reorder
@@ -0,0 +1,56 @@
+###################################################################################################
+# This file contains test case composed of the following tags:
+#     * # (coments): Lines starting with this character would be ignored.
+#     * arguments: is set of words separated by spaces that can later be parsed
+#     * board: represent a workspace, the first line is the dimensions of the board width x height (wxh)
+# There are different characters on the board that represent different things:
+#     * x: The x character represents spaces that would be ignored, for example it can be used in
+#          the first row if we don't know how wide the smartspace is.
+#     * i: Represents an icon on the workspace, none in particular just an icon
+#     * [a-z]: Represents a widget and it can be any number or character
+#          except any other already in use. The whole continuos are of the same character is the
+#          area of the widget.
+#     * [A-Z]: Represents a folder and number of icons in the folder is represented by the order of
+#          letter in the alphabet, A=2, B=3, C=4 ... etc.
+# Test are parsed by CellLayoutTestCaseReader.java and boards are parsed by CellLayoutBoard.java
+###################################################################################################
+# 5x5 Test
+board: 5x5
+xxxxx|aeeee
+--mm-|acccc
+--mm-|acccc
+ggggg|acccc
+ggggg|adddd
+arguments: 7 1
+board: 10x5
+xxxxx|aeeee
+---mm|acccc
+---mm|acccc
+ggggg|acccc
+ggggg|adddd
+# 4x4 Test
+board: 4x4
+xxxx|aeee
+--mm|accc
+--mm|accc
+gggg|accc
+arguments: 5 1
+board: 8x4
+xxxx|aeee
+--mm|accc
+--mm|accc
+gggg|accc
+# 6x5 Test
+board: 6x5
+xxxxxx|aeeeee
+--mm--|accccc
+--mm--|accccc
+gggggg|accccc
+gggggg|addddd
+arguments: 8 1
+board: 12x5
+xxxxxx|aeeeee
+----mm|accccc
+----mm|accccc
+gggggg|accccc
+gggggg|addddd
diff --git a/tests/assets/ReorderWidgets/multiple_cell_layouts_reorder_other_side b/tests/assets/ReorderWidgets/multiple_cell_layouts_reorder_other_side
new file mode 100644
index 0000000..376638e
--- /dev/null
+++ b/tests/assets/ReorderWidgets/multiple_cell_layouts_reorder_other_side
@@ -0,0 +1,56 @@
+###################################################################################################
+# This file contains test case composed of the following tags:
+#     * # (coments): Lines starting with this character would be ignored.
+#     * arguments: is set of words separated by spaces that can later be parsed
+#     * board: represent a workspace, the first line is the dimensions of the board width x height (wxh)
+# There are different characters on the board that represent different things:
+#     * x: The x character represents spaces that would be ignored, for example it can be used in
+#          the first row if we don't know how wide the smartspace is.
+#     * i: Represents an icon on the workspace, none in particular just an icon
+#     * [a-z]: Represents a widget and it can be any number or character
+#          except any other already in use. The whole continuos are of the same character is the
+#          area of the widget.
+#     * [A-Z]: Represents a folder and number of icons in the folder is represented by the order of
+#          letter in the alphabet, A=2, B=3, C=4 ... etc.
+# Test are parsed by CellLayoutTestCaseReader.java and boards are parsed by CellLayoutBoard.java
+###################################################################################################
+# 5x5 Test
+board: 5x5
+xxxxx|aaaaa
+--mm-|plllh
+--mm-|piiih
+ggggg|piiih
+ggggg|fffff
+arguments: 7 1
+board: 10x5
+xxxxx|aaaaa
+--lll|p-mmh
+---ii|pimmh
+ggggg|piiih
+ggggg|fffff
+# 4x4 Test
+board: 4x4
+xxxx|aaaa
+--mm|pllh
+--mm|piih
+gggg|ffff
+arguments: 5 1
+board: 8x4
+xxxx|aaaa
+--ll|pmmh
+--ii|pmmh
+gggg|ffff
+# 6x5 Test
+board: 6x5
+xxxxxx|aaaaaa
+--mmm-|pllllh
+--mmm-|piiiih
+--mmm-|piiiih
+gggggg|ffffff
+arguments: 8 1
+board: 12x5
+xxxxxx|aaaaaa
+--llll|p-mmmh
+---iii|pimmmh
+---iii|pimmmh
+gggggg|ffffff
\ No newline at end of file
diff --git a/tests/assets/ReorderWidgets/multiple_cell_layouts_simple_reorder b/tests/assets/ReorderWidgets/multiple_cell_layouts_simple_reorder
new file mode 100644
index 0000000..44301d2
--- /dev/null
+++ b/tests/assets/ReorderWidgets/multiple_cell_layouts_simple_reorder
@@ -0,0 +1,56 @@
+###################################################################################################
+# This file contains test case composed of the following tags:
+#     * # (coments): Lines starting with this character would be ignored.
+#     * arguments: is set of words separated by spaces that can later be parsed
+#     * board: represent a workspace, the first line is the dimensions of the board width x height (wxh)
+# There are different characters on the board that represent different things:
+#     * x: The x character represents spaces that would be ignored, for example it can be used in
+#          the first row if we don't know how wide the smartspace is.
+#     * i: Represents an icon on the workspace, none in particular just an icon
+#     * [a-z]: Represents a widget and it can be any number or character
+#          except any other already in use. The whole continuos are of the same character is the
+#          area of the widget.
+#     * [A-Z]: Represents a folder and number of icons in the folder is represented by the order of
+#          letter in the alphabet, A=2, B=3, C=4 ... etc.
+# Test are parsed by CellLayoutTestCaseReader.java and boards are parsed by CellLayoutBoard.java
+###################################################################################################
+# 5x5 Test
+board: 5x5
+xxxxx|-----
+--mm-|-----
+--mm-|-----
+-----|-----
+-----|-----
+arguments: 8 3
+board: 10x5
+xxxxx|-----
+-----|-----
+-----|-----
+-----|---mm
+-----|---mm
+# 4x4 Test
+board: 4x4
+xxxx|----
+--mm|----
+--mm|----
+----|----
+arguments: 5 3
+board: 8x4
+xxxx|----
+----|----
+----|-mm-
+----|-mm-
+# 6x5 Test
+board: 6x5
+xxxxxx|------
+--m---|------
+------|------
+------|------
+------|------
+arguments: 10 4
+board: 12x5
+xxxxxx|------
+------|------
+------|------
+------|------
+------|----m-
\ No newline at end of file
diff --git a/tests/res/drawable/test_widget_dynamic_colors_icon.xml b/tests/res/drawable/test_widget_dynamic_colors_icon.xml
new file mode 100644
index 0000000..69f6675
--- /dev/null
+++ b/tests/res/drawable/test_widget_dynamic_colors_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2023 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#964B00"/>
+    </foreground>
+    <monochrome>
+        <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
+            <path
+                android:fillColor="#FF000000"
+                android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
+        </vector>
+    </monochrome>
+</adaptive-icon>
diff --git a/tests/res/drawable/test_widget_no_config_icon.xml b/tests/res/drawable/test_widget_no_config_icon.xml
new file mode 100644
index 0000000..e3d0125
--- /dev/null
+++ b/tests/res/drawable/test_widget_no_config_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2023 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#00FFFF"/>
+    </foreground>
+    <monochrome>
+        <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
+            <path
+                android:fillColor="#FF000000"
+                android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
+        </vector>
+    </monochrome>
+</adaptive-icon>
diff --git a/tests/res/drawable/test_widget_with_config_icon.xml b/tests/res/drawable/test_widget_with_config_icon.xml
new file mode 100644
index 0000000..98b797b
--- /dev/null
+++ b/tests/res/drawable/test_widget_with_config_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2023 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#008000" />
+    </foreground>
+    <monochrome>
+        <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
+            <path
+                android:fillColor="#FF000000"
+                android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
+        </vector>
+    </monochrome>
+</adaptive-icon>
diff --git a/tests/res/drawable/test_widget_with_dialog_icon.xml b/tests/res/drawable/test_widget_with_dialog_icon.xml
new file mode 100644
index 0000000..d2879d2
--- /dev/null
+++ b/tests/res/drawable/test_widget_with_dialog_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2023 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#800080"/>
+    </foreground>
+    <monochrome>
+        <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
+            <path
+                android:fillColor="#FF000000"
+                android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
+        </vector>
+    </monochrome>
+</adaptive-icon>
diff --git a/tests/res/raw/cache_data_updated_task_data.txt b/tests/res/raw/cache_data_updated_task_data.txt
deleted file mode 100644
index 603dbe3..0000000
--- a/tests/res/raw/cache_data_updated_task_data.txt
+++ /dev/null
@@ -1,28 +0,0 @@
-# Model data used by CacheDataUpdatedTaskTest
-
-classMap s com.android.launcher3.model.data.WorkspaceItemInfo
-
-# Items for the BgDataModel
-
-# App shortcuts
-bgItem s itemType=0 title=app1-class1 intent=component=app1/class1 id=1
-bgItem s itemType=0 title=app1-class2 intent=component=app1/class2 id=2
-bgItem s itemType=0 title=app2-class1 intent=component=app2/class1 id=3
-bgItem s itemType=0 title=app2-class2 intent=component=app2/class2 id=4
-
-# Auto install app shortcut
-bgItem s itemType=0 status=2 title=app3-class1 intent=component=app3/class1 id=5
-bgItem s itemType=0 status=2 title=app3-class2 intent=component=app3/class2 id=6
-
-# Custom shortcuts
-bgItem s itemType=1 title=app1-shrt intent=component=app1/class3 id=7
-bgItem s itemType=1 title=app4-shrt intent=component=app4/class1 id=8
-
-# Restored custom shortcut
-bgItem s itemType=1 status=1 title=app3-shrt intent=component=app3/class3 id=9
-bgItem s itemType=1 status=1 title=app5-shrt intent=component=app5/class1 id=10
-
-allApps componentName=app1/class1 intent=component=app1/class1
-allApps componentName=app1/class2 intent=component=app1/class2
-allApps componentName=app2/class1 intent=component=app2/class1
-allApps componentName=app2/class2 intent=component=app2/class2
\ No newline at end of file
diff --git a/tests/res/raw/package_install_state_change_task_data.txt b/tests/res/raw/package_install_state_change_task_data.txt
deleted file mode 100644
index e82ea9d..0000000
--- a/tests/res/raw/package_install_state_change_task_data.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-# Model data used by PackageInstallStateChangeTaskTest
-
-classMap s com.android.launcher3.model.data.WorkspaceItemInfo
-classMap w com.android.launcher3.model.data.LauncherAppWidgetInfo
-
-# Items for the BgDataModel
-
-# App shortcuts
-bgItem s itemType=0 title=app1-class1 intent=component=app1/class1 id=1
-bgItem s itemType=0 title=app1-class2 intent=component=app1/class2 id=2
-bgItem s itemType=0 title=app2-class1 intent=component=app2/class1 id=3
-bgItem s itemType=0 title=app2-class2 intent=component=app2/class2 id=4
-
-# Promise icons for app3
-bgItem s itemType=0 status=2 title=app3-class1 intent=component=app3/class1 id=5
-bgItem s itemType=0 status=2 title=app3-class2 intent=component=app3/class2 id=6
-bgItem s itemType=1 status=1 title=app3-shrt intent=component=app3/class3 id=7
-
-# Promise icon for app4
-bgItem s itemType=1 status=1 title=app4-shrt intent=component=app4/class1 id=8
-
-# Widget
-bgItem w providerName=app4/provider1 id=9
-bgItem w providerName=app5/provider1 id=10
\ No newline at end of file
diff --git a/tests/res/raw/widgets_predication_update_task_data.txt b/tests/res/raw/widgets_predication_update_task_data.txt
deleted file mode 100644
index 941d195..0000000
--- a/tests/res/raw/widgets_predication_update_task_data.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-# Model data used by WidgetsPredictionUpdateTasksTest
-
-classMap s com.android.launcher3.model.data.WorkspaceItemInfo
-classMap w com.android.launcher3.model.data.LauncherAppWidgetInfo
-
-# Items for the BgDataModel
-
-# App shortcuts
-bgItem s itemType=0 title=app1-class1 intent=component=app1/class1 id=1
-bgItem s itemType=0 title=app1-class2 intent=component=app1/class2 id=2
-bgItem s itemType=0 title=app2-class1 intent=component=app2/class1 id=3
-bgItem s itemType=0 title=app2-class2 intent=component=app2/class2 id=4
-
-# Promise icons for app3
-bgItem s itemType=0 status=2 title=app3-class1 intent=component=app3/class1 id=5
-bgItem s itemType=0 status=2 title=app3-class2 intent=component=app3/class2 id=6
-bgItem s itemType=1 status=1 title=app3-shrt intent=component=app3/class3 id=7
-
-# Promise icon for app4
-bgItem s itemType=1 status=1 title=app4-shrt intent=component=app4/class1 id=8
-
-# Widget
-bgItem w providerName=app4/provider1 id=9
-bgItem w providerName=app5/provider1 id=10
\ No newline at end of file
diff --git a/tests/res/values/attrs.xml b/tests/res/values/attrs.xml
index 2310d9e..e5ee064 100644
--- a/tests/res/values/attrs.xml
+++ b/tests/res/values/attrs.xml
@@ -18,7 +18,7 @@
 <!-- Attributes have to be copied to test for correct parsing of files -->
 <resources>
     <!--  Responsive grids attributes  -->
-    <declare-styleable name="WorkspaceSpec">
+    <declare-styleable name="ResponsiveSpec">
         <attr name="specType" format="integer">
             <enum name="height" value="0" />
             <enum name="width" value="1" />
@@ -26,10 +26,26 @@
         <attr name="maxAvailableSize" format="dimension" />
     </declare-styleable>
 
-    <declare-styleable name="SpecSize">
+    <declare-styleable name="WorkspaceSpec">
+        <attr name="specType" />
+        <attr name="maxAvailableSize" />
+    </declare-styleable>
+
+    <declare-styleable name="FolderSpec">
+        <attr name="specType" />
+        <attr name="maxAvailableSize" />
+    </declare-styleable>
+
+    <declare-styleable name="AllAppsSpec">
+        <attr name="specType" />
+        <attr name="maxAvailableSize" />
+    </declare-styleable>
+
+    <declare-styleable name="SizeSpec">
         <attr name="fixedSize" format="dimension" />
         <attr name="ofAvailableSpace" format="float" />
         <attr name="ofRemainderSpace" format="float" />
+        <attr name="matchWorkspace" format="boolean" />
+        <attr name="maxSize" format="dimension" />
     </declare-styleable>
-
 </resources>
diff --git a/tests/res/values/strings.xml b/tests/res/values/strings.xml
index 0ad87fb..54ade56 100644
--- a/tests/res/values/strings.xml
+++ b/tests/res/values/strings.xml
@@ -3,4 +3,5 @@
     <string name="shortcut1" translatable="false">Shortcut 1</string>
     <string name="shortcut2" translatable="false">Shortcut 2</string>
     <string name="shortcut3" translatable="false">Shortcut 3</string>
+    <string name="shortcut4" translatable="false">Shortcut 4</string>
 </resources>
diff --git a/tests/res/xml/appwidget_no_config_large.xml b/tests/res/xml/appwidget_no_config_large.xml
new file mode 100644
index 0000000..b3b69d9
--- /dev/null
+++ b/tests/res/xml/appwidget_no_config_large.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:minWidth="1dp"
+    android:minHeight="1dp"
+    android:minResizeWidth="1dp"
+    android:maxResizeWidth="3000dp"
+    android:targetCellHeight="1"
+    android:targetCellWidth="5"
+    android:updatePeriodMillis="86400000"
+    android:initialLayout="@layout/test_layout_appwidget_red"
+    android:previewLayout="@layout/test_layout_appwidget_red"
+    android:resizeMode="horizontal|vertical"
+    android:widgetCategory="home_screen">
+</appwidget-provider>
\ No newline at end of file
diff --git a/tests/res/xml/invalid_all_apps_file_case_1.xml b/tests/res/xml/invalid_all_apps_file_case_1.xml
new file mode 100644
index 0000000..6fd35b1
--- /dev/null
+++ b/tests/res/xml/invalid_all_apps_file_case_1.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<allAppsSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <allAppsSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="9999dp">
+        <!--  missing startPadding  -->
+        <endPadding launcher:fixedSize="0dp" />
+        <gutter launcher:matchWorkspace="true" />
+        <cellSize launcher:matchWorkspace="true" />
+    </allAppsSpec>
+
+    <allAppsSpec
+        launcher:specType="width"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:matchWorkspace="true" />
+        <endPadding launcher:matchWorkspace="true" />
+        <gutter launcher:matchWorkspace="true" />
+        <cellSize launcher:matchWorkspace="true" />
+    </allAppsSpec>
+
+</allAppsSpecs>
+
diff --git a/tests/res/xml/invalid_all_apps_file_case_2.xml b/tests/res/xml/invalid_all_apps_file_case_2.xml
new file mode 100644
index 0000000..de9c1ac
--- /dev/null
+++ b/tests/res/xml/invalid_all_apps_file_case_2.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<allAppsSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <allAppsSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="0dp" />
+        <endPadding launcher:fixedSize="0dp" />
+        <!--  more than 1 value in one tag -->
+        <gutter
+            launcher:matchWorkspace="true"
+            launcher:fixedSize="16dp" />
+        <cellSize launcher:matchWorkspace="true" />
+    </allAppsSpec>
+
+    <allAppsSpec
+        launcher:specType="width"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:matchWorkspace="true" />
+        <endPadding launcher:matchWorkspace="true" />
+        <gutter launcher:matchWorkspace="true" />
+        <cellSize launcher:matchWorkspace="true" />
+    </allAppsSpec>
+
+</allAppsSpecs>
\ No newline at end of file
diff --git a/tests/res/xml/invalid_all_apps_file_case_3.xml b/tests/res/xml/invalid_all_apps_file_case_3.xml
new file mode 100644
index 0000000..7af0af4
--- /dev/null
+++ b/tests/res/xml/invalid_all_apps_file_case_3.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<allAppsSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <allAppsSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="0dp" />
+        <endPadding launcher:fixedSize="0dp" />
+        <gutter launcher:matchWorkspace="true" />
+        <!--  value bigger than 1 -->
+        <cellSize launcher:ofRemainderSpace="1.001" />
+    </allAppsSpec>
+
+    <allAppsSpec
+        launcher:specType="width"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:matchWorkspace="true" />
+        <endPadding launcher:matchWorkspace="true" />
+        <gutter launcher:matchWorkspace="true" />
+        <cellSize launcher:matchWorkspace="true" />
+    </allAppsSpec>
+
+</allAppsSpecs>
+
diff --git a/tests/res/xml/invalid_folders_specs_1.xml b/tests/res/xml/invalid_folders_specs_1.xml
new file mode 100644
index 0000000..0864249
--- /dev/null
+++ b/tests/res/xml/invalid_folders_specs_1.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- Tablet - 6x5 portrait -->
+<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
+        <!--  missing startPadding  -->
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:matchWorkspace="true" />
+    </folderSpec>
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="102dp" />
+    </folderSpec>
+
+    <!-- Height spec is fixed -->
+    <folderSpec launcher:specType="height" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="24dp" />
+        <!-- mapped to footer height size -->
+        <endPadding launcher:fixedSize="64dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="104dp" />
+    </folderSpec>
+</folderSpecs>
diff --git a/tests/res/xml/invalid_folders_specs_2.xml b/tests/res/xml/invalid_folders_specs_2.xml
new file mode 100644
index 0000000..0b7dd62
--- /dev/null
+++ b/tests/res/xml/invalid_folders_specs_2.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- Tablet - 6x5 portrait -->
+<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <!--  more than 1 value in one tag -->
+        <gutter
+            launcher:ofAvailableSpace="0.0125"
+            launcher:fixedSize="16dp" />
+        <cellSize launcher:matchWorkspace="true" />
+    </folderSpec>
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="102dp" />
+    </folderSpec>
+
+    <!-- Height spec is fixed -->
+    <folderSpec launcher:specType="height" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="24dp" />
+        <!-- mapped to footer height size -->
+        <endPadding launcher:fixedSize="64dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="104dp" />
+    </folderSpec>
+</folderSpecs>
diff --git a/tests/res/xml/invalid_folders_specs_3.xml b/tests/res/xml/invalid_folders_specs_3.xml
new file mode 100644
index 0000000..83fd3e1
--- /dev/null
+++ b/tests/res/xml/invalid_folders_specs_3.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- Tablet - 6x5 portrait - More the one value first gutter -->
+<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <!--  value bigger than 1 -->
+        <cellSize launcher:ofRemainderSpace="1.001" />
+    </folderSpec>
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="102dp" />
+    </folderSpec>
+
+    <!-- Height spec is fixed -->
+    <folderSpec launcher:specType="height" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="24dp" />
+        <!-- mapped to footer height size -->
+        <endPadding launcher:fixedSize="64dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="104dp" />
+    </folderSpec>
+</folderSpecs>
diff --git a/tests/res/xml/invalid_folders_specs_4.xml b/tests/res/xml/invalid_folders_specs_4.xml
new file mode 100644
index 0000000..2d8c730
--- /dev/null
+++ b/tests/res/xml/invalid_folders_specs_4.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<!-- missing height spec -->
+<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:matchWorkspace="true" />
+    </folderSpec>
+</folderSpecs>
diff --git a/tests/res/xml/invalid_folders_specs_5.xml b/tests/res/xml/invalid_folders_specs_5.xml
new file mode 100644
index 0000000..b4f1f4d
--- /dev/null
+++ b/tests/res/xml/invalid_folders_specs_5.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<!-- missing breakpoints > 800dp -->
+<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:matchWorkspace="true" />
+    </folderSpec>
+
+    <!-- Height spec is fixed -->
+    <folderSpec launcher:specType="height" launcher:maxAvailableSize="800dp">
+        <startPadding launcher:fixedSize="24dp" />
+        <!-- mapped to footer height size -->
+        <endPadding launcher:fixedSize="64dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="104dp" />
+    </folderSpec>
+</folderSpecs>
diff --git a/tests/res/xml/invalid_hotseat_file_case_1.xml b/tests/res/xml/invalid_hotseat_file_case_1.xml
new file mode 100644
index 0000000..fcbc5ea
--- /dev/null
+++ b/tests/res/xml/invalid_hotseat_file_case_1.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<hotseatSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+
+    <hotseatSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="847dp">
+        <hotseatQsbSpace launcher:ofAvailableSpace="1" />
+    </hotseatSpec>
+
+    <hotseatSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="9999dp">
+        <hotseatQsbSpace launcher:fixedSize="36dp" />
+    </hotseatSpec>
+
+</hotseatSpecs>
\ No newline at end of file
diff --git a/tests/res/xml/invalid_workspace_file_case_4.xml b/tests/res/xml/invalid_workspace_file_case_4.xml
new file mode 100644
index 0000000..9e74c85
--- /dev/null
+++ b/tests/res/xml/invalid_workspace_file_case_4.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<workspaceSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <workspaceSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="648dp">
+        <startPadding
+            launcher:ofAvailableSpace="0.0125" />
+        <endPadding
+            launcher:ofAvailableSpace="0.05" />
+        <!--  value in workspace spec using matchWorkspace -->
+        <gutter
+            launcher:matchWorkspace="true" />
+        <cellSize
+            launcher:ofRemainderSpace="0.2" />
+    </workspaceSpec>
+
+    <workspaceSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding
+            launcher:ofAvailableSpace="0.0306" />
+        <endPadding
+            launcher:ofAvailableSpace="0.068" />
+        <gutter
+            launcher:fixedSize="16dp" />
+        <cellSize
+            launcher:ofRemainderSpace="0.2" />
+    </workspaceSpec>
+
+    <!-- Width spec is always the same -->
+    <workspaceSpec
+        launcher:specType="width"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding
+            launcher:ofRemainderSpace="0.21436227" />
+        <endPadding
+            launcher:ofRemainderSpace="0.21436227" />
+        <gutter
+            launcher:ofRemainderSpace="0.11425509" />
+        <cellSize
+            launcher:fixedSize="120dp" />
+    </workspaceSpec>
+</workspaceSpecs>
diff --git a/tests/res/xml/max_shortcuts.xml b/tests/res/xml/max_shortcuts.xml
new file mode 100644
index 0000000..312a24c
--- /dev/null
+++ b/tests/res/xml/max_shortcuts.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+    <shortcut
+        android:shortcutId="max_shortcut1"
+        android:icon="@drawable/test_theme_icon"
+        android:shortcutShortLabel="@string/shortcut1">
+        <intent android:action="com.android.launcher3.intent.action.test_shortcut_max"/>
+    </shortcut>
+    <shortcut
+        android:shortcutId="max_shortcut2"
+        android:shortcutShortLabel="@string/shortcut2">
+        <intent android:action="com.android.launcher3.intent.action.test_shortcut_max"/>
+    </shortcut>
+    <shortcut
+        android:shortcutId="max_shortcut3"
+        android:shortcutShortLabel="@string/shortcut3">
+        <intent android:action="com.android.launcher3.intent.action.test_shortcut_max"/>
+    </shortcut>
+    <shortcut
+        android:shortcutId="max_shortcut4"
+        android:shortcutShortLabel="@string/shortcut4">
+        <intent android:action="com.android.launcher3.intent.action.test_shortcut_max"/>
+    </shortcut>
+</shortcuts>
diff --git a/tests/res/xml/shortcuts.xml b/tests/res/xml/shortcuts.xml
index 94e8edd..2ba9d7f 100644
--- a/tests/res/xml/shortcuts.xml
+++ b/tests/res/xml/shortcuts.xml
@@ -1,4 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
 <shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
     <shortcut
         android:shortcutId="shortcut1_themed"
diff --git a/tests/res/xml/single_shortcut.xml b/tests/res/xml/single_shortcut.xml
new file mode 100644
index 0000000..e8d938f
--- /dev/null
+++ b/tests/res/xml/single_shortcut.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+    <shortcut
+        android:shortcutId="single_shortcut_themed"
+        android:icon="@drawable/test_theme_icon"
+        android:shortcutShortLabel="@string/shortcut1">
+        <intent android:action="com.android.launcher3.intent.action.test_shortcut_single"/>
+    </shortcut>
+</shortcuts>
diff --git a/tests/res/xml/valid_all_apps_file.xml b/tests/res/xml/valid_all_apps_file.xml
new file mode 100644
index 0000000..0be55d1
--- /dev/null
+++ b/tests/res/xml/valid_all_apps_file.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<allAppsSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <allAppsSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="0dp" />
+        <endPadding launcher:fixedSize="0dp" />
+        <gutter launcher:matchWorkspace="true" />
+        <cellSize launcher:matchWorkspace="true" />
+    </allAppsSpec>
+
+    <allAppsSpec
+        launcher:specType="width"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:matchWorkspace="true" />
+        <endPadding launcher:matchWorkspace="true" />
+        <gutter launcher:matchWorkspace="true" />
+        <cellSize launcher:matchWorkspace="true" />
+    </allAppsSpec>
+
+</allAppsSpecs>
diff --git a/tests/res/xml/valid_folders_specs.xml b/tests/res/xml/valid_folders_specs.xml
new file mode 100644
index 0000000..0c45544
--- /dev/null
+++ b/tests/res/xml/valid_folders_specs.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:matchWorkspace="true" />
+    </folderSpec>
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="102dp" />
+    </folderSpec>
+
+    <!-- Height spec is fixed -->
+    <folderSpec launcher:specType="height" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="24dp" />
+        <!-- mapped to footer height size -->
+        <endPadding launcher:fixedSize="64dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:matchWorkspace="true" />
+    </folderSpec>
+</folderSpecs>
diff --git a/tests/res/xml/valid_hotseat_file.xml b/tests/res/xml/valid_hotseat_file.xml
new file mode 100644
index 0000000..c7f52e8
--- /dev/null
+++ b/tests/res/xml/valid_hotseat_file.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<hotseatSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+
+    <hotseatSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="847dp">
+        <hotseatQsbSpace launcher:fixedSize="24dp" />
+    </hotseatSpec>
+
+    <hotseatSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="9999dp">
+        <hotseatQsbSpace launcher:fixedSize="36dp" />
+    </hotseatSpec>
+
+</hotseatSpecs>
\ No newline at end of file
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 2ac0dd7..825b3dc 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -39,7 +39,6 @@
     public static final int HINT_STATE_TWO_BUTTON_ORDINAL = 8;
     public static final int OVERVIEW_SPLIT_SELECT_ORDINAL = 9;
     public static final int EDIT_MODE_STATE_ORDINAL = 10;
-    public static final String TAPL_EVENTS_TAG = "TaplEvents";
     public static final String SEQUENCE_MAIN = "Main";
     public static final String SEQUENCE_TIS = "TIS";
     public static final String SEQUENCE_PILFER = "Pilfer";
@@ -110,6 +109,7 @@
     public static final String REQUEST_GET_TEST_EVENTS = "get-test-events";
     public static final String REQUEST_GET_HAD_NONTEST_EVENTS = "get-had-nontest-events";
     public static final String REQUEST_STOP_EVENT_LOGGING = "stop-event-logging";
+    public static final String REQUEST_REINITIALIZE_DATA = "reinitialize-data";
     public static final String REQUEST_CLEAR_DATA = "clear-data";
     public static final String REQUEST_HOTSEAT_ICON_NAMES = "get-hotseat-icon-names";
     public static final String REQUEST_IS_TABLET = "is-tablet";
@@ -152,15 +152,20 @@
     public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation";
 
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
-    public static final String WORK_TAB_MISSING = "b/243688989";
     public static final String TWO_TASKBAR_LONG_CLICKS = "b/262282528";
-    public static final String FLAKY_ACTIVITY_COUNT = "b/260260325";
+    public static final String FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP = "b/286084688";
+    public static final String ICON_MISSING = "b/282963545";
+    public static final String LAUNCH_SPLIT_PAIR = "b/288939273";
 
+    public static final String OVERVIEW_OVER_HOME = "b/279059025";
+    public static final String INCORRECT_HOME_STATE = "b/293191790";
     public static final String REQUEST_EMULATE_DISPLAY = "emulate-display";
     public static final String REQUEST_STOP_EMULATE_DISPLAY = "stop-emulate-display";
     public static final String REQUEST_IS_EMULATE_DISPLAY_RUNNING = "is-emulate-display-running";
     public static final String REQUEST_EMULATE_PRINT_DEVICE = "emulate-print-device";
 
+    public static final String REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW = "enable-grid-only-overview";
+
     /** Logs {@link Log#d(String, String)} if {@link #sDebugTracing} is true. */
     public static void testLogD(String tag, String message) {
         if (!sDebugTracing) {
diff --git a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index 3de4d55..dd79ca8 100644
--- a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -22,11 +22,16 @@
 import android.util.DisplayMetrics
 import android.view.Surface
 import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.testing.shared.ResourceUtils
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.NavigationMode
 import com.android.launcher3.util.WindowBounds
 import com.android.launcher3.util.window.CachedDisplayInfo
 import com.android.launcher3.util.window.WindowManagerProxy
+import java.io.BufferedReader
+import java.io.File
+import java.io.PrintWriter
+import java.io.StringWriter
 import kotlin.math.max
 import kotlin.math.min
 import org.junit.After
@@ -154,41 +159,55 @@
     }
 
     protected fun initializeVarsForTwoPanel(
-        deviceTabletSpec: DeviceSpec,
-        deviceSpec: DeviceSpec,
+        deviceSpecUnfolded: DeviceSpec,
+        deviceSpecFolded: DeviceSpec,
         isLandscape: Boolean = false,
-        isGestureMode: Boolean = true
+        isGestureMode: Boolean = true,
+        isFolded: Boolean = false
     ) {
-        val (tabletNaturalX, tabletNaturalY) = deviceTabletSpec.naturalSize
-        val tabletWindowsBounds =
-            tabletWindowsBounds(deviceTabletSpec, tabletNaturalX, tabletNaturalY)
-        val tabletDisplayInfo =
+        val (unfoldedNaturalX, unfoldedNaturalY) = deviceSpecUnfolded.naturalSize
+        val unfoldedWindowsBounds =
+            tabletWindowsBounds(deviceSpecUnfolded, unfoldedNaturalX, unfoldedNaturalY)
+        val unfoldedDisplayInfo =
             CachedDisplayInfo(
-                Point(tabletNaturalX, tabletNaturalY),
+                Point(unfoldedNaturalX, unfoldedNaturalY),
                 Surface.ROTATION_0,
                 Rect(0, 0, 0, 0)
             )
 
-        val (phoneNaturalX, phoneNaturalY) = deviceSpec.naturalSize
-        val phoneWindowsBounds =
-            phoneWindowsBounds(deviceSpec, isGestureMode, phoneNaturalX, phoneNaturalY)
-        val phoneDisplayInfo =
+        val (foldedNaturalX, foldedNaturalY) = deviceSpecFolded.naturalSize
+        val foldedWindowsBounds =
+            phoneWindowsBounds(deviceSpecFolded, isGestureMode, foldedNaturalX, foldedNaturalY)
+        val foldedDisplayInfo =
             CachedDisplayInfo(
-                Point(phoneNaturalX, phoneNaturalY),
+                Point(foldedNaturalX, foldedNaturalY),
                 Surface.ROTATION_0,
                 Rect(0, 0, 0, 0)
             )
 
         val perDisplayBoundsCache =
-            mapOf(tabletDisplayInfo to tabletWindowsBounds, phoneDisplayInfo to phoneWindowsBounds)
+            mapOf(
+                unfoldedDisplayInfo to unfoldedWindowsBounds,
+                foldedDisplayInfo to foldedWindowsBounds
+            )
 
-        initializeCommonVars(
-            perDisplayBoundsCache,
-            tabletDisplayInfo,
-            rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
-            isGestureMode,
-            densityDpi = deviceTabletSpec.densityDpi
-        )
+        if (isFolded) {
+            initializeCommonVars(
+                perDisplayBoundsCache = perDisplayBoundsCache,
+                displayInfo = foldedDisplayInfo,
+                rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0,
+                isGestureMode = isGestureMode,
+                densityDpi = deviceSpecFolded.densityDpi
+            )
+        } else {
+            initializeCommonVars(
+                perDisplayBoundsCache = perDisplayBoundsCache,
+                displayInfo = unfoldedDisplayInfo,
+                rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
+                isGestureMode = isGestureMode,
+                densityDpi = deviceSpecUnfolded.densityDpi
+            )
+        }
     }
 
     private fun phoneWindowsBounds(
@@ -196,7 +215,7 @@
         isGestureMode: Boolean,
         naturalX: Int,
         naturalY: Int
-    ): Array<WindowBounds> {
+    ): List<WindowBounds> {
         val buttonsNavHeight = Utilities.dpToPx(48f, deviceSpec.densityDpi)
 
         val rotation0Insets =
@@ -231,7 +250,7 @@
                 if (isGestureMode) deviceSpec.gesturePx else 0
             )
 
-        return arrayOf(
+        return listOf(
             WindowBounds(Rect(0, 0, naturalX, naturalY), rotation0Insets, Surface.ROTATION_0),
             WindowBounds(Rect(0, 0, naturalY, naturalX), rotation90Insets, Surface.ROTATION_90),
             WindowBounds(Rect(0, 0, naturalX, naturalY), rotation180Insets, Surface.ROTATION_180),
@@ -243,11 +262,11 @@
         deviceSpec: DeviceSpec,
         naturalX: Int,
         naturalY: Int
-    ): Array<WindowBounds> {
+    ): List<WindowBounds> {
         val naturalInsets = Rect(0, deviceSpec.statusBarNaturalPx, 0, 0)
         val rotatedInsets = Rect(0, deviceSpec.statusBarRotatedPx, 0, 0)
 
-        return arrayOf(
+        return listOf(
             WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_0),
             WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_90),
             WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_180),
@@ -256,7 +275,7 @@
     }
 
     private fun initializeCommonVars(
-        perDisplayBoundsCache: Map<CachedDisplayInfo, Array<WindowBounds>>,
+        perDisplayBoundsCache: Map<CachedDisplayInfo, List<WindowBounds>>,
         displayInfo: CachedDisplayInfo,
         rotation: Int,
         isGestureMode: Boolean = true,
@@ -287,4 +306,27 @@
         whenever(displayController.info).thenReturn(info)
         whenever(displayController.isTransientTaskbar).thenReturn(isGestureMode)
     }
+
+    /** Create a new dump of DeviceProfile, saves to a file in the device and returns it */
+    protected fun dump(context: Context, dp: DeviceProfile, fileName: String): String {
+        val stringWriter = StringWriter()
+        PrintWriter(stringWriter).use { dp.dump(context, "", it) }
+        return stringWriter.toString().also { content -> writeToDevice(context, fileName, content) }
+    }
+
+    /** Read a file from assets/ and return it as a string */
+    protected fun readDumpFromAssets(context: Context, fileName: String): String =
+        context.assets.open("dumpTests/$fileName").bufferedReader().use(BufferedReader::readText)
+
+    private fun writeToDevice(context: Context, fileName: String, content: String) {
+        File(context.getDir("dumpTests", Context.MODE_PRIVATE), fileName).writeText(content)
+    }
+
+    protected fun Float.dpToPx(): Float {
+        return ResourceUtils.pxFromDp(this, context!!.resources.displayMetrics).toFloat()
+    }
+
+    protected fun Int.dpToPx(): Int {
+        return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics)
+    }
 }
diff --git a/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java b/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java
index 3c2b49a..ff667e6 100644
--- a/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java
+++ b/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java
@@ -103,6 +103,8 @@
         public static final char IGNORE = 'x';
         // The cells marked by this will be filled by app icons
         public static final char ICON = 'i';
+        // The cells marked by FOLDER will be filled by folders with 27 app icons inside
+        public static final char FOLDER = 'Z';
         // Empty space
         public static final char EMPTY = '-';
         // Widget that will be saved as "main widget" for easier retrieval
@@ -139,9 +141,14 @@
             return this.mType == CellType.IGNORE;
         }
 
+        boolean contains(int x, int y) {
+            return mBounds.contains(x, y);
+        }
+
         @Override
         public String toString() {
-            return "WidgetRect type = " + mType + " bounds = " + mBounds.toString();
+            return "WidgetRect type = " + mType + " x = " + getCellX() + " | y " + getCellY()
+                    + " xs = " + getSpanX() + " ys = " + getSpanY();
         }
     }
 
@@ -171,6 +178,25 @@
         }
     }
 
+    public static class FolderPoint {
+        public Point coord;
+        public char mType;
+
+        public FolderPoint(Point coord, char type) {
+            this.coord = coord;
+            mType = type;
+        }
+
+        /**
+         * [A-Z]: Represents a folder and number of icons in the folder is represented by
+         * the order of letter in the alphabet, A=2, B=3, C=4 ... etc.
+         */
+        public int getNumberIconsInside() {
+            return (mType - 'A') + 2;
+        }
+    }
+
+
     private HashSet<Character> mUsedWidgetTypes = new HashSet<>();
 
     static final int INFINITE = 99999;
@@ -181,6 +207,7 @@
     Map<Character, WidgetRect> mWidgetsMap = new HashMap<>();
 
     List<IconPoint> mIconPoints = new ArrayList<>();
+    List<FolderPoint> mFolderPoints = new ArrayList<>();
 
     WidgetRect mMain = null;
 
@@ -205,6 +232,17 @@
         }
     }
 
+    public boolean pointInsideRect(int x, int y, WidgetRect rect) {
+        Boolean isXInRect = x >= rect.getCellX() && x < rect.getCellX() + rect.getSpanX();
+        Boolean isYInRect = y >= rect.getCellY() && y < rect.getCellY() + rect.getSpanY();
+        return isXInRect && isYInRect;
+    }
+
+    public WidgetRect getWidgetAt(int x, int y) {
+        return mWidgetsRects.stream()
+                .filter(widgetRect -> pointInsideRect(x, y, widgetRect)).findFirst().orElse(null);
+    }
+
     public List<WidgetRect> getWidgets() {
         return mWidgetsRects;
     }
@@ -213,6 +251,10 @@
         return mIconPoints;
     }
 
+    public List<FolderPoint> getFolders() {
+        return mFolderPoints;
+    }
+
     public WidgetRect getMain() {
         return mMain;
     }
@@ -248,6 +290,17 @@
             }
             return true;
         }).collect(Collectors.toList());
+
+        // Remove overlapping folders and remove them from the board
+        mFolderPoints = mFolderPoints.stream().filter(folderPoint -> {
+            int x = folderPoint.coord.x;
+            int y = folderPoint.coord.y;
+            if (rect.contains(x, y)) {
+                mWidget[x][y] = '-';
+                return false;
+            }
+            return true;
+        }).collect(Collectors.toList());
     }
 
     private void removeOverlappingItems(Point p) {
@@ -269,6 +322,17 @@
             }
             return true;
         }).collect(Collectors.toList());
+
+        // Remove overlapping folders and remove them from the board
+        mFolderPoints = mFolderPoints.stream().filter(folderPoint -> {
+            int x = folderPoint.coord.x;
+            int y = folderPoint.coord.y;
+            if (p.x == x && p.y == y) {
+                mWidget[x][y] = '-';
+                return false;
+            }
+            return true;
+        }).collect(Collectors.toList());
     }
 
     private char getNextWidgetType() {
@@ -373,6 +437,18 @@
         return iconPoints;
     }
 
+    private static List<FolderPoint> getFolderPoints(char[][] board) {
+        List<FolderPoint> folderPoints = new ArrayList<>();
+        for (int x = 0; x < board.length; x++) {
+            for (int y = 0; y < board[0].length; y++) {
+                if (isFolder(board[x][y])) {
+                    folderPoints.add(new FolderPoint(new Point(x, y), board[x][y]));
+                }
+            }
+        }
+        return folderPoints;
+    }
+
     public static WidgetRect getMainFromList(List<CellLayoutBoard> boards) {
         for (CellLayoutBoard board : boards) {
             WidgetRect main = board.getMain();
@@ -383,6 +459,17 @@
         return null;
     }
 
+    public static WidgetRect getWidgetIn(List<CellLayoutBoard> boards, int x, int y) {
+        for (CellLayoutBoard board : boards) {
+            WidgetRect main = board.getWidgetAt(x, y);
+            if (main != null) {
+                return main;
+            }
+            x -= board.mWidth;
+        }
+        return null;
+    }
+
     public static CellLayoutBoard boardFromString(String boardStr) {
         String[] lines = boardStr.split("\n");
         CellLayoutBoard board = new CellLayoutBoard();
@@ -406,6 +493,7 @@
             board.mWidgetsMap.put(widgetRect.mType, widgetRect);
         });
         board.mIconPoints = getIconPoints(board.mWidget);
+        board.mFolderPoints = getFolderPoints(board.mWidget);
         return board;
     }
 
diff --git a/tests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java b/tests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
index e33a304..419cb3d 100644
--- a/tests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
+++ b/tests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
@@ -63,8 +63,8 @@
     }
 
     public static class Board extends TestSection {
-        Point gridSize;
-        String board;
+        public Point gridSize;
+        public String board;
 
         public Board(Point gridSize, String board) {
             super(State.BOARD);
@@ -127,7 +127,7 @@
         }
     }
 
-    List<TestSection> parse() {
+    public List<TestSection> parse() {
         List<TestSection> sections = new ArrayList<>();
         String[] lines = mTest.split("\n");
         Iterator<String> it = Arrays.stream(lines).iterator();
diff --git a/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java b/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
index 0d2f252..86a7bd3 100644
--- a/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
+++ b/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
@@ -42,8 +42,8 @@
                         params.getCellX(), params.getCellY(),
                         launcher.getWorkspace().getIdForScreen(cellLayout), CONTAINER_DESKTOP);
                 int screenId = pos.screenId;
-                if (screenId >= boards.size() - 1) {
-                    boards.add(new CellLayoutBoard());
+                for (int j = boards.size(); j <= screenId; j++) {
+                    boards.add(new CellLayoutBoard(cellLayout.getCountX(), cellLayout.getCountY()));
                 }
                 CellLayoutBoard board = boards.get(screenId);
                 // is icon
@@ -51,7 +51,7 @@
                     board.addIcon(pos.cellX, pos.cellY);
                 } else {
                     // is widget
-                    board.addWidget(params.getCellX(), params.getCellY(), params.cellHSpan,
+                    board.addWidget(pos.cellX, pos.cellY, params.cellHSpan,
                             params.cellVSpan);
                 }
             }
diff --git a/tests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java b/tests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
index 8ce932d..bf7a21c 100644
--- a/tests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
+++ b/tests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
@@ -15,62 +15,116 @@
  */
 package com.android.launcher3.celllayout;
 
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
 
-import android.content.ContentResolver;
-import android.content.ContentValues;
 import android.content.Context;
 
+import androidx.test.uiautomator.UiDevice;
+
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.ModelDbController;
+import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
+import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.util.ContentWriter;
 
 import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
+import java.util.function.Supplier;
 
 public class FavoriteItemsTransaction {
-    private ArrayList<ItemInfo> mItemsToSubmit;
+    private ArrayList<Supplier<ItemInfo>> mItemsToSubmit;
     private Context mContext;
-    private ContentResolver mResolver;
-    public AbstractLauncherUiTest mTest;
 
-    public FavoriteItemsTransaction(Context context, AbstractLauncherUiTest test) {
+    public FavoriteItemsTransaction(Context context) {
         mItemsToSubmit = new ArrayList<>();
         mContext = context;
-        mResolver = mContext.getContentResolver();
-        mTest = test;
     }
 
-    public FavoriteItemsTransaction addItem(ItemInfo itemInfo) {
+    public FavoriteItemsTransaction addItem(Supplier<ItemInfo> itemInfo) {
         this.mItemsToSubmit.add(itemInfo);
         return this;
     }
 
-    public FavoriteItemsTransaction removeLast() {
-        this.mItemsToSubmit.remove(this.mItemsToSubmit.size() - 1);
-        return this;
-    }
-
     /**
      * Commits all the ItemInfo into the database of Favorites
      **/
-    public void commit() throws ExecutionException, InterruptedException {
-        List<ContentValues> values = new ArrayList<>();
-        for (ItemInfo item : this.mItemsToSubmit) {
-            ContentWriter writer = new ContentWriter(mContext);
-            item.onAddToDatabase(writer);
-            writer.put(LauncherSettings.Favorites._ID, item.id);
-            values.add(writer.getValues(mContext));
-        }
-        // Submit the icons to the database in the model thread to prevent race conditions
-        MODEL_EXECUTOR.submit(() -> mResolver.bulkInsert(LauncherSettings.Favorites.CONTENT_URI,
-                values.toArray(new ContentValues[0]))).get();
-        // Reload the state of the Launcher
-        MAIN_EXECUTOR.submit(() -> LauncherAppState.getInstance(
-                mContext).getModel().forceReload()).get();
+    public void commit() {
+        LauncherModel model = LauncherAppState.getInstance(mContext).getModel();
+        // Load the model once so that there is no pending migration:
+        loadModelSync(model);
+
+        runOnExecutorSync(MODEL_EXECUTOR, () -> {
+            ModelDbController controller = model.getModelDbController();
+            // Migrate any previous data so that the DB state is correct
+            controller.tryMigrateDB();
+
+            // Create DB again to load fresh data
+            controller.createEmptyDB();
+            controller.clearEmptyDbFlag();
+
+            // Add new data
+            try (SQLiteTransaction transaction = controller.newTransaction()) {
+                int count = mItemsToSubmit.size();
+                ArrayList<ItemInfo> containerItems = new ArrayList<>();
+                for (int i = 0; i < count; i++) {
+                    ContentWriter writer = new ContentWriter(mContext);
+                    ItemInfo item = mItemsToSubmit.get(i).get();
+
+                    if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+                        FolderInfo folderInfo = (FolderInfo) item;
+                        for (ItemInfo itemInfo : folderInfo.contents) {
+                            itemInfo.container = i;
+                            containerItems.add(itemInfo);
+                        }
+                    }
+
+                    item.onAddToDatabase(writer);
+                    writer.put(LauncherSettings.Favorites._ID, i);
+                    controller.insert(TABLE_NAME, writer.getValues(mContext));
+                }
+
+                for (int i = 0; i < containerItems.size(); i++) {
+                    ContentWriter writer = new ContentWriter(mContext);
+                    ItemInfo item = containerItems.get(i);
+                    item.onAddToDatabase(writer);
+                    writer.put(LauncherSettings.Favorites._ID, count + i);
+                    controller.insert(TABLE_NAME, writer.getValues(mContext));
+                }
+                transaction.commit();
+            }
+        });
+
+        // Reload model
+        runOnExecutorSync(MAIN_EXECUTOR, model::forceReload);
+        loadModelSync(model);
+    }
+
+    private void loadModelSync(LauncherModel model) {
+        Callbacks mockCb = new Callbacks() { };
+        runOnExecutorSync(MAIN_EXECUTOR, () -> model.addCallbacksAndLoad(mockCb));
+        runOnExecutorSync(MODEL_EXECUTOR, () -> { });
+
+        runOnExecutorSync(MAIN_EXECUTOR, () -> { });
+        runOnExecutorSync(MAIN_EXECUTOR, () -> model.removeCallbacks(mockCb));
+    }
+
+    /**
+     * Commits the transaction and waits for home load
+     */
+    public void commitAndLoadHome(LauncherInstrumentation inst) {
+        commit();
+
+        // Launch the home activity
+        UiDevice.getInstance(getInstrumentation()).pressHome();
+        inst.waitForLauncherInitialized();
     }
 }
diff --git a/tests/src/com/android/launcher3/celllayout/MultipleCellLayoutsSimpleReorder.java b/tests/src/com/android/launcher3/celllayout/MultipleCellLayoutsSimpleReorder.java
deleted file mode 100644
index 706c1a7..0000000
--- a/tests/src/com/android/launcher3/celllayout/MultipleCellLayoutsSimpleReorder.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.celllayout.testcases;
-
-import android.graphics.Point;
-
-import com.android.launcher3.celllayout.ReorderTestCase;
-
-import java.util.Map;
-
-/**
- * The grids represent the workspace to be build by TestWorkspaceBuilder, to see what each character
- * in the board mean refer to {@code CellType}
- */
-public class MultipleCellLayoutsSimpleReorder {
-
-    /** 5x5 Test
-     **/
-    private static final String START_BOARD_STR_5x5 = ""
-            + "xxxxx|-----\n"
-            + "--mm-|-----\n"
-            + "--mm-|-----\n"
-            + "-----|-----\n"
-            + "-----|-----";
-    private static final Point MOVE_TO_5x5 = new Point(8, 3);
-    private static final String END_BOARD_STR_5x5 = ""
-            + "xxxxx|-----\n"
-            + "-----|-----\n"
-            + "-----|-----\n"
-            + "-----|---mm\n"
-            + "-----|---mm";
-    private static final ReorderTestCase TEST_CASE_5x5 = new ReorderTestCase(START_BOARD_STR_5x5,
-            MOVE_TO_5x5,
-            END_BOARD_STR_5x5);
-
-    /** 4x4 Test
-     **/
-    private static final String START_BOARD_STR_4x4 = ""
-            + "xxxx|----\n"
-            + "--mm|----\n"
-            + "--mm|----\n"
-            + "----|----";
-    private static final Point MOVE_TO_4x4 = new Point(5, 3);
-    private static final String END_BOARD_STR_4x4 = ""
-            + "xxxx|----\n"
-            + "----|----\n"
-            + "----|-mm-\n"
-            + "----|-mm-";
-    private static final ReorderTestCase TEST_CASE_4x4 = new ReorderTestCase(START_BOARD_STR_4x4,
-            MOVE_TO_4x4,
-            END_BOARD_STR_4x4);
-
-
-    /** 6x5 Test
-     **/
-    private static final String START_BOARD_STR_6x5 = ""
-            + "xxxxxx|------\n"
-            + "--m---|------\n"
-            + "------|------\n"
-            + "------|------\n"
-            + "------|------";
-    private static final Point MOVE_TO_6x5 = new Point(10, 4);
-    private static final String END_BOARD_STR_6x5 = ""
-            + "xxxxxx|------\n"
-            + "------|------\n"
-            + "------|------\n"
-            + "------|------\n"
-            + "------|----m-";
-    private static final ReorderTestCase TEST_CASE_6x5 = new ReorderTestCase(START_BOARD_STR_6x5,
-            MOVE_TO_6x5,
-            END_BOARD_STR_6x5);
-
-    public static final Map<Point, ReorderTestCase> TEST_BY_GRID_SIZE =
-            Map.of(new Point(5, 5), TEST_CASE_5x5,
-                    new Point(4, 4), TEST_CASE_4x4,
-                    new Point(6, 5), TEST_CASE_6x5);
-}
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java b/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
index ee05fe6..91a0634 100644
--- a/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
+++ b/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
@@ -164,7 +164,7 @@
         Context c = new ActivityContextWrapper(getApplicationContext());
         DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(c).getDeviceProfile(c).copy(c);
         mPrevNumColumns = dp.inv.numColumns;
-        mPrevNumRows = dp.inv.numColumns;
+        mPrevNumRows = dp.inv.numRows;
     }
 
     @After
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
index 243edc3..00d7ce6 100644
--- a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
+++ b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
@@ -18,20 +18,24 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.graphics.Point;
+import android.net.Uri;
 import android.util.Log;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.celllayout.testcases.MultipleCellLayoutsSimpleReorder;
+import com.android.launcher3.MultipageCellLayout;
 import com.android.launcher3.tapl.Widget;
 import com.android.launcher3.tapl.WidgetResizeFrame;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TaplTestsLauncher3;
 import com.android.launcher3.util.rule.ShellCommandRule;
 
+import org.junit.Assert;
 import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Rule;
@@ -44,7 +48,6 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.ExecutionException;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -55,13 +58,14 @@
 
     private static final String TAG = ReorderWidgets.class.getSimpleName();
 
+    private static final List<String> FOLDABLE_GRIDS = List.of("normal", "practical", "reasonable");
+
     TestWorkspaceBuilder mWorkspaceBuilder;
 
     @Before
     public void setup() throws Throwable {
-        mWorkspaceBuilder = new TestWorkspaceBuilder(this, mTargetContext);
+        mWorkspaceBuilder = new TestWorkspaceBuilder(mTargetContext);
         TaplTestsLauncher3.initialize(this);
-        clearHomescreen();
     }
 
     /**
@@ -102,20 +106,63 @@
         return getFromLauncher(CellLayoutTestUtils::workspaceToBoards);
     }
 
-    private void runTestCase(ReorderTestCase testCase)
-            throws ExecutionException, InterruptedException {
+    private CellLayoutBoard.WidgetRect getWidgetClosestTo(Point point) {
+        ArrayList<CellLayoutBoard> workspaceBoards = workspaceToBoards();
+        int maxDistance = 9999;
+        CellLayoutBoard.WidgetRect bestRect = null;
+        for (int i = 0; i < workspaceBoards.get(0).getWidgets().size(); i++) {
+            CellLayoutBoard.WidgetRect widget = workspaceBoards.get(0).getWidgets().get(i);
+            if (widget.getCellX() == 0 && widget.getCellY() == 0) {
+                continue;
+            }
+            int distance = Math.abs(point.x - widget.getCellX())
+                    + Math.abs(point.y - widget.getCellY());
+            if (distance == 0) {
+                break;
+            }
+            if (distance < maxDistance) {
+                maxDistance = distance;
+                bestRect = widget;
+            }
+        }
+        return bestRect;
+    }
+
+    /**
+     * This function might be odd, its function is to select a widget and leave it in its place.
+     * The idea is to make the test broader and also test after a widgets resized because the
+     * underlying code does different things in that case
+     */
+    private void triggerWidgetResize(ReorderTestCase testCase) {
+        CellLayoutBoard.WidgetRect widgetRect = getWidgetClosestTo(testCase.moveMainTo);
+        if (widgetRect == null) {
+            // Some test doesn't have a widget in the final position, in those cases we will ignore
+            // them
+            return;
+        }
+        Widget widget = mLauncher.getWorkspace().getWidgetAtCell(widgetRect.getCellX(),
+                widgetRect.getCellY());
+        WidgetResizeFrame resizeFrame = widget.dragWidgetToWorkspace(widgetRect.getCellX(),
+                widgetRect.getCellY(), widgetRect.getSpanX(), widgetRect.getSpanY());
+        resizeFrame.dismiss();
+    }
+
+    private void runTestCase(ReorderTestCase testCase) {
         CellLayoutBoard.WidgetRect mainWidgetCellPos = CellLayoutBoard.getMainFromList(
                 testCase.mStart);
 
         FavoriteItemsTransaction transaction =
-                new FavoriteItemsTransaction(mTargetContext, this);
+                new FavoriteItemsTransaction(mTargetContext);
         transaction = buildWorkspaceFromBoards(testCase.mStart, transaction);
         transaction.commit();
+        mLauncher.waitForLauncherInitialized();
         // resetLoaderState triggers the launcher to start loading the workspace which allows
         // waitForLauncherCondition to wait for that condition, otherwise the condition would
         // always be true and it wouldn't wait for the changes to be applied.
-        resetLoaderState();
         waitForLauncherCondition("Workspace didn't finish loading", l -> !l.isWorkspaceLoading());
+
+        triggerWidgetResize(testCase);
+
         Widget widget = mLauncher.getWorkspace().getWidgetAtCell(mainWidgetCellPos.getCellX(),
                 mainWidgetCellPos.getCellY());
         assertNotNull(widget);
@@ -137,41 +184,91 @@
      *
      * @param testCaseMap map containing all the tests per grid size (Point)
      */
-    private void runTestCaseMap(Map<Point, ReorderTestCase> testCaseMap, String testName)
-            throws ExecutionException, InterruptedException {
+    private boolean runTestCaseMap(Map<Point, ReorderTestCase> testCaseMap, String testName) {
         Point iconGridDimensions = mLauncher.getWorkspace().getIconGridDimensions();
         Log.d(TAG, "Running test " + testName + " for grid " + iconGridDimensions);
-        Assume.assumeTrue(
-                "The test " + testName + " doesn't support " + iconGridDimensions + " grid layout",
-                testCaseMap.containsKey(iconGridDimensions));
+        if (!testCaseMap.containsKey(iconGridDimensions)) {
+            Log.d(TAG, "The test " + testName + " doesn't support " + iconGridDimensions
+                    + " grid layout");
+            return false;
+        }
         runTestCase(testCaseMap.get(iconGridDimensions));
+
+        return true;
+    }
+
+    private void runTestCaseMapForAllGrids(Map<Point, ReorderTestCase> testCaseMap,
+            String testName) {
+        boolean runAtLeastOnce = false;
+        for (String grid : FOLDABLE_GRIDS) {
+            applyGridOption(grid);
+            mLauncher.waitForLauncherInitialized();
+            runAtLeastOnce |= runTestCaseMap(testCaseMap, testName);
+        }
+        Assume.assumeTrue("None of the grids are supported", runAtLeastOnce);
+    }
+
+    private void applyGridOption(Object argValue) {
+        String testProviderAuthority = mTargetContext.getPackageName() + ".grid_control";
+        Uri gridUri = new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(testProviderAuthority)
+                .appendPath("default_grid")
+                .build();
+        ContentValues values = new ContentValues();
+        values.putObject("name", argValue);
+        Assert.assertEquals(1,
+                mTargetContext.getContentResolver().update(gridUri, values, null, null));
     }
 
     @Test
     public void simpleReorder() throws Exception {
-        runTestCaseMap(getTestMap("ReorderWidgets/simple_reorder_case"), "push_reorder_case");
+        runTestCaseMap(getTestMap("ReorderWidgets/simple_reorder_case"),
+                "push_reorder_case");
     }
 
     @Test
     public void pushTest() throws Exception {
-        runTestCaseMap(getTestMap("ReorderWidgets/push_reorder_case"), "push_reorder_case");
+        runTestCaseMap(getTestMap("ReorderWidgets/push_reorder_case"),
+                "push_reorder_case");
     }
 
     @Test
     public void fullReorder() throws Exception {
-        runTestCaseMap(getTestMap("ReorderWidgets/full_reorder_case"), "full_reorder_case");
+        runTestCaseMap(getTestMap("ReorderWidgets/full_reorder_case"),
+                "full_reorder_case");
     }
 
     @Test
     public void moveOutReorder() throws Exception {
-        runTestCaseMap(getTestMap("ReorderWidgets/move_out_reorder_case"), "move_out_reorder_case");
+        runTestCaseMap(getTestMap("ReorderWidgets/move_out_reorder_case"),
+                "move_out_reorder_case");
     }
 
     @Test
-    public void multipleCellLayoutsSimpleReorder() throws ExecutionException, InterruptedException {
-        Assume.assumeTrue("Test doesn't support foldables", !mLauncher.isTwoPanels());
-        runTestCaseMap(MultipleCellLayoutsSimpleReorder.TEST_BY_GRID_SIZE,
-                MultipleCellLayoutsSimpleReorder.class.getSimpleName());
+    public void multipleCellLayoutsSimpleReorder() throws Exception {
+        Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
+                l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
+        runTestCaseMapForAllGrids(getTestMap("ReorderWidgets/multiple_cell_layouts_simple_reorder"),
+                "multiple_cell_layouts_simple_reorder");
+    }
+
+    @Test
+    public void multipleCellLayoutsNoSpaceReorder() throws Exception {
+        Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
+                l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
+        runTestCaseMapForAllGrids(
+                getTestMap("ReorderWidgets/multiple_cell_layouts_no_space_reorder"),
+                "multiple_cell_layouts_no_space_reorder");
+    }
+
+    @Test
+    public void multipleCellLayoutsReorderToOtherSide() throws Exception {
+        Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
+                l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
+        runTestCaseMapForAllGrids(
+                getTestMap("ReorderWidgets/multiple_cell_layouts_reorder_other_side"),
+                "multiple_cell_layouts_reorder_other_side");
     }
 
     private void addTestCase(Iterator<CellLayoutTestCaseReader.TestSection> sections,
@@ -184,7 +281,7 @@
                 ((CellLayoutTestCaseReader.Board) sections.next());
         Point moveTo = new Point(Integer.parseInt(point.arguments[0]),
                 Integer.parseInt(point.arguments[1]));
-        testCaseMap.put(startBoard.gridSize,
+        testCaseMap.put(endBoard.gridSize,
                 new ReorderTestCase(startBoard.board, moveTo, endBoard.board));
     }
 
diff --git a/tests/src/com/android/launcher3/celllayout/TestWorkspaceBuilder.java b/tests/src/com/android/launcher3/celllayout/TestWorkspaceBuilder.java
index 5945605..6489bc1 100644
--- a/tests/src/com/android/launcher3/celllayout/TestWorkspaceBuilder.java
+++ b/tests/src/com/android/launcher3/celllayout/TestWorkspaceBuilder.java
@@ -15,6 +15,10 @@
  */
 package com.android.launcher3.celllayout;
 
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.launcher3.ui.TestViewHelpers.findWidgetProvider;
 import static com.android.launcher3.util.WidgetUtils.createWidgetInfo;
 
 import android.content.ComponentName;
@@ -25,33 +29,30 @@
 import android.os.UserHandle;
 import android.util.Log;
 
-import androidx.test.core.app.ApplicationProvider;
-
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherSettings;
 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.ui.AbstractLauncherUiTest;
-import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+
 public class TestWorkspaceBuilder {
 
     private static final String TAG = "CellLayoutBoardBuilder";
-    private static final ComponentName APP_COMPONENT_NAME = new ComponentName(
+    private static final String TEST_ACTIVITY_PACKAGE_PREFIX = "com.android.launcher3.tests.";
+    private ComponentName mAppComponentName = new ComponentName(
             "com.google.android.calculator", "com.android.calculator2.Calculator");
-
-    public AbstractLauncherUiTest mTest;
-
     private UserHandle mMyUser;
 
     private Context mContext;
     private ContentResolver mResolver;
 
-    public TestWorkspaceBuilder(AbstractLauncherUiTest test, Context context) {
-        mTest = test;
+    public TestWorkspaceBuilder(Context context) {
         mMyUser = Process.myUserHandle();
         mContext = context;
         mResolver = mContext.getContentResolver();
@@ -68,10 +69,9 @@
             for (int y = initY; y < initY + widgetRect.getSpanY(); y++) {
                 try {
                     // this widgets are filling, we don't care if we can't place them
-                    ItemInfo item = createWidgetInCell(
+                    transaction.addItem(createWidgetInCell(
                             new CellLayoutBoard.WidgetRect(CellLayoutBoard.CellType.IGNORE,
-                                    new Rect(x, y, x, y)), screenId);
-                    transaction.addItem(item);
+                                    new Rect(x, y, x, y)), screenId));
                 } catch (Exception e) {
                     Log.d(TAG, "Unable to place filling widget at " + x + "," + y);
                 }
@@ -80,15 +80,21 @@
         return transaction;
     }
 
-    private int getID() {
-        return LauncherSettings.Settings.call(
-                        mResolver, LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
-                .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+    private AppInfo getApp() {
+        return new AppInfo(mAppComponentName, "test icon", mMyUser,
+                AppInfo.makeLaunchIntent(mAppComponentName));
     }
 
-    private AppInfo getApp() {
-        return new AppInfo(APP_COMPONENT_NAME, "test icon", mMyUser,
-                AppInfo.makeLaunchIntent(APP_COMPONENT_NAME));
+    /**
+     * Helper to set the app to use for the test workspace,
+     *  using activity-alias from AndroidManifest-common.
+     * @param testAppName the android:name field of the test app activity-alias to use
+     */
+    public void setTestAppActivityAlias(String testAppName) {
+        this.mAppComponentName = new ComponentName(
+            getInstrumentation().getContext().getPackageName(),
+        TEST_ACTIVITY_PACKAGE_PREFIX + testAppName
+        );
     }
 
     private void addCorrespondingWidgetRect(CellLayoutBoard.WidgetRect widgetRect,
@@ -108,7 +114,10 @@
         board.getWidgets().forEach(
                 (widgetRect) -> addCorrespondingWidgetRect(widgetRect, transaction, screenId));
         board.getIcons().forEach((iconPoint) ->
-                transaction.addItem(createIconInCell(iconPoint, screenId))
+                transaction.addItem(() -> createIconInCell(iconPoint, screenId))
+        );
+        board.getFolders().forEach((folderPoint) ->
+                transaction.addItem(() -> createFolderInCell(folderPoint, screenId))
         );
         return transaction;
     }
@@ -118,29 +127,52 @@
      * be clean otherwise this doesn't overrides the existing icons.
      */
     public FavoriteItemsTransaction fillHotseatIcons(FavoriteItemsTransaction transaction) {
-        int hotseatCount = InvariantDeviceProfile.INSTANCE.get(mContext).numDatabaseHotseatIcons;
-        for (int i = 0; i < hotseatCount; i++) {
-            transaction.addItem(getHotseatValues(i));
-        }
+        IntStream.range(0, InvariantDeviceProfile.INSTANCE.get(mContext).numDatabaseHotseatIcons)
+                .forEach(i -> transaction.addItem(() -> getHotseatValues(i)));
         return transaction;
     }
 
-    private ItemInfo createWidgetInCell(CellLayoutBoard.WidgetRect widgetRect, int screenId) {
-        LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(mTest, false);
-        LauncherAppWidgetInfo item = createWidgetInfo(info,
-                ApplicationProvider.getApplicationContext(), true);
-        item.id = getID();
-        item.cellX = widgetRect.getCellX();
-        item.cellY = widgetRect.getCellY();
-        item.spanX = widgetRect.getSpanX();
-        item.spanY = widgetRect.getSpanY();
+    private Supplier<ItemInfo> createWidgetInCell(
+            CellLayoutBoard.WidgetRect widgetRect, int screenId) {
+        // Create the widget lazily since the appWidgetId can get lost during setup
+        return () -> {
+            LauncherAppWidgetProviderInfo info = findWidgetProvider(false);
+            LauncherAppWidgetInfo item = createWidgetInfo(info, getApplicationContext(), true);
+            item.cellX = widgetRect.getCellX();
+            item.cellY = widgetRect.getCellY();
+            item.spanX = widgetRect.getSpanX();
+            item.spanY = widgetRect.getSpanY();
+            item.screenId = screenId;
+            return item;
+        };
+    }
+
+    public FolderInfo createFolderInCell(CellLayoutBoard.FolderPoint folderPoint, int screenId) {
+        FolderInfo folderInfo = new FolderInfo();
+        folderInfo.screenId = screenId;
+        folderInfo.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+        folderInfo.cellX = folderPoint.coord.x;
+        folderInfo.cellY = folderPoint.coord.y;
+        folderInfo.minSpanY = folderInfo.minSpanX = folderInfo.spanX = folderInfo.spanY = 1;
+        folderInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, null);
+
+        for (int i = 0; i < folderPoint.getNumberIconsInside(); i++) {
+            folderInfo.add(getDefaultWorkspaceItem(screenId), false);
+        }
+
+        return folderInfo;
+    }
+
+    private WorkspaceItemInfo getDefaultWorkspaceItem(int screenId) {
+        WorkspaceItemInfo item = new WorkspaceItemInfo(getApp());
         item.screenId = screenId;
+        item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1;
+        item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
         return item;
     }
 
     private ItemInfo createIconInCell(CellLayoutBoard.IconPoint iconPoint, int screenId) {
         WorkspaceItemInfo item = new WorkspaceItemInfo(getApp());
-        item.id = getID();
         item.screenId = screenId;
         item.cellX = iconPoint.getCoord().x;
         item.cellY = iconPoint.getCoord().y;
@@ -151,7 +183,6 @@
 
     private ItemInfo getHotseatValues(int x) {
         WorkspaceItemInfo item = new WorkspaceItemInfo(getApp());
-        item.id = getID();
         item.cellX = x;
         item.cellY = 0;
         item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1;
diff --git a/tests/src/com/android/launcher3/folder/PreviewBackgroundTest.java b/tests/src/com/android/launcher3/folder/PreviewBackgroundTest.java
new file mode 100644
index 0000000..715a1f8
--- /dev/null
+++ b/tests/src/com/android/launcher3/folder/PreviewBackgroundTest.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.folder;
+
+import static com.android.launcher3.folder.PreviewBackground.ACCEPT_SCALE_FACTOR;
+import static com.android.launcher3.folder.PreviewBackground.CONSUMPTION_ANIMATION_DURATION;
+import static com.android.launcher3.folder.PreviewBackground.HOVER_ANIMATION_DURATION;
+import static com.android.launcher3.folder.PreviewBackground.HOVER_SCALE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.PathInterpolator;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.CellLayout;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class PreviewBackgroundTest {
+
+    private static final float REST_SCALE = 1f;
+    private static final float EPSILON = 0.00001f;
+
+    @Mock
+    CellLayout mCellLayout;
+
+    private final PreviewBackground mPreviewBackground = new PreviewBackground();
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mPreviewBackground.mScale = REST_SCALE;
+        mPreviewBackground.mIsAccepting = false;
+        mPreviewBackground.mIsHovered = false;
+        mPreviewBackground.mIsHoveredOrAnimating = false;
+        mPreviewBackground.invalidate();
+    }
+
+    @Test
+    public void testAnimateScale_restToHovered() {
+        mPreviewBackground.setHovered(true);
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                HOVER_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_restToNotHovered() {
+        mPreviewBackground.setHovered(false);
+
+        assertEquals("Scale changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON);
+        assertNull("Animator not null.", mPreviewBackground.mScaleAnimator);
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_hoveredToHovered() {
+        mPreviewBackground.mScale = HOVER_SCALE;
+        mPreviewBackground.mIsHovered = true;
+        mPreviewBackground.mIsHoveredOrAnimating = true;
+        mPreviewBackground.invalidate();
+
+        mPreviewBackground.setHovered(true);
+
+        assertEquals("Scale changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON);
+        assertNull("Animator not null.", mPreviewBackground.mScaleAnimator);
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_hoveredToRest() {
+        mPreviewBackground.mScale = HOVER_SCALE;
+        mPreviewBackground.mIsHovered = true;
+        mPreviewBackground.mIsHoveredOrAnimating = true;
+        mPreviewBackground.invalidate();
+
+        mPreviewBackground.setHovered(false);
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                HOVER_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_restToAccept() {
+        mPreviewBackground.animateToAccept(mCellLayout, 0, 0);
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale changed.", mPreviewBackground.mScale, ACCEPT_SCALE_FACTOR, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                CONSUMPTION_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator()
+                        instanceof AccelerateDecelerateInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 1.", mPreviewBackground.getAcceptScaleProgress(), 1,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_restToRest() {
+        mPreviewBackground.animateToRest();
+
+        assertEquals("Scale changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON);
+        assertNull("Animator not null.", mPreviewBackground.mScaleAnimator);
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_acceptToRest() {
+        mPreviewBackground.mScale = ACCEPT_SCALE_FACTOR;
+        mPreviewBackground.mIsAccepting = true;
+        mPreviewBackground.invalidate();
+
+        mPreviewBackground.animateToRest();
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                CONSUMPTION_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator()
+                        instanceof AccelerateDecelerateInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_acceptToHover() {
+        mPreviewBackground.mScale = ACCEPT_SCALE_FACTOR;
+        mPreviewBackground.mIsAccepting = true;
+        mPreviewBackground.invalidate();
+
+        mPreviewBackground.mIsAccepting = false;
+        mPreviewBackground.setHovered(true);
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                HOVER_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_hoverToAccept() {
+        mPreviewBackground.mScale = HOVER_SCALE;
+        mPreviewBackground.mIsHovered = true;
+        mPreviewBackground.mIsHoveredOrAnimating = true;
+        mPreviewBackground.invalidate();
+
+        mPreviewBackground.animateToAccept(mCellLayout, 0, 0);
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, ACCEPT_SCALE_FACTOR, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                CONSUMPTION_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator()
+                        instanceof AccelerateDecelerateInterpolator);
+        mPreviewBackground.mIsHovered = false;
+        endAnimation();
+        assertEquals("Scale progress not 1.", mPreviewBackground.getAcceptScaleProgress(), 1,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_midwayToHoverToAccept() {
+        mPreviewBackground.setHovered(true);
+        runAnimationToFraction(0.5f);
+        assertTrue("Scale not changed.",
+                mPreviewBackground.mScale > REST_SCALE && mPreviewBackground.mScale < HOVER_SCALE);
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+
+        mPreviewBackground.animateToAccept(mCellLayout, 0, 0);
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, ACCEPT_SCALE_FACTOR, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                CONSUMPTION_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator()
+                        instanceof AccelerateDecelerateInterpolator);
+        mPreviewBackground.mIsHovered = false;
+        endAnimation();
+        assertEquals("Scale progress not 1.", mPreviewBackground.getAcceptScaleProgress(), 1,
+                EPSILON);
+        assertNull("Animator not null.", mPreviewBackground.mScaleAnimator);
+    }
+
+    @Test
+    public void testAnimateScale_partWayToAcceptToHover() {
+        mPreviewBackground.animateToAccept(mCellLayout, 0, 0);
+        runAnimationToFraction(0.25f);
+        assertTrue("Scale not changed part way.", mPreviewBackground.mScale > REST_SCALE
+                && mPreviewBackground.mScale < ACCEPT_SCALE_FACTOR);
+
+        mPreviewBackground.mIsAccepting = false;
+        mPreviewBackground.setHovered(true);
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                HOVER_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_midwayToAcceptEqualsHover() {
+        mPreviewBackground.animateToAccept(mCellLayout, 0, 0);
+        runAnimationToFraction(0.5f);
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON);
+        mPreviewBackground.mIsAccepting = false;
+
+        mPreviewBackground.setHovered(true);
+
+        assertEquals("Scale changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON);
+        assertNull("Animator not null.", mPreviewBackground.mScaleAnimator);
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_midwayToHoverToRest() {
+        mPreviewBackground.setHovered(true);
+        runAnimationToFraction(0.5f);
+        assertTrue("Scale not changed midway.",
+                mPreviewBackground.mScale > REST_SCALE && mPreviewBackground.mScale < HOVER_SCALE);
+
+        mPreviewBackground.mIsHovered = false;
+        mPreviewBackground.animateToRest();
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                HOVER_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_midwayToAcceptToRest() {
+        mPreviewBackground.animateToAccept(mCellLayout, 0, 0);
+        runAnimationToFraction(0.5f);
+        assertTrue("Scale not changed.", mPreviewBackground.mScale > REST_SCALE
+                && mPreviewBackground.mScale < ACCEPT_SCALE_FACTOR);
+
+        mPreviewBackground.animateToRest();
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                CONSUMPTION_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator()
+                        instanceof AccelerateDecelerateInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    private void runAnimationToFraction(float animationFraction) {
+        mPreviewBackground.mScaleAnimator.setCurrentFraction(animationFraction);
+    }
+
+    private void endAnimation() {
+        mPreviewBackground.mScaleAnimator.end();
+    }
+}
diff --git a/tests/src/com/android/launcher3/icons/FastBitmapDrawableTest.java b/tests/src/com/android/launcher3/icons/FastBitmapDrawableTest.java
new file mode 100644
index 0000000..038c98b
--- /dev/null
+++ b/tests/src/com/android/launcher3/icons/FastBitmapDrawableTest.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.icons;
+
+import static com.android.launcher3.icons.FastBitmapDrawable.CLICK_FEEDBACK_DURATION;
+import static com.android.launcher3.icons.FastBitmapDrawable.HOVERED_SCALE;
+import static com.android.launcher3.icons.FastBitmapDrawable.HOVER_FEEDBACK_DURATION;
+import static com.android.launcher3.icons.FastBitmapDrawable.PRESSED_SCALE;
+import static com.android.launcher3.icons.FastBitmapDrawable.SCALE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Bitmap;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.PathInterpolator;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Spy;
+
+/**
+ * Tests for FastBitmapDrawable.
+ */
+@SmallTest
+@UiThreadTest
+@RunWith(AndroidJUnit4.class)
+public class FastBitmapDrawableTest {
+    private static final float EPSILON = 0.00001f;
+
+    @Spy
+    FastBitmapDrawable mFastBitmapDrawable =
+            spy(new FastBitmapDrawable(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)));
+
+    @Before
+    public void setUp() {
+        FastBitmapDrawable.setFlagHoverEnabled(true);
+        when(mFastBitmapDrawable.isVisible()).thenReturn(true);
+        mFastBitmapDrawable.mIsPressed = false;
+        mFastBitmapDrawable.mIsHovered = false;
+        mFastBitmapDrawable.resetScale();
+    }
+
+    @Test
+    public void testOnStateChange_noState() {
+        int[] state = new int[]{};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // No scale changes without state change.
+        assertFalse("State change handled.", isHandled);
+        assertNull("Scale animation not null.", mFastBitmapDrawable.mScaleAnimation);
+    }
+
+    @Test
+    public void testOnStateChange_statePressed() {
+        int[] state = new int[]{android.R.attr.state_pressed};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // Animate to state pressed.
+        assertTrue("State change not handled.", isHandled);
+        assertEquals("Duration not correct.", mFastBitmapDrawable.mScaleAnimation.getDuration(),
+                CLICK_FEEDBACK_DURATION);
+        mFastBitmapDrawable.mScaleAnimation.end();
+        assertEquals("End value not correct.",
+                (float) SCALE.get(mFastBitmapDrawable), PRESSED_SCALE, EPSILON);
+        assertTrue("Wrong interpolator used.",
+                mFastBitmapDrawable.mScaleAnimation.getInterpolator()
+                        instanceof AccelerateInterpolator);
+    }
+
+    @Test
+    public void testOnStateChange_stateHovered() {
+        int[] state = new int[]{android.R.attr.state_hovered};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // Animate to state hovered.
+        assertTrue("State change not handled.", isHandled);
+        assertEquals("Duration not correct.", mFastBitmapDrawable.mScaleAnimation.getDuration(),
+                HOVER_FEEDBACK_DURATION);
+        mFastBitmapDrawable.mScaleAnimation.end();
+        assertEquals("End value not correct.",
+                (float) SCALE.get(mFastBitmapDrawable), HOVERED_SCALE, EPSILON);
+        assertTrue("Wrong interpolator used.",
+                mFastBitmapDrawable.mScaleAnimation.getInterpolator() instanceof PathInterpolator);
+    }
+
+    @Test
+    public void testOnStateChange_stateHoveredFlagDisabled() {
+        FastBitmapDrawable.setFlagHoverEnabled(false);
+        int[] state = new int[]{android.R.attr.state_hovered};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // No state change with flag disabled.
+        assertFalse("Hover state change handled with flag disabled.", isHandled);
+        assertNull("Animation should not run with hover flag disabled.",
+                mFastBitmapDrawable.mScaleAnimation);
+    }
+
+    @Test
+    public void testOnStateChange_statePressedAndHovered() {
+        int[] state = new int[]{android.R.attr.state_pressed, android.R.attr.state_hovered};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // Animate to pressed state only.
+        assertTrue("State change not handled.", isHandled);
+        assertEquals("Duration not correct.", mFastBitmapDrawable.mScaleAnimation.getDuration(),
+                CLICK_FEEDBACK_DURATION);
+        mFastBitmapDrawable.mScaleAnimation.end();
+        assertEquals("End value not correct.",
+                (float) SCALE.get(mFastBitmapDrawable), PRESSED_SCALE, EPSILON);
+        assertTrue("Wrong interpolator used.",
+                mFastBitmapDrawable.mScaleAnimation.getInterpolator()
+                        instanceof AccelerateInterpolator);
+    }
+
+    @Test
+    public void testOnStateChange_stateHoveredAndPressed() {
+        int[] state = new int[]{android.R.attr.state_hovered, android.R.attr.state_pressed};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // Animate to pressed state only.
+        assertTrue("State change not handled.", isHandled);
+        assertEquals("Duration not correct.", mFastBitmapDrawable.mScaleAnimation.getDuration(),
+                CLICK_FEEDBACK_DURATION);
+        mFastBitmapDrawable.mScaleAnimation.end();
+        assertEquals("End value not correct.",
+                (float) SCALE.get(mFastBitmapDrawable), PRESSED_SCALE, EPSILON);
+        assertTrue("Wrong interpolator used.",
+                mFastBitmapDrawable.mScaleAnimation.getInterpolator()
+                        instanceof AccelerateInterpolator);
+    }
+
+    @Test
+    public void testOnStateChange_stateHoveredAndPressedToPressed() {
+        mFastBitmapDrawable.mIsPressed = true;
+        mFastBitmapDrawable.mIsHovered = true;
+        SCALE.setValue(mFastBitmapDrawable, PRESSED_SCALE);
+        int[] state = new int[]{android.R.attr.state_pressed};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // No scale change from pressed state to pressed state.
+        assertTrue("State not changed.", isHandled);
+        assertEquals("End value not correct.",
+                (float) SCALE.get(mFastBitmapDrawable), PRESSED_SCALE, EPSILON);
+    }
+
+    @Test
+    public void testOnStateChange_stateHoveredAndPressedToHovered() {
+        mFastBitmapDrawable.mIsPressed = true;
+        mFastBitmapDrawable.mIsHovered = true;
+        SCALE.setValue(mFastBitmapDrawable, PRESSED_SCALE);
+        int[] state = new int[]{android.R.attr.state_hovered};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // No scale change from pressed state to hovered state.
+        assertTrue("State not changed.", isHandled);
+        assertEquals("End value not correct.",
+                (float) SCALE.get(mFastBitmapDrawable), HOVERED_SCALE, EPSILON);
+    }
+
+    @Test
+    public void testOnStateChange_stateHoveredToPressed() {
+        mFastBitmapDrawable.mIsHovered = true;
+        SCALE.setValue(mFastBitmapDrawable, HOVERED_SCALE);
+        int[] state = new int[]{android.R.attr.state_pressed};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // No scale change from pressed state to hovered state.
+        assertTrue("State not changed.", isHandled);
+        assertEquals("End value not correct.",
+                (float) SCALE.get(mFastBitmapDrawable), PRESSED_SCALE, EPSILON);
+    }
+
+    @Test
+    public void testOnStateChange_statePressedToHovered() {
+        mFastBitmapDrawable.mIsPressed = true;
+        SCALE.setValue(mFastBitmapDrawable, PRESSED_SCALE);
+        int[] state = new int[]{android.R.attr.state_hovered};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // No scale change from pressed state to hovered state.
+        assertTrue("State not changed.", isHandled);
+        assertEquals("End value not correct.",
+                (float) SCALE.get(mFastBitmapDrawable), HOVERED_SCALE, EPSILON);
+    }
+
+    @Test
+    public void testOnStateChange_stateDefaultFromPressed() {
+        mFastBitmapDrawable.mIsPressed = true;
+        SCALE.setValue(mFastBitmapDrawable, PRESSED_SCALE);
+        int[] state = new int[]{};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // Animate to default state from pressed state.
+        assertTrue("State change not handled.", isHandled);
+        assertEquals("Duration not correct.", mFastBitmapDrawable.mScaleAnimation.getDuration(),
+                CLICK_FEEDBACK_DURATION);
+        mFastBitmapDrawable.mScaleAnimation.end();
+        assertEquals("End value not correct.", (float) SCALE.get(mFastBitmapDrawable), 1f, EPSILON);
+        assertTrue("Wrong interpolator used.",
+                mFastBitmapDrawable.mScaleAnimation.getInterpolator()
+                        instanceof DecelerateInterpolator);
+    }
+
+    @Test
+    public void testOnStateChange_stateDefaultFromHovered() {
+        mFastBitmapDrawable.mIsHovered = true;
+        SCALE.setValue(mFastBitmapDrawable, HOVERED_SCALE);
+        int[] state = new int[]{};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // Animate to default state from hovered state.
+        assertTrue("State change not handled.", isHandled);
+        assertEquals("Duration not correct.", mFastBitmapDrawable.mScaleAnimation.getDuration(),
+                HOVER_FEEDBACK_DURATION);
+        mFastBitmapDrawable.mScaleAnimation.end();
+        assertEquals("End value not correct.", (float) SCALE.get(mFastBitmapDrawable), 1f, EPSILON);
+        assertTrue("Wrong interpolator used.",
+                mFastBitmapDrawable.mScaleAnimation.getInterpolator() instanceof PathInterpolator);
+    }
+
+    @Test
+    public void testOnStateChange_stateHoveredWhilePartiallyScaled() {
+        SCALE.setValue(mFastBitmapDrawable, 0.5f);
+        int[] state = new int[]{android.R.attr.state_hovered};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // Animate to hovered state from midway to pressed state.
+        assertTrue("State change not handled.", isHandled);
+        assertEquals("Duration not correct.",
+                mFastBitmapDrawable.mScaleAnimation.getDuration(), HOVER_FEEDBACK_DURATION);
+        mFastBitmapDrawable.mScaleAnimation.end();
+        assertEquals("End value not correct.",
+                (float) SCALE.get(mFastBitmapDrawable), HOVERED_SCALE, EPSILON);
+        assertTrue("Wrong interpolator used.",
+                mFastBitmapDrawable.mScaleAnimation.getInterpolator() instanceof PathInterpolator);
+    }
+
+    @Test
+    public void testOnStateChange_statePressedWhilePartiallyScaled() {
+        SCALE.setValue(mFastBitmapDrawable, 0.5f);
+        int[] state = new int[]{android.R.attr.state_pressed};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // Animate to pressed state from midway to hovered state.
+        assertTrue("State change not handled.", isHandled);
+        assertEquals("Duration not correct.",
+                mFastBitmapDrawable.mScaleAnimation.getDuration(), CLICK_FEEDBACK_DURATION);
+        mFastBitmapDrawable.mScaleAnimation.end();
+        assertEquals("End value not correct.",
+                (float) SCALE.get(mFastBitmapDrawable), PRESSED_SCALE, EPSILON);
+        assertTrue("Wrong interpolator used.",
+                mFastBitmapDrawable.mScaleAnimation.getInterpolator()
+                        instanceof AccelerateInterpolator);
+    }
+
+    @Test
+    public void testOnStateChange_stateDefaultFromPressedNotVisible() {
+        when(mFastBitmapDrawable.isVisible()).thenReturn(false);
+        mFastBitmapDrawable.mIsPressed = true;
+        SCALE.setValue(mFastBitmapDrawable, PRESSED_SCALE);
+        clearInvocations(mFastBitmapDrawable);
+        int[] state = new int[]{};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // No animations when state was pressed but drawable no longer visible. Set values directly.
+        assertTrue("State change not handled.", isHandled);
+        assertNull("Scale animation not null.", mFastBitmapDrawable.mScaleAnimation);
+        assertEquals("End value not correct.", (float) SCALE.get(mFastBitmapDrawable), 1f, EPSILON);
+        verify(mFastBitmapDrawable).invalidateSelf();
+    }
+
+    @Test
+    public void testOnStateChange_stateDefaultFromHoveredNotVisible() {
+        when(mFastBitmapDrawable.isVisible()).thenReturn(false);
+        mFastBitmapDrawable.mIsHovered = true;
+        SCALE.setValue(mFastBitmapDrawable, HOVERED_SCALE);
+        clearInvocations(mFastBitmapDrawable);
+        int[] state = new int[]{};
+
+        boolean isHandled = mFastBitmapDrawable.onStateChange(state);
+
+        // No animations when state was hovered but drawable no longer visible. Set values directly.
+        assertTrue("State change not handled.", isHandled);
+        assertNull("Scale animation not null.", mFastBitmapDrawable.mScaleAnimation);
+        assertEquals("End value not correct.", (float) SCALE.get(mFastBitmapDrawable), 1f, EPSILON);
+        verify(mFastBitmapDrawable).invalidateSelf();
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/AbstractWorkspaceModelTest.kt b/tests/src/com/android/launcher3/model/AbstractWorkspaceModelTest.kt
index 03352fe..98191fe 100644
--- a/tests/src/com/android/launcher3/model/AbstractWorkspaceModelTest.kt
+++ b/tests/src/com/android/launcher3/model/AbstractWorkspaceModelTest.kt
@@ -17,17 +17,18 @@
 
 import android.content.ComponentName
 import android.content.Context
-import android.content.Intent
 import android.graphics.Rect
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherAppState
-import com.android.launcher3.LauncherSettings
+import com.android.launcher3.model.data.AppInfo
 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.LauncherLayoutBuilder
 import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY
+import com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE
 import java.util.UUID
 
 /** Base class for workspace related tests. */
@@ -38,6 +39,7 @@
         val nonEmptyScreenSpaces = listOf(Rect(1, 2, 3, 4))
     }
 
+    protected lateinit var mLayoutBuilder: LauncherLayoutBuilder
     protected lateinit var mTargetContext: Context
     protected lateinit var mIdp: InvariantDeviceProfile
     protected lateinit var mAppState: LauncherAppState
@@ -47,6 +49,7 @@
     protected lateinit var mScreenOccupancy: IntSparseArrayMap<GridOccupancy>
 
     open fun setup() {
+        mLayoutBuilder = LauncherLayoutBuilder()
         mModelHelper = LauncherModelHelper()
         mTargetContext = mModelHelper.sandboxContext
         mIdp = InvariantDeviceProfile.INSTANCE[mTargetContext]
@@ -64,10 +67,11 @@
 
     /** Sets up workspaces with the given screen IDs with some items and a 2x2 space. */
     fun setupWorkspaces(screenIdsWithItems: List<Int>) {
-        var nextItemId = 1
-        screenIdsWithItems.forEach { screenId ->
-            nextItemId = setupWorkspace(nextItemId, screenId, nonEmptyScreenSpaces)
-        }
+        screenIdsWithItems.forEach { screenId -> setupWorkspace(screenId, nonEmptyScreenSpaces) }
+        mModelHelper.setupDefaultLayoutProvider(mLayoutBuilder)
+        mIdp.numRows = 5
+        mIdp.numColumns = mIdp.numRows
+        mModelHelper.loadModelSync()
     }
 
     /**
@@ -78,30 +82,23 @@
         screen1: List<Rect>? = null,
         screen2: List<Rect>? = null,
         screen3: List<Rect>? = null,
-    ) = listOf(screen0, screen1, screen2, screen3).let(this::setupWithSpaces)
+    ) {
+        listOf(screen0, screen1, screen2, screen3).let(this::setupWithSpaces)
+        mModelHelper.setupDefaultLayoutProvider(mLayoutBuilder)
+        mIdp.numRows = 5
+        mIdp.numColumns = mIdp.numRows
+        mModelHelper.loadModelSync()
+    }
 
     private fun setupWithSpaces(workspaceSpaces: List<List<Rect>?>) {
-        var nextItemId = 1
         workspaceSpaces.forEachIndexed { screenId, spaces ->
             if (spaces != null) {
-                nextItemId = setupWorkspace(nextItemId, screenId, spaces)
+                setupWorkspace(screenId, spaces)
             }
         }
     }
 
-    private fun setupWorkspace(startId: Int, screenId: Int, spaces: List<Rect>): Int {
-        return mModelHelper.executeSimpleTask { dataModel ->
-            writeWorkspaceWithSpaces(dataModel, startId, screenId, spaces)
-        }
-    }
-
-    private fun writeWorkspaceWithSpaces(
-        bgDataModel: BgDataModel,
-        itemStartId: Int,
-        screenId: Int,
-        spaces: List<Rect>,
-    ): Int {
-        var itemId = itemStartId
+    private fun setupWorkspace(screenId: Int, spaces: List<Rect>) {
         val occupancy = GridOccupancy(mIdp.numColumns, mIdp.numRows)
         occupancy.markCells(0, 0, mIdp.numColumns, mIdp.numRows, true)
         spaces.forEach { spaceRect -> occupancy.markCells(spaceRect, false) }
@@ -109,35 +106,22 @@
         mScreenOccupancy.append(screenId, occupancy)
         for (x in 0 until mIdp.numColumns) {
             for (y in 0 until mIdp.numRows) {
-                if (!occupancy.cells[x][y]) {
-                    continue
+                if (occupancy.cells[x][y]) {
+                    mLayoutBuilder.atWorkspace(x, y, screenId).putApp(TEST_PACKAGE, TEST_ACTIVITY)
                 }
-                val info = getExistingItem()
-                info.id = itemId++
-                info.screenId = screenId
-                info.cellX = x
-                info.cellY = y
-                info.container = LauncherSettings.Favorites.CONTAINER_DESKTOP
-                bgDataModel.addItem(mTargetContext, info, false)
-                val writer = ContentWriter(mTargetContext)
-                info.writeToValues(writer)
-                writer.put(LauncherSettings.Favorites._ID, info.id)
-                mTargetContext.contentResolver.insert(
-                    LauncherSettings.Favorites.CONTENT_URI,
-                    writer.getValues(mTargetContext)
-                )
             }
         }
-        return itemId
     }
 
     fun getExistingItem() =
-        WorkspaceItemInfo().apply { intent = Intent().setComponent(ComponentName("a", "b")) }
+        WorkspaceItemInfo().apply {
+            intent = AppInfo.makeLaunchIntent(ComponentName(TEST_PACKAGE, TEST_ACTIVITY))
+        }
 
     fun getNewItem(): WorkspaceItemInfo {
         val itemPackage = UUID.randomUUID().toString()
         return WorkspaceItemInfo().apply {
-            intent = Intent().setComponent(ComponentName(itemPackage, itemPackage))
+            intent = AppInfo.makeLaunchIntent(ComponentName(itemPackage, itemPackage))
         }
     }
 }
diff --git a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
index 6636b8a..1155227 100644
--- a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
@@ -23,7 +23,7 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.IntArray
-import com.android.launcher3.util.IntSet
+import com.android.launcher3.util.TestUtil.runOnExecutorSync
 import com.android.launcher3.util.any
 import com.android.launcher3.util.eq
 import com.android.launcher3.util.same
@@ -32,8 +32,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
@@ -46,11 +44,7 @@
 @RunWith(AndroidJUnit4::class)
 class AddWorkspaceItemsTaskTest : AbstractWorkspaceModelTest() {
 
-    @Captor private lateinit var mAnimatedItemArgumentCaptor: ArgumentCaptor<ArrayList<ItemInfo>>
-
-    @Captor private lateinit var mNotAnimatedItemArgumentCaptor: ArgumentCaptor<ArrayList<ItemInfo>>
-
-    @Mock private lateinit var mDataModelCallbacks: BgDataModel.Callbacks
+    private lateinit var mDataModelCallbacks: MyCallbacks
 
     @Mock private lateinit var mWorkspaceItemSpaceFinder: WorkspaceItemSpaceFinder
 
@@ -58,7 +52,7 @@
     override fun setup() {
         super.setup()
         MockitoAnnotations.initMocks(this)
-        whenever(mDataModelCallbacks.getPagesToBindSynchronously(any())).thenReturn(IntSet())
+        mDataModelCallbacks = MyCallbacks()
         Executors.MAIN_EXECUTOR.submit { mModelHelper.model.addCallbacks(mDataModelCallbacks) }
             .get()
     }
@@ -105,7 +99,7 @@
         val addedItems = testAddItems(nonEmptyScreenIds, itemToAdd)
 
         assertThat(addedItems.size).isEqualTo(0)
-        verifyZeroInteractions(mWorkspaceItemSpaceFinder, mDataModelCallbacks)
+        verifyZeroInteractions(mWorkspaceItemSpaceFinder)
     }
 
     @Test
@@ -191,22 +185,14 @@
     ): List<AddedItem> {
         setupWorkspaces(nonEmptyScreenIds)
         val task = newTask(*itemsToAdd)
-        var updateCount = 0
-        mModelHelper.executeTaskForTest(task).forEach {
-            updateCount++
-            it.run()
-        }
 
         val addedItems = mutableListOf<AddedItem>()
-        if (updateCount > 0) {
-            verify(mDataModelCallbacks)
-                .bindAppsAdded(
-                    any(),
-                    mNotAnimatedItemArgumentCaptor.capture(),
-                    mAnimatedItemArgumentCaptor.capture()
-                )
-            addedItems.addAll(mAnimatedItemArgumentCaptor.value.map { AddedItem(it, true) })
-            addedItems.addAll(mNotAnimatedItemArgumentCaptor.value.map { AddedItem(it, false) })
+
+        runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            mDataModelCallbacks.addedItems.clear()
+            mModelHelper.model.enqueueModelUpdateTask(task)
+            runOnExecutorSync(Executors.MAIN_EXECUTOR) {}
+            addedItems.addAll(mDataModelCallbacks.addedItems)
         }
 
         return addedItems
@@ -224,3 +210,17 @@
 }
 
 private data class AddedItem(val itemInfo: ItemInfo, val isAnimated: Boolean)
+
+private class MyCallbacks : BgDataModel.Callbacks {
+
+    val addedItems = mutableListOf<AddedItem>()
+
+    override fun bindAppsAdded(
+        newScreens: IntArray?,
+        addNotAnimated: ArrayList<ItemInfo>,
+        addAnimated: ArrayList<ItemInfo>
+    ) {
+        addedItems.addAll(addAnimated.map { AddedItem(it, true) })
+        addedItems.addAll(addNotAnimated.map { AddedItem(it, false) })
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index f55b244..f771052 100644
--- a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -1,32 +1,31 @@
 package com.android.launcher3.model;
 
+import static android.os.Process.myUserHandle;
+
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY2;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY3;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertTrue;
 
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
-import android.graphics.Color;
-import android.os.Process;
-import android.os.UserHandle;
-import android.os.UserManager;
 
-import androidx.annotation.NonNull;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.icons.cache.CachingLogic;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.PackageUserKey;
 
 import org.junit.After;
 import org.junit.Before;
@@ -35,6 +34,7 @@
 
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.List;
 
 /**
  * Tests for {@link CacheDataUpdatedTask}
@@ -43,49 +43,40 @@
 @RunWith(AndroidJUnit4.class)
 public class CacheDataUpdatedTaskTest {
 
-    private static final String NEW_LABEL_PREFIX = "new-label-";
+    private static final String PENDING_APP_1 = TEST_PACKAGE + ".pending1";
+    private static final String PENDING_APP_2 = TEST_PACKAGE + ".pending2";
 
     private LauncherModelHelper mModelHelper;
+    private Context mContext;
+
+    private int mSession1;
 
     @Before
     public void setup() throws Exception {
         mModelHelper = new LauncherModelHelper();
-        mModelHelper.initializeData("cache_data_updated_task_data");
+        mContext = mModelHelper.sandboxContext;
+        mSession1 = mModelHelper.createInstallerSession(PENDING_APP_1);
+        mModelHelper.createInstallerSession(PENDING_APP_2);
 
-        // Add placeholder entries in the cache to simulate update
-        Context context = mModelHelper.sandboxContext;
-        IconCache iconCache = LauncherAppState.getInstance(context).getIconCache();
-        CachingLogic<ItemInfo> placeholderLogic = new CachingLogic<ItemInfo>() {
-            @Override
-            @NonNull
-            public ComponentName getComponent(@NonNull ItemInfo info) {
-                return info.getTargetComponent();
-            }
+        LauncherLayoutBuilder builder = new LauncherLayoutBuilder()
+                .atHotseat(1).putFolder("MyFolder")
+                .addApp(TEST_PACKAGE, TEST_ACTIVITY)    // 2
+                .addApp(TEST_PACKAGE, TEST_ACTIVITY2)   // 3
+                .addApp(TEST_PACKAGE, TEST_ACTIVITY3)   // 4
 
-            @NonNull
-            @Override
-            public UserHandle getUser(@NonNull ItemInfo info) {
-                return info.user;
-            }
+                // Pending App 1
+                .addApp(PENDING_APP_1, TEST_ACTIVITY)   // 5
+                .addApp(PENDING_APP_1, TEST_ACTIVITY2)  // 6
+                .addApp(PENDING_APP_1, TEST_ACTIVITY3)  // 7
 
-            @NonNull
-            @Override
-            public CharSequence getLabel(@NonNull ItemInfo info) {
-                return NEW_LABEL_PREFIX + info.id;
-            }
-
-            @NonNull
-            @Override
-            public BitmapInfo loadIcon(@NonNull Context context, @NonNull ItemInfo info) {
-                return BitmapInfo.of(Bitmap.createBitmap(1, 1, Config.ARGB_8888), Color.RED);
-            }
-        };
-
-        UserManager um = context.getSystemService(UserManager.class);
-        for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
-            iconCache.addIconToDBAndMemCache(info, placeholderLogic, new PackageInfo(),
-                    um.getSerialNumberForUser(info.user), true);
-        }
+                // Pending App 2
+                .addApp(PENDING_APP_2, TEST_ACTIVITY)   // 8
+                .addApp(PENDING_APP_2, TEST_ACTIVITY2)  // 9
+                .addApp(PENDING_APP_2, TEST_ACTIVITY3)  // 10
+                .build();
+        mModelHelper.setupDefaultLayoutProvider(builder);
+        mModelHelper.loadModelSync();
+        assertEquals(10, mModelHelper.getBgDataModel().itemsIdMap.size());
     }
 
     @After
@@ -94,27 +85,63 @@
     }
 
     private CacheDataUpdatedTask newTask(int op, String... pkg) {
-        return new CacheDataUpdatedTask(op, Process.myUserHandle(),
+        return new CacheDataUpdatedTask(op, myUserHandle(),
                 new HashSet<>(Arrays.asList(pkg)));
     }
 
     @Test
-    public void testCacheUpdate_update_apps() throws Exception {
-        // Clear all icons from apps list so that its easy to check what was updated
-        for (AppInfo info : mModelHelper.getAllAppsList().data) {
-            info.bitmap = BitmapInfo.LOW_RES_INFO;
-        }
+    public void testCacheUpdate_update_apps() {
+        // Run on model executor so that no other task runs in the middle.
+        runOnExecutorSync(MODEL_EXECUTOR, () -> {
+            // Clear all icons from apps list so that its easy to check what was updated
+            allItems().forEach(wi -> wi.bitmap = BitmapInfo.LOW_RES_INFO);
 
-        mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, "app1"));
+            mModelHelper.getModel().enqueueModelUpdateTask(
+                    newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, TEST_PACKAGE));
 
-        // Verify that only the app icons of app1 (id 1 & 2) are updated. Custom shortcut (id 7)
-        // is not updated
-        verifyUpdate(1, 2);
+            // Verify that only the app icons of TEST_PACKAGE (id 2, 3, 4) are updated.
+            verifyUpdate(2, 3, 4);
+        });
+    }
 
-        // Verify that only app1 var updated in allAppsList
-        assertFalse(mModelHelper.getAllAppsList().data.isEmpty());
-        for (AppInfo info : mModelHelper.getAllAppsList().data) {
-            if (info.componentName.getPackageName().equals("app1")) {
+    @Test
+    public void testSessionUpdate_ignores_normal_apps() {
+        // Run on model executor so that no other task runs in the middle.
+        runOnExecutorSync(MODEL_EXECUTOR, () -> {
+            // Clear all icons from apps list so that its easy to check what was updated
+            allItems().forEach(wi -> wi.bitmap = BitmapInfo.LOW_RES_INFO);
+
+            mModelHelper.getModel().enqueueModelUpdateTask(
+                    newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, TEST_PACKAGE));
+
+            // TEST_PACKAGE has no restored shortcuts. Verify that nothing was updated.
+            verifyUpdate();
+        });
+    }
+
+    @Test
+    public void testSessionUpdate_updates_pending_apps() {
+        // Run on model executor so that no other task runs in the middle.
+        runOnExecutorSync(MODEL_EXECUTOR, () -> {
+            LauncherAppState.getInstance(mContext).getIconCache().updateSessionCache(
+                    new PackageUserKey(PENDING_APP_1, myUserHandle()),
+                    mContext.getPackageManager().getPackageInstaller().getSessionInfo(mSession1));
+
+            // Clear all icons from apps list so that its easy to check what was updated
+            allItems().forEach(wi -> wi.bitmap = BitmapInfo.LOW_RES_INFO);
+
+            mModelHelper.getModel().enqueueModelUpdateTask(
+                    newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, PENDING_APP_1));
+
+            // Only restored apps from PENDING_APP_1 (id 5, 6, 7) are updated
+            verifyUpdate(5, 6, 7);
+        });
+    }
+
+    private void verifyUpdate(int... idsUpdated) {
+        IntSet updates = IntSet.wrap(idsUpdated);
+        for (WorkspaceItemInfo info : allItems()) {
+            if (updates.contains(info.id)) {
                 assertFalse(info.bitmap.isNullOrLowRes());
             } else {
                 assertTrue(info.bitmap.isNullOrLowRes());
@@ -122,33 +149,7 @@
         }
     }
 
-    @Test
-    public void testSessionUpdate_ignores_normal_apps() throws Exception {
-        mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app1"));
-
-        // app1 has no restored shortcuts. Verify that nothing was updated.
-        verifyUpdate();
-    }
-
-    @Test
-    public void testSessionUpdate_updates_pending_apps() throws Exception {
-        mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app3"));
-
-        // app3 has only restored apps (id 5, 6) and shortcuts (id 9). Verify that only apps were
-        // were updated
-        verifyUpdate(5, 6);
-    }
-
-    private void verifyUpdate(Integer... idsUpdated) {
-        HashSet<Integer> updates = new HashSet<>(Arrays.asList(idsUpdated));
-        for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
-            if (updates.contains(info.id)) {
-                assertEquals(NEW_LABEL_PREFIX + info.id, info.title);
-                assertFalse(((WorkspaceItemInfo) info).bitmap.isNullOrLowRes());
-            } else {
-                assertNotSame(NEW_LABEL_PREFIX + info.id, info.title);
-                assertTrue(((WorkspaceItemInfo) info).bitmap.isNullOrLowRes());
-            }
-        }
+    private List<WorkspaceItemInfo> allItems() {
+        return ((FolderInfo) mModelHelper.getBgDataModel().itemsIdMap.get(1)).contents;
     }
 }
diff --git a/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt b/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
index 3b480ca..4fa5352 100644
--- a/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
+++ b/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
@@ -24,7 +24,6 @@
 import android.os.Process
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherPrefs.Companion.WORKSPACE_SIZE
@@ -108,8 +107,8 @@
     fun testMigration() {
         // Src Hotseat icons
         addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE)
-        addItem(ITEM_TYPE_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE)
-        addItem(ITEM_TYPE_SHORTCUT, 3, CONTAINER_HOTSEAT, 0, 0, testPackage3, 3, TMP_TABLE)
+        addItem(ITEM_TYPE_DEEP_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE)
+        addItem(ITEM_TYPE_DEEP_SHORTCUT, 3, CONTAINER_HOTSEAT, 0, 0, testPackage3, 3, TMP_TABLE)
         addItem(ITEM_TYPE_APPLICATION, 4, CONTAINER_HOTSEAT, 0, 0, testPackage4, 4, TMP_TABLE)
         // Src grid icons
         // _ _ _ _ _
@@ -124,7 +123,7 @@
         addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 4, 3, testPackage9, 9, TMP_TABLE)
 
         // Dest hotseat icons
-        addItem(ITEM_TYPE_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage2)
+        addItem(ITEM_TYPE_DEEP_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage2)
         // Dest grid icons
         addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 2, testPackage10)
 
@@ -219,8 +218,8 @@
         // Hotseat items in grid A
         // 1 2 _ 3 4
         addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE)
-        addItem(ITEM_TYPE_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE)
-        addItem(ITEM_TYPE_SHORTCUT, 3, CONTAINER_HOTSEAT, 0, 0, testPackage3, 3, TMP_TABLE)
+        addItem(ITEM_TYPE_DEEP_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE)
+        addItem(ITEM_TYPE_DEEP_SHORTCUT, 3, CONTAINER_HOTSEAT, 0, 0, testPackage3, 3, TMP_TABLE)
         addItem(ITEM_TYPE_APPLICATION, 4, CONTAINER_HOTSEAT, 0, 0, testPackage4, 4, TMP_TABLE)
         // Workspace items in grid A
         // _ _ _ _ _
@@ -235,7 +234,7 @@
 
         // Hotseat items in grid B
         // 2 _ _ _
-        addItem(ITEM_TYPE_SHORTCUT, 0, CONTAINER_HOTSEAT, 0, 0, testPackage2)
+        addItem(ITEM_TYPE_DEEP_SHORTCUT, 0, CONTAINER_HOTSEAT, 0, 0, testPackage2)
         // Workspace items in grid B
         // _ _ _ _
         // _ _ _ 10
@@ -291,7 +290,7 @@
                 null
             )
                 ?: throw IllegalStateException()
-        var locMap = parseLocMap(context, c)
+        var locMap = parseLocMap(c)
         // Expected items in grid B
         // _ _ _ _
         // 5 6 7 8
@@ -348,7 +347,7 @@
                 null
             )
                 ?: throw IllegalStateException()
-        locMap = parseLocMap(context, c)
+        locMap = parseLocMap(c)
         // Expected workspace items in grid A
         // _ _ _ _ _
         // _ _ _ _ 5
@@ -410,7 +409,7 @@
                 null
             )
                 ?: throw IllegalStateException()
-        locMap = parseLocMap(context, c)
+        locMap = parseLocMap(c)
         // Expected workspace items in grid B
         // _ _ _ _
         // 5 6 _ 8
@@ -436,7 +435,7 @@
         c.close()
     }
 
-    private fun parseLocMap(context: Context, c: Cursor): Map<String, Triple<Int, Int, Int>> {
+    private fun parseLocMap(c: Cursor): Map<String, Triple<Int, Int, Int>> {
         // Check workspace items
         val intentIndex = c.getColumnIndex(INTENT)
         val screenIndex = c.getColumnIndex(SCREEN)
@@ -465,7 +464,16 @@
                     1,
                     TMP_TABLE
                 ),
-                addItem(ITEM_TYPE_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE),
+                addItem(
+                    ITEM_TYPE_DEEP_SHORTCUT,
+                    1,
+                    CONTAINER_HOTSEAT,
+                    0,
+                    0,
+                    testPackage2,
+                    2,
+                    TMP_TABLE
+                ),
                 addItem(
                     ITEM_TYPE_APPLICATION,
                     2,
@@ -476,7 +484,16 @@
                     3,
                     TMP_TABLE
                 ),
-                addItem(ITEM_TYPE_SHORTCUT, 3, CONTAINER_HOTSEAT, 0, 0, testPackage4, 4, TMP_TABLE)
+                addItem(
+                    ITEM_TYPE_DEEP_SHORTCUT,
+                    3,
+                    CONTAINER_HOTSEAT,
+                    0,
+                    0,
+                    testPackage4,
+                    4,
+                    TMP_TABLE
+                )
             )
         val numSrcDatabaseHotseatIcons = srcHotseatItems.size
         idp.numDatabaseHotseatIcons = 6
@@ -532,9 +549,9 @@
     @Test
     fun migrateFromLargerHotseat() {
         addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE)
-        addItem(ITEM_TYPE_SHORTCUT, 2, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE)
+        addItem(ITEM_TYPE_DEEP_SHORTCUT, 2, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE)
         addItem(ITEM_TYPE_APPLICATION, 3, CONTAINER_HOTSEAT, 0, 0, testPackage3, 3, TMP_TABLE)
-        addItem(ITEM_TYPE_SHORTCUT, 4, CONTAINER_HOTSEAT, 0, 0, testPackage4, 4, TMP_TABLE)
+        addItem(ITEM_TYPE_DEEP_SHORTCUT, 4, CONTAINER_HOTSEAT, 0, 0, testPackage4, 4, TMP_TABLE)
         addItem(ITEM_TYPE_APPLICATION, 5, CONTAINER_HOTSEAT, 0, 0, testPackage5, 5, TMP_TABLE)
 
         idp.numDatabaseHotseatIcons = 4
diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 78812c0..544ed6b 100644
--- a/tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -30,7 +30,7 @@
 import static com.android.launcher3.LauncherSettings.Favorites.INTENT;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
 import static com.android.launcher3.LauncherSettings.Favorites.OPTIONS;
 import static com.android.launcher3.LauncherSettings.Favorites.PROFILE_ID;
 import static com.android.launcher3.LauncherSettings.Favorites.RANK;
@@ -158,13 +158,13 @@
 
     @Test
     public void loadSimpleShortcut() {
-        initCursor(ITEM_TYPE_SHORTCUT, "my-shortcut");
+        initCursor(ITEM_TYPE_DEEP_SHORTCUT, "my-shortcut");
         assertTrue(mLoaderCursor.moveToNext());
 
         WorkspaceItemInfo info = mLoaderCursor.loadSimpleWorkspaceItem();
         assertTrue(mApp.getIconCache().isDefaultIcon(info.bitmap, info.user));
         assertEquals("my-shortcut", info.title);
-        assertEquals(ITEM_TYPE_SHORTCUT, info.itemType);
+        assertEquals(ITEM_TYPE_DEEP_SHORTCUT, info.itemType);
     }
 
     @Test
diff --git a/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
index 519191e..4ba61ac 100644
--- a/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
+++ b/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
@@ -1,5 +1,12 @@
 package com.android.launcher3.model;
 
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY2;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY3;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
+
 import static org.junit.Assert.assertEquals;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -9,6 +16,8 @@
 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.IntSet;
+import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.LauncherModelHelper;
 
 import org.junit.After;
@@ -16,9 +25,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.Arrays;
-import java.util.HashSet;
-
 /**
  * Tests for {@link PackageInstallStateChangedTask}
  */
@@ -26,12 +32,36 @@
 @RunWith(AndroidJUnit4.class)
 public class PackageInstallStateChangedTaskTest {
 
+    private static final String PENDING_APP_1 = TEST_PACKAGE + ".pending1";
+    private static final String PENDING_APP_2 = TEST_PACKAGE + ".pending2";
+
     private LauncherModelHelper mModelHelper;
+    private IntSet mDownloadingApps;
 
     @Before
     public void setup() throws Exception {
         mModelHelper = new LauncherModelHelper();
-        mModelHelper.initializeData("package_install_state_change_task_data");
+        mModelHelper.createInstallerSession(PENDING_APP_1);
+        mModelHelper.createInstallerSession(PENDING_APP_2);
+
+        LauncherLayoutBuilder builder = new LauncherLayoutBuilder()
+                .atWorkspace(0, 0, 1).putApp(TEST_PACKAGE, TEST_ACTIVITY)               // 1
+                .atWorkspace(0, 0, 2).putApp(TEST_PACKAGE, TEST_ACTIVITY2)              // 2
+                .atWorkspace(0, 0, 3).putApp(TEST_PACKAGE, TEST_ACTIVITY3)              // 3
+
+                .atWorkspace(0, 0, 4).putApp(PENDING_APP_1, TEST_ACTIVITY)              // 4
+                .atWorkspace(0, 0, 5).putApp(PENDING_APP_1, TEST_ACTIVITY2)             // 5
+                .atWorkspace(0, 0, 6).putApp(PENDING_APP_1, TEST_ACTIVITY3)             // 6
+                .atWorkspace(0, 0, 7).putWidget(PENDING_APP_1, "pending.widget", 1, 1)  // 7
+
+                .atWorkspace(0, 0, 8).putApp(PENDING_APP_2, TEST_ACTIVITY)              // 8
+                .atWorkspace(0, 0, 9).putApp(PENDING_APP_2, TEST_ACTIVITY2)             // 9
+                .atWorkspace(0, 0, 10).putApp(PENDING_APP_2, TEST_ACTIVITY3);           // 10
+
+        mDownloadingApps = IntSet.wrap(4, 5, 6, 7, 8, 9, 10);
+        mModelHelper.setupDefaultLayoutProvider(builder);
+        mModelHelper.loadModelSync();
+        assertEquals(10, mModelHelper.getBgDataModel().itemsIdMap.size());
     }
 
     @After
@@ -47,36 +77,45 @@
     }
 
     @Test
-    public void testSessionUpdate_ignore_installed() throws Exception {
-        mModelHelper.executeTaskForTest(newTask("app1", 30));
+    public void testSessionUpdate_ignore_installed() {
+        // Run on model executor so that no other task runs in the middle.
+        runOnExecutorSync(MODEL_EXECUTOR, () -> {
+            mModelHelper.getModel().enqueueModelUpdateTask(newTask(TEST_PACKAGE, 30));
 
-        // No shortcuts were updated
-        verifyProgressUpdate(0);
+            // No shortcuts were updated
+            verifyProgressUpdate(0);
+        });
     }
 
     @Test
-    public void testSessionUpdate_shortcuts_updated() throws Exception {
-        mModelHelper.executeTaskForTest(newTask("app3", 30));
+    public void testSessionUpdate_shortcuts_updated() {
+        // Run on model executor so that no other task runs in the middle.
+        runOnExecutorSync(MODEL_EXECUTOR, () -> {
+            mModelHelper.getModel().enqueueModelUpdateTask(newTask(PENDING_APP_1, 30));
 
-        verifyProgressUpdate(30, 5, 6, 7);
+            verifyProgressUpdate(30, 4, 5, 6, 7);
+        });
     }
 
     @Test
-    public void testSessionUpdate_widgets_updated() throws Exception {
-        mModelHelper.executeTaskForTest(newTask("app4", 30));
+    public void testSessionUpdate_widgets_updated() {
+        // Run on model executor so that no other task runs in the middle.
+        runOnExecutorSync(MODEL_EXECUTOR, () -> {
+            mModelHelper.getModel().enqueueModelUpdateTask(newTask(PENDING_APP_2, 30));
 
-        verifyProgressUpdate(30, 8, 9);
+            verifyProgressUpdate(30, 8, 9, 10);
+        });
     }
 
-    private void verifyProgressUpdate(int progress, Integer... idsUpdated) {
-        HashSet<Integer> updates = new HashSet<>(Arrays.asList(idsUpdated));
+    private void verifyProgressUpdate(int progress, int... idsUpdated) {
+        IntSet updates = IntSet.wrap(idsUpdated);
         for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
-            if (info instanceof WorkspaceItemInfo) {
-                assertEquals(updates.contains(info.id) ? progress: 100,
-                        ((WorkspaceItemInfo) info).getProgressLevel());
+            int expectedProgress = updates.contains(info.id) ? progress
+                    : (mDownloadingApps.contains(info.id) ? 0 : 100);
+            if (info instanceof WorkspaceItemInfo wi) {
+                assertEquals(expectedProgress, wi.getProgressLevel());
             } else {
-                assertEquals(updates.contains(info.id) ? progress: -1,
-                        ((LauncherAppWidgetInfo) info).installProgress);
+                assertEquals(expectedProgress, ((LauncherAppWidgetInfo) info).installProgress);
             }
         }
     }
diff --git a/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt b/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
index a81413e..270672f 100644
--- a/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
+++ b/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
@@ -64,8 +64,8 @@
                     "\tinv.numColumns: 5\n" +
                     "\tinv.numSearchContainerColumns: 5\n" +
                     "\tminCellSize: PointF(0.0, 0.0)dp\n" +
-                    "\tcellWidthPx: 165.0px (62.857143dp)\n" +
-                    "\tcellHeightPx: 235.0px (89.52381dp)\n" +
+                    "\tcellWidthPx: 159.0px (60.57143dp)\n" +
+                    "\tcellHeightPx: 229.0px (87.2381dp)\n" +
                     "\tgetCellSize().x: 207.0px (78.85714dp)\n" +
                     "\tgetCellSize().y: 379.0px (144.38095dp)\n" +
                     "\tcellLayoutBorderSpacePx Horizontal: 0.0px (0.0dp)\n" +
@@ -76,15 +76,16 @@
                     "\tcellLayoutPaddingPx.bottom: 28.0px (10.666667dp)\n" +
                     "\ticonSizePx: 147.0px (56.0dp)\n" +
                     "\ticonTextSizePx: 38.0px (14.476191dp)\n" +
-                    "\ticonDrawablePaddingPx: 18.0px (6.857143dp)\n" +
+                    "\ticonDrawablePaddingPx: 12.0px (4.571429dp)\n" +
                     "\tinv.numFolderRows: 4\n" +
                     "\tinv.numFolderColumns: 4\n" +
                     "\tfolderCellWidthPx: 195.0px (74.28571dp)\n" +
                     "\tfolderCellHeightPx: 230.0px (87.61905dp)\n" +
                     "\tfolderChildIconSizePx: 147.0px (56.0dp)\n" +
                     "\tfolderChildTextSizePx: 38.0px (14.476191dp)\n" +
-                    "\tfolderChildDrawablePaddingPx: 10.0px (3.8095238dp)\n" +
-                    "\tfolderCellLayoutBorderSpacePx: 0.0px (0.0dp)\n" +
+                    "\tfolderChildDrawablePaddingPx: 4.0px (1.5238096dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.x: 0.0px (0.0dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.y: 0.0px (0.0dp)\n" +
                     "\tfolderContentPaddingLeftRight: 21.0px (8.0dp)\n" +
                     "\tfolderTopPadding: 63.0px (24.0dp)\n" +
                     "\tfolderFooterHeight: 147.0px (56.0dp)\n" +
@@ -105,7 +106,7 @@
                     "\tallAppsBorderSpacePxX: 42.0px (16.0dp)\n" +
                     "\tallAppsBorderSpacePxY: 42.0px (16.0dp)\n" +
                     "\tnumShownAllAppsColumns: 5\n" +
-                    "\tallAppsLeftRightPadding: 21.0px (8.0dp)\n" +
+                    "\tallAppsLeftRightPadding: 0.0px (0.0dp)\n" +
                     "\tallAppsLeftRightMargin: 0.0px (0.0dp)\n" +
                     "\thotseatBarSizePx: 294.0px (112.0dp)\n" +
                     "\tinv.hotseatColumnSpan: 5\n" +
@@ -138,8 +139,8 @@
                     "\tworkspacePadding.bottom: 203.0px (77.333336dp)\n" +
                     "\ticonScale: 1.0px (0.3809524dp)\n" +
                     "\tcellScaleToFit : 1.0px (0.3809524dp)\n" +
-                    "\textraSpace: 722.0px (275.0476dp)\n" +
-                    "\tunscaled extraSpace: 722.0px (275.0476dp)\n" +
+                    "\textraSpace: 752.0px (286.4762dp)\n" +
+                    "\tunscaled extraSpace: 752.0px (286.4762dp)\n" +
                     "\tmaxEmptySpace: 0.0px (0.0dp)\n" +
                     "\tworkspaceTopPadding: 0.0px (0.0dp)\n" +
                     "\tworkspaceBottomPadding: 0.0px (0.0dp)\n" +
@@ -200,8 +201,8 @@
                     "\tinv.numColumns: 5\n" +
                     "\tinv.numSearchContainerColumns: 5\n" +
                     "\tminCellSize: PointF(0.0, 0.0)dp\n" +
-                    "\tcellWidthPx: 165.0px (62.857143dp)\n" +
-                    "\tcellHeightPx: 235.0px (89.52381dp)\n" +
+                    "\tcellWidthPx: 159.0px (60.57143dp)\n" +
+                    "\tcellHeightPx: 229.0px (87.2381dp)\n" +
                     "\tgetCellSize().x: 207.0px (78.85714dp)\n" +
                     "\tgetCellSize().y: 383.0px (145.90475dp)\n" +
                     "\tcellLayoutBorderSpacePx Horizontal: 0.0px (0.0dp)\n" +
@@ -212,15 +213,16 @@
                     "\tcellLayoutPaddingPx.bottom: 28.0px (10.666667dp)\n" +
                     "\ticonSizePx: 147.0px (56.0dp)\n" +
                     "\ticonTextSizePx: 38.0px (14.476191dp)\n" +
-                    "\ticonDrawablePaddingPx: 18.0px (6.857143dp)\n" +
+                    "\ticonDrawablePaddingPx: 12.0px (4.571429dp)\n" +
                     "\tinv.numFolderRows: 4\n" +
                     "\tinv.numFolderColumns: 4\n" +
                     "\tfolderCellWidthPx: 195.0px (74.28571dp)\n" +
                     "\tfolderCellHeightPx: 230.0px (87.61905dp)\n" +
                     "\tfolderChildIconSizePx: 147.0px (56.0dp)\n" +
                     "\tfolderChildTextSizePx: 38.0px (14.476191dp)\n" +
-                    "\tfolderChildDrawablePaddingPx: 10.0px (3.8095238dp)\n" +
-                    "\tfolderCellLayoutBorderSpacePx: 0.0px (0.0dp)\n" +
+                    "\tfolderChildDrawablePaddingPx: 4.0px (1.5238096dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.x: 0.0px (0.0dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.y: 0.0px (0.0dp)\n" +
                     "\tfolderContentPaddingLeftRight: 21.0px (8.0dp)\n" +
                     "\tfolderTopPadding: 63.0px (24.0dp)\n" +
                     "\tfolderFooterHeight: 147.0px (56.0dp)\n" +
@@ -241,7 +243,7 @@
                     "\tallAppsBorderSpacePxX: 42.0px (16.0dp)\n" +
                     "\tallAppsBorderSpacePxY: 42.0px (16.0dp)\n" +
                     "\tnumShownAllAppsColumns: 5\n" +
-                    "\tallAppsLeftRightPadding: 21.0px (8.0dp)\n" +
+                    "\tallAppsLeftRightPadding: 0.0px (0.0dp)\n" +
                     "\tallAppsLeftRightMargin: 0.0px (0.0dp)\n" +
                     "\thotseatBarSizePx: 273.0px (104.0dp)\n" +
                     "\tinv.hotseatColumnSpan: 5\n" +
@@ -274,8 +276,8 @@
                     "\tworkspacePadding.bottom: 245.0px (93.333336dp)\n" +
                     "\ticonScale: 1.0px (0.3809524dp)\n" +
                     "\tcellScaleToFit : 1.0px (0.3809524dp)\n" +
-                    "\textraSpace: 743.0px (283.0476dp)\n" +
-                    "\tunscaled extraSpace: 743.0px (283.0476dp)\n" +
+                    "\textraSpace: 773.0px (294.4762dp)\n" +
+                    "\tunscaled extraSpace: 773.0px (294.4762dp)\n" +
                     "\tmaxEmptySpace: 0.0px (0.0dp)\n" +
                     "\tworkspaceTopPadding: 0.0px (0.0dp)\n" +
                     "\tworkspaceBottomPadding: 0.0px (0.0dp)\n" +
@@ -336,7 +338,7 @@
                     "\tinv.numColumns: 5\n" +
                     "\tinv.numSearchContainerColumns: 5\n" +
                     "\tminCellSize: PointF(0.0, 0.0)dp\n" +
-                    "\tcellWidthPx: 158.0px (60.190475dp)\n" +
+                    "\tcellWidthPx: 152.0px (57.904762dp)\n" +
                     "\tcellHeightPx: 166.0px (63.238094dp)\n" +
                     "\tgetCellSize().x: 368.0px (140.19048dp)\n" +
                     "\tgetCellSize().y: 193.0px (73.52381dp)\n" +
@@ -355,8 +357,9 @@
                     "\tfolderCellHeightPx: 205.0px (78.09524dp)\n" +
                     "\tfolderChildIconSizePx: 131.0px (49.904762dp)\n" +
                     "\tfolderChildTextSizePx: 34.0px (12.952381dp)\n" +
-                    "\tfolderChildDrawablePaddingPx: 9.0px (3.4285715dp)\n" +
-                    "\tfolderCellLayoutBorderSpacePx: 0.0px (0.0dp)\n" +
+                    "\tfolderChildDrawablePaddingPx: 4.0px (1.5238096dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.x: 0.0px (0.0dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.y: 0.0px (0.0dp)\n" +
                     "\tfolderContentPaddingLeftRight: 21.0px (8.0dp)\n" +
                     "\tfolderTopPadding: 56.0px (21.333334dp)\n" +
                     "\tfolderFooterHeight: 131.0px (49.904762dp)\n" +
@@ -472,7 +475,7 @@
                     "\tinv.numColumns: 5\n" +
                     "\tinv.numSearchContainerColumns: 5\n" +
                     "\tminCellSize: PointF(0.0, 0.0)dp\n" +
-                    "\tcellWidthPx: 158.0px (60.190475dp)\n" +
+                    "\tcellWidthPx: 152.0px (57.904762dp)\n" +
                     "\tcellHeightPx: 166.0px (63.238094dp)\n" +
                     "\tgetCellSize().x: 393.0px (149.71428dp)\n" +
                     "\tgetCellSize().y: 180.0px (68.57143dp)\n" +
@@ -491,8 +494,9 @@
                     "\tfolderCellHeightPx: 192.0px (73.14286dp)\n" +
                     "\tfolderChildIconSizePx: 123.0px (46.857143dp)\n" +
                     "\tfolderChildTextSizePx: 32.0px (12.190476dp)\n" +
-                    "\tfolderChildDrawablePaddingPx: 8.0px (3.047619dp)\n" +
-                    "\tfolderCellLayoutBorderSpacePx: 0.0px (0.0dp)\n" +
+                    "\tfolderChildDrawablePaddingPx: 3.0px (1.1428572dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.x: 0.0px (0.0dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.y: 0.0px (0.0dp)\n" +
                     "\tfolderContentPaddingLeftRight: 21.0px (8.0dp)\n" +
                     "\tfolderTopPadding: 53.0px (20.190475dp)\n" +
                     "\tfolderFooterHeight: 123.0px (46.857143dp)\n" +
@@ -621,15 +625,16 @@
                     "\tcellLayoutPaddingPx.bottom: 59.0px (29.5dp)\n" +
                     "\ticonSizePx: 120.0px (60.0dp)\n" +
                     "\ticonTextSizePx: 28.0px (14.0dp)\n" +
-                    "\ticonDrawablePaddingPx: 14.0px (7.0dp)\n" +
+                    "\ticonDrawablePaddingPx: 9.0px (4.5dp)\n" +
                     "\tinv.numFolderRows: 3\n" +
                     "\tinv.numFolderColumns: 3\n" +
                     "\tfolderCellWidthPx: 240.0px (120.0dp)\n" +
                     "\tfolderCellHeightPx: 208.0px (104.0dp)\n" +
                     "\tfolderChildIconSizePx: 120.0px (60.0dp)\n" +
                     "\tfolderChildTextSizePx: 28.0px (14.0dp)\n" +
-                    "\tfolderChildDrawablePaddingPx: 16.0px (8.0dp)\n" +
-                    "\tfolderCellLayoutBorderSpacePx: 0.0px (0.0dp)\n" +
+                    "\tfolderChildDrawablePaddingPx: 11.0px (5.5dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.x: 0.0px (0.0dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.y: 0.0px (0.0dp)\n" +
                     "\tfolderContentPaddingLeftRight: 0.0px (0.0dp)\n" +
                     "\tfolderTopPadding: 48.0px (24.0dp)\n" +
                     "\tfolderFooterHeight: 112.0px (56.0dp)\n" +
@@ -644,7 +649,7 @@
                     "\tallAppsCloseDuration: 500\n" +
                     "\tallAppsIconSizePx: 120.0px (60.0dp)\n" +
                     "\tallAppsIconTextSizePx: 28.0px (14.0dp)\n" +
-                    "\tallAppsIconDrawablePaddingPx: 14.0px (7.0dp)\n" +
+                    "\tallAppsIconDrawablePaddingPx: 9.0px (4.5dp)\n" +
                     "\tallAppsCellHeightPx: 284.0px (142.0dp)\n" +
                     "\tallAppsCellWidthPx: 252.0px (126.0dp)\n" +
                     "\tallAppsBorderSpacePxX: 32.0px (16.0dp)\n" +
@@ -758,15 +763,16 @@
                     "\tcellLayoutPaddingPx.bottom: 59.0px (29.5dp)\n" +
                     "\ticonSizePx: 120.0px (60.0dp)\n" +
                     "\ticonTextSizePx: 28.0px (14.0dp)\n" +
-                    "\ticonDrawablePaddingPx: 14.0px (7.0dp)\n" +
+                    "\ticonDrawablePaddingPx: 9.0px (4.5dp)\n" +
                     "\tinv.numFolderRows: 3\n" +
                     "\tinv.numFolderColumns: 3\n" +
                     "\tfolderCellWidthPx: 240.0px (120.0dp)\n" +
                     "\tfolderCellHeightPx: 208.0px (104.0dp)\n" +
                     "\tfolderChildIconSizePx: 120.0px (60.0dp)\n" +
                     "\tfolderChildTextSizePx: 28.0px (14.0dp)\n" +
-                    "\tfolderChildDrawablePaddingPx: 16.0px (8.0dp)\n" +
-                    "\tfolderCellLayoutBorderSpacePx: 0.0px (0.0dp)\n" +
+                    "\tfolderChildDrawablePaddingPx: 11.0px (5.5dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.x: 0.0px (0.0dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.y: 0.0px (0.0dp)\n" +
                     "\tfolderContentPaddingLeftRight: 0.0px (0.0dp)\n" +
                     "\tfolderTopPadding: 48.0px (24.0dp)\n" +
                     "\tfolderFooterHeight: 112.0px (56.0dp)\n" +
@@ -781,7 +787,7 @@
                     "\tallAppsCloseDuration: 500\n" +
                     "\tallAppsIconSizePx: 120.0px (60.0dp)\n" +
                     "\tallAppsIconTextSizePx: 28.0px (14.0dp)\n" +
-                    "\tallAppsIconDrawablePaddingPx: 14.0px (7.0dp)\n" +
+                    "\tallAppsIconDrawablePaddingPx: 9.0px (4.5dp)\n" +
                     "\tallAppsCellHeightPx: 284.0px (142.0dp)\n" +
                     "\tallAppsCellWidthPx: 252.0px (126.0dp)\n" +
                     "\tallAppsBorderSpacePxX: 32.0px (16.0dp)\n" +
@@ -895,15 +901,16 @@
                     "\tcellLayoutPaddingPx.bottom: 72.0px (36.0dp)\n" +
                     "\ticonSizePx: 120.0px (60.0dp)\n" +
                     "\ticonTextSizePx: 28.0px (14.0dp)\n" +
-                    "\ticonDrawablePaddingPx: 14.0px (7.0dp)\n" +
+                    "\ticonDrawablePaddingPx: 9.0px (4.5dp)\n" +
                     "\tinv.numFolderRows: 3\n" +
                     "\tinv.numFolderColumns: 3\n" +
                     "\tfolderCellWidthPx: 204.0px (102.0dp)\n" +
                     "\tfolderCellHeightPx: 240.0px (120.0dp)\n" +
                     "\tfolderChildIconSizePx: 120.0px (60.0dp)\n" +
                     "\tfolderChildTextSizePx: 28.0px (14.0dp)\n" +
-                    "\tfolderChildDrawablePaddingPx: 27.0px (13.5dp)\n" +
-                    "\tfolderCellLayoutBorderSpacePx: 0.0px (0.0dp)\n" +
+                    "\tfolderChildDrawablePaddingPx: 22.0px (11.0dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.x: 0.0px (0.0dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.y: 0.0px (0.0dp)\n" +
                     "\tfolderContentPaddingLeftRight: 0.0px (0.0dp)\n" +
                     "\tfolderTopPadding: 48.0px (24.0dp)\n" +
                     "\tfolderFooterHeight: 112.0px (56.0dp)\n" +
@@ -918,7 +925,7 @@
                     "\tallAppsCloseDuration: 500\n" +
                     "\tallAppsIconSizePx: 120.0px (60.0dp)\n" +
                     "\tallAppsIconTextSizePx: 28.0px (14.0dp)\n" +
-                    "\tallAppsIconDrawablePaddingPx: 14.0px (7.0dp)\n" +
+                    "\tallAppsIconDrawablePaddingPx: 9.0px (4.5dp)\n" +
                     "\tallAppsCellHeightPx: 316.0px (158.0dp)\n" +
                     "\tallAppsCellWidthPx: 192.0px (96.0dp)\n" +
                     "\tallAppsBorderSpacePxX: 16.0px (8.0dp)\n" +
@@ -1032,15 +1039,16 @@
                     "\tcellLayoutPaddingPx.bottom: 72.0px (36.0dp)\n" +
                     "\ticonSizePx: 120.0px (60.0dp)\n" +
                     "\ticonTextSizePx: 28.0px (14.0dp)\n" +
-                    "\ticonDrawablePaddingPx: 14.0px (7.0dp)\n" +
+                    "\ticonDrawablePaddingPx: 9.0px (4.5dp)\n" +
                     "\tinv.numFolderRows: 3\n" +
                     "\tinv.numFolderColumns: 3\n" +
                     "\tfolderCellWidthPx: 204.0px (102.0dp)\n" +
                     "\tfolderCellHeightPx: 240.0px (120.0dp)\n" +
                     "\tfolderChildIconSizePx: 120.0px (60.0dp)\n" +
                     "\tfolderChildTextSizePx: 28.0px (14.0dp)\n" +
-                    "\tfolderChildDrawablePaddingPx: 27.0px (13.5dp)\n" +
-                    "\tfolderCellLayoutBorderSpacePx: 0.0px (0.0dp)\n" +
+                    "\tfolderChildDrawablePaddingPx: 22.0px (11.0dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.x: 0.0px (0.0dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.y: 0.0px (0.0dp)\n" +
                     "\tfolderContentPaddingLeftRight: 0.0px (0.0dp)\n" +
                     "\tfolderTopPadding: 48.0px (24.0dp)\n" +
                     "\tfolderFooterHeight: 112.0px (56.0dp)\n" +
@@ -1055,7 +1063,7 @@
                     "\tallAppsCloseDuration: 500\n" +
                     "\tallAppsIconSizePx: 120.0px (60.0dp)\n" +
                     "\tallAppsIconTextSizePx: 28.0px (14.0dp)\n" +
-                    "\tallAppsIconDrawablePaddingPx: 14.0px (7.0dp)\n" +
+                    "\tallAppsIconDrawablePaddingPx: 9.0px (4.5dp)\n" +
                     "\tallAppsCellHeightPx: 316.0px (158.0dp)\n" +
                     "\tallAppsCellWidthPx: 192.0px (96.0dp)\n" +
                     "\tallAppsBorderSpacePxX: 16.0px (8.0dp)\n" +
@@ -1162,8 +1170,8 @@
                     "\tinv.numColumns: 4\n" +
                     "\tinv.numSearchContainerColumns: 4\n" +
                     "\tminCellSize: PointF(0.0, 0.0)dp\n" +
-                    "\tcellWidthPx: 159.0px (60.57143dp)\n" +
-                    "\tcellHeightPx: 223.0px (84.95238dp)\n" +
+                    "\tcellWidthPx: 154.0px (58.666668dp)\n" +
+                    "\tcellHeightPx: 218.0px (83.04762dp)\n" +
                     "\tgetCellSize().x: 270.0px (102.85714dp)\n" +
                     "\tgetCellSize().y: 342.0px (130.28572dp)\n" +
                     "\tcellLayoutBorderSpacePx Horizontal: 0.0px (0.0dp)\n" +
@@ -1174,15 +1182,16 @@
                     "\tcellLayoutPaddingPx.bottom: 0.0px (0.0dp)\n" +
                     "\ticonSizePx: 141.0px (53.714287dp)\n" +
                     "\ticonTextSizePx: 34.0px (12.952381dp)\n" +
-                    "\ticonDrawablePaddingPx: 18.0px (6.857143dp)\n" +
+                    "\ticonDrawablePaddingPx: 13.0px (4.952381dp)\n" +
                     "\tinv.numFolderRows: 3\n" +
                     "\tinv.numFolderColumns: 4\n" +
                     "\tfolderCellWidthPx: 189.0px (72.0dp)\n" +
                     "\tfolderCellHeightPx: 219.0px (83.42857dp)\n" +
                     "\tfolderChildIconSizePx: 141.0px (53.714287dp)\n" +
                     "\tfolderChildTextSizePx: 34.0px (12.952381dp)\n" +
-                    "\tfolderChildDrawablePaddingPx: 10.0px (3.8095238dp)\n" +
-                    "\tfolderCellLayoutBorderSpacePx: 0.0px (0.0dp)\n" +
+                    "\tfolderChildDrawablePaddingPx: 5.0px (1.9047619dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.x: 0.0px (0.0dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.y: 0.0px (0.0dp)\n" +
                     "\tfolderContentPaddingLeftRight: 21.0px (8.0dp)\n" +
                     "\tfolderTopPadding: 63.0px (24.0dp)\n" +
                     "\tfolderFooterHeight: 147.0px (56.0dp)\n" +
@@ -1236,8 +1245,8 @@
                     "\tworkspacePadding.bottom: 330.0px (125.71429dp)\n" +
                     "\ticonScale: 1.0px (0.3809524dp)\n" +
                     "\tcellScaleToFit : 1.0px (0.3809524dp)\n" +
-                    "\textraSpace: 478.0px (182.09525dp)\n" +
-                    "\tunscaled extraSpace: 478.0px (182.09525dp)\n" +
+                    "\textraSpace: 498.0px (189.71428dp)\n" +
+                    "\tunscaled extraSpace: 498.0px (189.71428dp)\n" +
                     "\tmaxEmptySpace: 0.0px (0.0dp)\n" +
                     "\tworkspaceTopPadding: 0.0px (0.0dp)\n" +
                     "\tworkspaceBottomPadding: 0.0px (0.0dp)\n" +
@@ -1303,8 +1312,8 @@
                     "\tinv.numColumns: 4\n" +
                     "\tinv.numSearchContainerColumns: 4\n" +
                     "\tminCellSize: PointF(0.0, 0.0)dp\n" +
-                    "\tcellWidthPx: 159.0px (60.57143dp)\n" +
-                    "\tcellHeightPx: 223.0px (84.95238dp)\n" +
+                    "\tcellWidthPx: 154.0px (58.666668dp)\n" +
+                    "\tcellHeightPx: 218.0px (83.04762dp)\n" +
                     "\tgetCellSize().x: 270.0px (102.85714dp)\n" +
                     "\tgetCellSize().y: 342.0px (130.28572dp)\n" +
                     "\tcellLayoutBorderSpacePx Horizontal: 0.0px (0.0dp)\n" +
@@ -1315,15 +1324,16 @@
                     "\tcellLayoutPaddingPx.bottom: 0.0px (0.0dp)\n" +
                     "\ticonSizePx: 141.0px (53.714287dp)\n" +
                     "\ticonTextSizePx: 34.0px (12.952381dp)\n" +
-                    "\ticonDrawablePaddingPx: 18.0px (6.857143dp)\n" +
+                    "\ticonDrawablePaddingPx: 13.0px (4.952381dp)\n" +
                     "\tinv.numFolderRows: 3\n" +
                     "\tinv.numFolderColumns: 4\n" +
                     "\tfolderCellWidthPx: 189.0px (72.0dp)\n" +
                     "\tfolderCellHeightPx: 219.0px (83.42857dp)\n" +
                     "\tfolderChildIconSizePx: 141.0px (53.714287dp)\n" +
                     "\tfolderChildTextSizePx: 34.0px (12.952381dp)\n" +
-                    "\tfolderChildDrawablePaddingPx: 10.0px (3.8095238dp)\n" +
-                    "\tfolderCellLayoutBorderSpacePx: 0.0px (0.0dp)\n" +
+                    "\tfolderChildDrawablePaddingPx: 5.0px (1.9047619dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.x: 0.0px (0.0dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.y: 0.0px (0.0dp)\n" +
                     "\tfolderContentPaddingLeftRight: 21.0px (8.0dp)\n" +
                     "\tfolderTopPadding: 63.0px (24.0dp)\n" +
                     "\tfolderFooterHeight: 147.0px (56.0dp)\n" +
@@ -1377,8 +1387,8 @@
                     "\tworkspacePadding.bottom: 330.0px (125.71429dp)\n" +
                     "\ticonScale: 1.0px (0.3809524dp)\n" +
                     "\tcellScaleToFit : 1.0px (0.3809524dp)\n" +
-                    "\textraSpace: 478.0px (182.09525dp)\n" +
-                    "\tunscaled extraSpace: 478.0px (182.09525dp)\n" +
+                    "\textraSpace: 498.0px (189.71428dp)\n" +
+                    "\tunscaled extraSpace: 498.0px (189.71428dp)\n" +
                     "\tmaxEmptySpace: 0.0px (0.0dp)\n" +
                     "\tworkspaceTopPadding: 0.0px (0.0dp)\n" +
                     "\tworkspaceBottomPadding: 0.0px (0.0dp)\n" +
@@ -1444,8 +1454,8 @@
                     "\tinv.numColumns: 4\n" +
                     "\tinv.numSearchContainerColumns: 4\n" +
                     "\tminCellSize: PointF(0.0, 0.0)dp\n" +
-                    "\tcellWidthPx: 159.0px (60.57143dp)\n" +
-                    "\tcellHeightPx: 223.0px (84.95238dp)\n" +
+                    "\tcellWidthPx: 154.0px (58.666668dp)\n" +
+                    "\tcellHeightPx: 218.0px (83.04762dp)\n" +
                     "\tgetCellSize().x: 224.0px (85.333336dp)\n" +
                     "\tgetCellSize().y: 430.0px (163.80952dp)\n" +
                     "\tcellLayoutBorderSpacePx Horizontal: 0.0px (0.0dp)\n" +
@@ -1456,15 +1466,16 @@
                     "\tcellLayoutPaddingPx.bottom: 0.0px (0.0dp)\n" +
                     "\ticonSizePx: 141.0px (53.714287dp)\n" +
                     "\ticonTextSizePx: 34.0px (12.952381dp)\n" +
-                    "\ticonDrawablePaddingPx: 18.0px (6.857143dp)\n" +
+                    "\ticonDrawablePaddingPx: 13.0px (4.952381dp)\n" +
                     "\tinv.numFolderRows: 3\n" +
                     "\tinv.numFolderColumns: 4\n" +
                     "\tfolderCellWidthPx: 189.0px (72.0dp)\n" +
                     "\tfolderCellHeightPx: 219.0px (83.42857dp)\n" +
                     "\tfolderChildIconSizePx: 141.0px (53.714287dp)\n" +
                     "\tfolderChildTextSizePx: 34.0px (12.952381dp)\n" +
-                    "\tfolderChildDrawablePaddingPx: 10.0px (3.8095238dp)\n" +
-                    "\tfolderCellLayoutBorderSpacePx: 0.0px (0.0dp)\n" +
+                    "\tfolderChildDrawablePaddingPx: 5.0px (1.9047619dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.x: 0.0px (0.0dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.y: 0.0px (0.0dp)\n" +
                     "\tfolderContentPaddingLeftRight: 21.0px (8.0dp)\n" +
                     "\tfolderTopPadding: 63.0px (24.0dp)\n" +
                     "\tfolderFooterHeight: 147.0px (56.0dp)\n" +
@@ -1518,8 +1529,8 @@
                     "\tworkspacePadding.bottom: 330.0px (125.71429dp)\n" +
                     "\ticonScale: 1.0px (0.3809524dp)\n" +
                     "\tcellScaleToFit : 1.0px (0.3809524dp)\n" +
-                    "\textraSpace: 829.0px (315.8095dp)\n" +
-                    "\tunscaled extraSpace: 829.0px (315.8095dp)\n" +
+                    "\textraSpace: 849.0px (323.42856dp)\n" +
+                    "\tunscaled extraSpace: 849.0px (323.42856dp)\n" +
                     "\tmaxEmptySpace: 0.0px (0.0dp)\n" +
                     "\tworkspaceTopPadding: 0.0px (0.0dp)\n" +
                     "\tworkspaceBottomPadding: 0.0px (0.0dp)\n" +
@@ -1581,8 +1592,8 @@
                     "\tinv.numColumns: 4\n" +
                     "\tinv.numSearchContainerColumns: 4\n" +
                     "\tminCellSize: PointF(0.0, 0.0)dp\n" +
-                    "\tcellWidthPx: 159.0px (60.57143dp)\n" +
-                    "\tcellHeightPx: 223.0px (84.95238dp)\n" +
+                    "\tcellWidthPx: 154.0px (58.666668dp)\n" +
+                    "\tcellHeightPx: 218.0px (83.04762dp)\n" +
                     "\tgetCellSize().x: 224.0px (85.333336dp)\n" +
                     "\tgetCellSize().y: 430.0px (163.80952dp)\n" +
                     "\tcellLayoutBorderSpacePx Horizontal: 0.0px (0.0dp)\n" +
@@ -1593,15 +1604,16 @@
                     "\tcellLayoutPaddingPx.bottom: 0.0px (0.0dp)\n" +
                     "\ticonSizePx: 141.0px (53.714287dp)\n" +
                     "\ticonTextSizePx: 34.0px (12.952381dp)\n" +
-                    "\ticonDrawablePaddingPx: 18.0px (6.857143dp)\n" +
+                    "\ticonDrawablePaddingPx: 13.0px (4.952381dp)\n" +
                     "\tinv.numFolderRows: 3\n" +
                     "\tinv.numFolderColumns: 4\n" +
                     "\tfolderCellWidthPx: 189.0px (72.0dp)\n" +
                     "\tfolderCellHeightPx: 219.0px (83.42857dp)\n" +
                     "\tfolderChildIconSizePx: 141.0px (53.714287dp)\n" +
                     "\tfolderChildTextSizePx: 34.0px (12.952381dp)\n" +
-                    "\tfolderChildDrawablePaddingPx: 10.0px (3.8095238dp)\n" +
-                    "\tfolderCellLayoutBorderSpacePx: 0.0px (0.0dp)\n" +
+                    "\tfolderChildDrawablePaddingPx: 5.0px (1.9047619dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.x: 0.0px (0.0dp)\n" +
+                    "\tfolderCellLayoutBorderSpacePx.y: 0.0px (0.0dp)\n" +
                     "\tfolderContentPaddingLeftRight: 21.0px (8.0dp)\n" +
                     "\tfolderTopPadding: 63.0px (24.0dp)\n" +
                     "\tfolderFooterHeight: 147.0px (56.0dp)\n" +
@@ -1655,8 +1667,8 @@
                     "\tworkspacePadding.bottom: 330.0px (125.71429dp)\n" +
                     "\ticonScale: 1.0px (0.3809524dp)\n" +
                     "\tcellScaleToFit : 1.0px (0.3809524dp)\n" +
-                    "\textraSpace: 829.0px (315.8095dp)\n" +
-                    "\tunscaled extraSpace: 829.0px (315.8095dp)\n" +
+                    "\textraSpace: 849.0px (323.42856dp)\n" +
+                    "\tunscaled extraSpace: 849.0px (323.42856dp)\n" +
                     "\tmaxEmptySpace: 0.0px (0.0dp)\n" +
                     "\tworkspaceTopPadding: 0.0px (0.0dp)\n" +
                     "\tworkspaceBottomPadding: 0.0px (0.0dp)\n" +
diff --git a/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt b/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt
index 2a27487..d102397 100644
--- a/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt
+++ b/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt
@@ -158,4 +158,25 @@
         assertThat(dp.isQsbInline).isFalse()
         assertThat(dp.hotseatQsbWidth).isEqualTo(1095)
     }
+
+    @Test
+    fun border_space_should_be_zero_when_numHotseatIcons_is_smallerOrEqual_1() {
+        initializeVarsForTablet(isGestureMode = false)
+        windowBounds = WindowBounds(Rect(0, 0, 1800, 2560), Rect(0, 104, 0, 0))
+
+        val numShownHotseatIcons = listOf(-1, 0, 1)
+        for (numHotseatIcons in numShownHotseatIcons) {
+            inv?.numShownHotseatIcons = numHotseatIcons
+
+            val dp = newDP()
+            dp.isTaskbarPresentInApps = true
+
+            assertThat(dp.numShownHotseatIcons).isEqualTo(numHotseatIcons)
+            assertThat(dp.hotseatBorderSpace).isEqualTo(0)
+
+            assertThat(dp.getHotseatLayoutPadding(context).left).isEqualTo(177)
+            assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(177)
+            assertThat(dp.hotseatQsbWidth).isEqualTo(1445)
+        }
+    }
 }
diff --git a/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt b/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
new file mode 100644
index 0000000..cd95e99
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.AbstractDeviceProfileTest
+import com.android.launcher3.tests.R as TestR
+import com.android.launcher3.util.TestResourceHelper
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AllAppsSpecsTest : AbstractDeviceProfileTest() {
+    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+    @Before
+    fun setup() {
+        initializeVarsForPhone(deviceSpecs["phone"]!!)
+    }
+
+    @Test
+    fun parseValidFile() {
+        val allAppsSpecs =
+            AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_all_apps_file))
+        assertThat(allAppsSpecs.heightSpecs.size).isEqualTo(1)
+        assertThat(allAppsSpecs.heightSpecs[0].toString())
+            .isEqualTo(
+                "AllAppsSpec(" +
+                    "maxAvailableSize=26247, " +
+                    "specType=HEIGHT, " +
+                    "startPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "endPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "gutter=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=true, " +
+                    "maxSize=2147483647), " +
+                    "cellSize=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=true, " +
+                    "maxSize=2147483647)" +
+                    ")"
+            )
+
+        assertThat(allAppsSpecs.widthSpecs.size).isEqualTo(1)
+        assertThat(allAppsSpecs.widthSpecs[0].toString())
+            .isEqualTo(
+                "AllAppsSpec(" +
+                    "maxAvailableSize=26247, " +
+                    "specType=WIDTH, " +
+                    "startPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=true, " +
+                    "maxSize=2147483647), " +
+                    "endPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=true, " +
+                    "maxSize=2147483647), " +
+                    "gutter=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=true, " +
+                    "maxSize=2147483647), " +
+                    "cellSize=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=true, " +
+                    "maxSize=2147483647)" +
+                    ")"
+            )
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_missingTag_throwsError() {
+        AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_1))
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
+        AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_2))
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_valueBiggerThan1_throwsError() {
+        AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_3))
+    }
+}
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
new file mode 100644
index 0000000..0f12e58
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.AbstractDeviceProfileTest
+import com.android.launcher3.tests.R as TestR
+import com.android.launcher3.util.TestResourceHelper
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CalculatedAllAppsSpecTest : AbstractDeviceProfileTest() {
+    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+    /**
+     * This test tests:
+     * - (height spec) copy values from workspace
+     * - (width spec) copy values from workspace
+     */
+    @Test
+    fun normalPhone_copiesFromWorkspace() {
+        val deviceSpec = deviceSpecs["phone"]!!
+        initializeVarsForPhone(deviceSpec)
+
+        val availableWidth = deviceSpec.naturalSize.first
+        // Hotseat size is roughly 495px on a real device,
+        // it doesn't need to be precise on unit tests
+        val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 495
+
+        val workspaceSpecs =
+            WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+        val widthSpec = workspaceSpecs.getCalculatedWidthSpec(4, availableWidth)
+        val heightSpec = workspaceSpecs.getCalculatedHeightSpec(5, availableHeight)
+
+        val allAppsSpecs =
+            AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_all_apps_file))
+
+        with(allAppsSpecs.getCalculatedWidthSpec(4, availableWidth, widthSpec)) {
+            assertThat(availableSpace).isEqualTo(availableWidth)
+            assertThat(cells).isEqualTo(4)
+            assertThat(startPaddingPx).isEqualTo(widthSpec.startPaddingPx)
+            assertThat(endPaddingPx).isEqualTo(widthSpec.endPaddingPx)
+            assertThat(gutterPx).isEqualTo(widthSpec.gutterPx)
+            assertThat(cellSizePx).isEqualTo(widthSpec.cellSizePx)
+        }
+
+        with(allAppsSpecs.getCalculatedHeightSpec(5, availableHeight, heightSpec)) {
+            assertThat(availableSpace).isEqualTo(availableHeight)
+            assertThat(cells).isEqualTo(5)
+            assertThat(startPaddingPx).isEqualTo(0)
+            assertThat(endPaddingPx).isEqualTo(0)
+            assertThat(gutterPx).isEqualTo(heightSpec.gutterPx)
+            assertThat(cellSizePx).isEqualTo(heightSpec.cellSizePx)
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt
new file mode 100644
index 0000000..f2a269a
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.AbstractDeviceProfileTest
+import com.android.launcher3.tests.R
+import com.android.launcher3.util.TestResourceHelper
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CalculatedFolderSpecsTest : AbstractDeviceProfileTest() {
+    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+    private val deviceSpec = deviceSpecs["phone"]!!
+
+    @Before
+    fun setup() {
+        initializeVarsForPhone(deviceSpec)
+    }
+
+    @Test
+    fun validate_matchWidthWorkspace() {
+        val columns = 6
+
+        // Loading workspace specs
+        val resourceHelperWorkspace = TestResourceHelper(context!!, R.xml.valid_workspace_file)
+        val workspaceSpecs = WorkspaceSpecs.create(resourceHelperWorkspace)
+
+        // Loading folders specs
+        val resourceHelperFolder = TestResourceHelper(context!!, R.xml.valid_folders_specs)
+        val folderSpecs = FolderSpecs.create(resourceHelperFolder)
+
+        assertThat(folderSpecs.widthSpecs.size).isEqualTo(2)
+        assertThat(folderSpecs.widthSpecs[0].cellSize.matchWorkspace).isEqualTo(true)
+        assertThat(folderSpecs.widthSpecs[1].cellSize.matchWorkspace).isEqualTo(false)
+
+        // Validate width spec <= 800
+        var availableWidth = deviceSpec.naturalSize.first
+        var calculatedWorkspace = workspaceSpecs.getCalculatedWidthSpec(columns, availableWidth)
+        var calculatedWidthFolderSpec =
+            folderSpecs.getCalculatedWidthSpec(columns, availableWidth, calculatedWorkspace)
+        with(calculatedWidthFolderSpec) {
+            assertThat(availableSpace).isEqualTo(availableWidth)
+            assertThat(cells).isEqualTo(columns)
+            assertThat(startPaddingPx).isEqualTo(16.dpToPx())
+            assertThat(endPaddingPx).isEqualTo(16.dpToPx())
+            assertThat(gutterPx).isEqualTo(16.dpToPx())
+            assertThat(cellSizePx).isEqualTo(calculatedWorkspace.cellSizePx)
+        }
+
+        // Validate width spec > 800
+        availableWidth = 2000.dpToPx()
+        calculatedWorkspace = workspaceSpecs.getCalculatedWidthSpec(columns, availableWidth)
+        calculatedWidthFolderSpec =
+            folderSpecs.getCalculatedWidthSpec(columns, availableWidth, calculatedWorkspace)
+        with(calculatedWidthFolderSpec) {
+            assertThat(availableSpace).isEqualTo(availableWidth)
+            assertThat(cells).isEqualTo(columns)
+            assertThat(startPaddingPx).isEqualTo(16.dpToPx())
+            assertThat(endPaddingPx).isEqualTo(16.dpToPx())
+            assertThat(gutterPx).isEqualTo(16.dpToPx())
+            assertThat(cellSizePx).isEqualTo(102.dpToPx())
+        }
+    }
+
+    @Test
+    fun validate_matchHeightWorkspace() {
+        // Hotseat is roughly 495px on a real device, it doesn't need to be precise on unit tests
+        val hotseatSize = 495
+        val statusBarHeight = deviceSpec.statusBarNaturalPx
+        val availableHeight = deviceSpec.naturalSize.second - statusBarHeight - hotseatSize
+        val rows = 5
+
+        // Loading workspace specs
+        val resourceHelperWorkspace = TestResourceHelper(context!!, R.xml.valid_workspace_file)
+        val workspaceSpecs = WorkspaceSpecs.create(resourceHelperWorkspace)
+
+        // Loading folders specs
+        val resourceHelperFolder = TestResourceHelper(context!!, R.xml.valid_folders_specs)
+        val folderSpecs = FolderSpecs.create(resourceHelperFolder)
+
+        assertThat(folderSpecs.heightSpecs.size).isEqualTo(1)
+        assertThat(folderSpecs.heightSpecs[0].cellSize.matchWorkspace).isEqualTo(true)
+
+        // Validate height spec
+        val calculatedWorkspace = workspaceSpecs.getCalculatedHeightSpec(rows, availableHeight)
+        val calculatedFolderSpec =
+            folderSpecs.getCalculatedHeightSpec(rows, availableHeight, calculatedWorkspace)
+        with(calculatedFolderSpec) {
+            assertThat(availableSpace).isEqualTo(availableHeight)
+            assertThat(cells).isEqualTo(rows)
+            assertThat(startPaddingPx).isEqualTo(24.dpToPx())
+            assertThat(endPaddingPx).isEqualTo(64.dpToPx())
+            assertThat(gutterPx).isEqualTo(16.dpToPx())
+            assertThat(cellSizePx).isEqualTo(calculatedWorkspace.cellSizePx)
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt
new file mode 100644
index 0000000..0ecf7ba
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.AbstractDeviceProfileTest
+import com.android.launcher3.tests.R as TestR
+import com.android.launcher3.util.TestResourceHelper
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CalculatedHotseatSpecTest : AbstractDeviceProfileTest() {
+    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+    /**
+     * This test tests:
+     * - (height spec) gets the correct breakpoint from the XML - skips the first breakpoint
+     */
+    @Test
+    fun normalPhone_returnsSecondBreakpointSpec() {
+        val deviceSpec = deviceSpecs["phone"]!!
+        initializeVarsForPhone(deviceSpec)
+
+        // Hotseat uses the whole device height
+        val availableHeight = deviceSpec.naturalSize.second
+
+        val hotseatSpecs =
+            HotseatSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_hotseat_file))
+        val heightSpec = hotseatSpecs.getCalculatedHeightSpec(availableHeight)
+
+        assertThat(heightSpec.availableSpace).isEqualTo(availableHeight)
+        assertThat(heightSpec.hotseatQsbSpace).isEqualTo(95)
+    }
+
+    /**
+     * This test tests:
+     * - (height spec) gets the correct breakpoint from the XML - use the first breakpoint
+     */
+    @Test
+    fun smallPhone_returnsFirstBreakpointSpec() {
+        val deviceSpec = deviceSpecs["phone"]!!
+        deviceSpec.densityDpi = 540 // larger display size
+        initializeVarsForPhone(deviceSpec)
+
+        // Hotseat uses the whole device height
+        val availableHeight = deviceSpec.naturalSize.second
+
+        val hotseatSpecs =
+            HotseatSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_hotseat_file))
+        val heightSpec = hotseatSpecs.getCalculatedHeightSpec(availableHeight)
+
+        assertThat(heightSpec.availableSpace).isEqualTo(availableHeight)
+        assertThat(heightSpec.hotseatQsbSpace).isEqualTo(81)
+    }
+}
diff --git a/tests/src/com/android/launcher3/workspace/CalculatedWorkspaceSpecTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
similarity index 94%
rename from tests/src/com/android/launcher3/workspace/CalculatedWorkspaceSpecTest.kt
rename to tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
index 7f03ba2..0af694e 100644
--- a/tests/src/com/android/launcher3/workspace/CalculatedWorkspaceSpecTest.kt
+++ b/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.workspace
+package com.android.launcher3.responsive
 
 import android.content.Context
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -49,7 +49,7 @@
         val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 495
 
         val workspaceSpecs =
-            WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+            WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
         val widthSpec = workspaceSpecs.getCalculatedWidthSpec(4, availableWidth)
         val heightSpec = workspaceSpecs.getCalculatedHeightSpec(5, availableHeight)
 
@@ -86,7 +86,7 @@
         val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 640
 
         val workspaceSpecs =
-            WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+            WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
         val widthSpec = workspaceSpecs.getCalculatedWidthSpec(4, availableWidth)
         val heightSpec = workspaceSpecs.getCalculatedHeightSpec(5, availableHeight)
 
diff --git a/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt b/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt
new file mode 100644
index 0000000..4b05949
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.AbstractDeviceProfileTest
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
+import com.android.launcher3.tests.R
+import com.android.launcher3.util.TestResourceHelper
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderSpecsTest : AbstractDeviceProfileTest() {
+    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+    @Before
+    fun setup() {
+        initializeVarsForPhone(deviceSpecs["tablet"]!!)
+    }
+
+    @Test
+    fun parseValidFile() {
+        val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+
+        val sizeSpec16 = SizeSpec(16f.dpToPx())
+        val widthSpecsExpected =
+            listOf(
+                FolderSpec(
+                    maxAvailableSize = 800.dpToPx(),
+                    specType = SpecType.WIDTH,
+                    startPadding = sizeSpec16,
+                    endPadding = sizeSpec16,
+                    gutter = sizeSpec16,
+                    cellSize = SizeSpec(matchWorkspace = true)
+                ),
+                FolderSpec(
+                    maxAvailableSize = 9999.dpToPx(),
+                    specType = SpecType.WIDTH,
+                    startPadding = sizeSpec16,
+                    endPadding = sizeSpec16,
+                    gutter = sizeSpec16,
+                    cellSize = SizeSpec(102f.dpToPx())
+                )
+            )
+
+        val heightSpecsExpected =
+            FolderSpec(
+                maxAvailableSize = 9999.dpToPx(),
+                specType = SpecType.HEIGHT,
+                startPadding = SizeSpec(24f.dpToPx()),
+                endPadding = SizeSpec(64f.dpToPx()),
+                gutter = sizeSpec16,
+                cellSize = SizeSpec(matchWorkspace = true)
+            )
+
+        assertThat(folderSpecs.widthSpecs.size).isEqualTo(widthSpecsExpected.size)
+        assertThat(folderSpecs.widthSpecs[0]).isEqualTo(widthSpecsExpected[0])
+        assertThat(folderSpecs.widthSpecs[1]).isEqualTo(widthSpecsExpected[1])
+
+        assertThat(folderSpecs.heightSpecs.size).isEqualTo(1)
+        assertThat(folderSpecs.heightSpecs[0]).isEqualTo(heightSpecsExpected)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_missingTag_throwsError() {
+        val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_1)
+        FolderSpecs.create(resourceHelper)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
+        val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_2)
+        FolderSpecs.create(resourceHelper)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_valueBiggerThan1_throwsError() {
+        val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_3)
+        FolderSpecs.create(resourceHelper)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_missingSpecs_throwsError() {
+        val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_4)
+        FolderSpecs.create(resourceHelper)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_missingWidthBreakpoint_throwsError() {
+        val availableSpace = 900.dpToPx()
+        val cells = 3
+
+        val workspaceSpec =
+            WorkspaceSpec(
+                maxAvailableSize = availableSpace,
+                specType = SpecType.WIDTH,
+                startPadding = SizeSpec(fixedSize = 10f),
+                endPadding = SizeSpec(fixedSize = 10f),
+                gutter = SizeSpec(fixedSize = 10f),
+                cellSize = SizeSpec(fixedSize = 10f)
+            )
+        val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
+
+        val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_5)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+        folderSpecs.getCalculatedWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_missingHeightBreakpoint_throwsError() {
+        val availableSpace = 900.dpToPx()
+        val cells = 3
+
+        val workspaceSpec =
+            WorkspaceSpec(
+                maxAvailableSize = availableSpace,
+                specType = SpecType.HEIGHT,
+                startPadding = SizeSpec(fixedSize = 10f),
+                endPadding = SizeSpec(fixedSize = 10f),
+                gutter = SizeSpec(fixedSize = 10f),
+                cellSize = SizeSpec(fixedSize = 10f)
+            )
+        val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
+
+        val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_5)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+        folderSpecs.getCalculatedHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
+    }
+
+    @Test
+    fun retrievesCalculatedWidthSpec() {
+        val availableSpace = 800.dpToPx()
+        val cells = 3
+
+        val workspaceSpec =
+            WorkspaceSpec(
+                maxAvailableSize = availableSpace,
+                specType = SpecType.WIDTH,
+                startPadding = SizeSpec(fixedSize = 10f),
+                endPadding = SizeSpec(fixedSize = 10f),
+                gutter = SizeSpec(fixedSize = 10f),
+                cellSize = SizeSpec(fixedSize = 10f)
+            )
+        val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
+
+        val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+        val calculatedWidthSpec =
+            folderSpecs.getCalculatedWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
+
+        assertThat(calculatedWidthSpec.cells).isEqualTo(cells)
+        assertThat(calculatedWidthSpec.availableSpace).isEqualTo(availableSpace)
+        assertThat(calculatedWidthSpec.startPaddingPx).isEqualTo(16.dpToPx())
+        assertThat(calculatedWidthSpec.endPaddingPx).isEqualTo(16.dpToPx())
+        assertThat(calculatedWidthSpec.gutterPx).isEqualTo(16.dpToPx())
+        assertThat(calculatedWidthSpec.cellSizePx).isEqualTo(calculatedWorkspaceSpec.cellSizePx)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun retrievesCalculatedWidthSpec_invalidCalculatedWorkspaceSpecType_throwsError() {
+        val availableSpace = 10.dpToPx()
+        val cells = 3
+
+        val workspaceSpec =
+            WorkspaceSpec(
+                maxAvailableSize = availableSpace,
+                specType = SpecType.HEIGHT,
+                startPadding = SizeSpec(fixedSize = 10f),
+                endPadding = SizeSpec(fixedSize = 10f),
+                gutter = SizeSpec(fixedSize = 10f),
+                cellSize = SizeSpec(fixedSize = 10f)
+            )
+        val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
+
+        val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+        folderSpecs.getCalculatedWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
+    }
+
+    @Test
+    fun retrievesCalculatedHeightSpec() {
+        val availableSpace = 700.dpToPx()
+        val cells = 3
+
+        val workspaceSpec =
+            WorkspaceSpec(
+                maxAvailableSize = availableSpace,
+                specType = SpecType.HEIGHT,
+                startPadding = SizeSpec(fixedSize = 10f),
+                endPadding = SizeSpec(fixedSize = 10f),
+                gutter = SizeSpec(fixedSize = 10f),
+                cellSize = SizeSpec(fixedSize = 10f)
+            )
+        val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
+
+        val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+        val calculatedHeightSpec =
+            folderSpecs.getCalculatedHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
+
+        assertThat(calculatedHeightSpec.cells).isEqualTo(cells)
+        assertThat(calculatedHeightSpec.availableSpace).isEqualTo(availableSpace)
+        assertThat(calculatedHeightSpec.startPaddingPx).isEqualTo(24.dpToPx())
+        assertThat(calculatedHeightSpec.endPaddingPx).isEqualTo(64.dpToPx())
+        assertThat(calculatedHeightSpec.gutterPx).isEqualTo(16.dpToPx())
+        assertThat(calculatedHeightSpec.cellSizePx).isEqualTo(calculatedWorkspaceSpec.cellSizePx)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun retrievesCalculatedHeightSpec_invalidCalculatedWorkspaceSpecType_throwsError() {
+        val availableSpace = 10.dpToPx()
+        val cells = 3
+
+        val workspaceSpec =
+            WorkspaceSpec(
+                maxAvailableSize = availableSpace,
+                specType = SpecType.WIDTH,
+                startPadding = SizeSpec(fixedSize = 10f),
+                endPadding = SizeSpec(fixedSize = 10f),
+                gutter = SizeSpec(fixedSize = 10f),
+                cellSize = SizeSpec(fixedSize = 10f)
+            )
+        val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
+
+        val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+        folderSpecs.getCalculatedHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
+    }
+}
diff --git a/tests/src/com/android/launcher3/responsive/HotseatSpecsTest.kt b/tests/src/com/android/launcher3/responsive/HotseatSpecsTest.kt
new file mode 100644
index 0000000..c764e47
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/HotseatSpecsTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.AbstractDeviceProfileTest
+import com.android.launcher3.tests.R as TestR
+import com.android.launcher3.util.TestResourceHelper
+import com.android.systemui.util.dpToPx
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HotseatSpecsTest : AbstractDeviceProfileTest() {
+    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+    @Before
+    fun setup() {
+        initializeVarsForPhone(deviceSpecs["phone"]!!)
+    }
+
+    @Test
+    fun parseValidFile() {
+        val hotseatSpecs =
+            HotseatSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_hotseat_file))
+        assertThat(hotseatSpecs.specs.size).isEqualTo(2)
+
+        val expectedSpecs =
+            listOf(
+                HotseatSpec(
+                    maxAvailableSize = 847.dpToPx(),
+                    specType = ResponsiveSpec.SpecType.HEIGHT,
+                    hotseatQsbSpace = SizeSpec(24f.dpToPx())
+                ),
+                HotseatSpec(
+                    maxAvailableSize = 9999.dpToPx(),
+                    specType = ResponsiveSpec.SpecType.HEIGHT,
+                    hotseatQsbSpace = SizeSpec(36f.dpToPx())
+                ),
+            )
+
+        assertThat(hotseatSpecs.specs.size).isEqualTo(expectedSpecs.size)
+        assertThat(hotseatSpecs.specs[0]).isEqualTo(expectedSpecs[0])
+        assertThat(hotseatSpecs.specs[1]).isEqualTo(expectedSpecs[1])
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_spaceIsNotFixedSize_throwsError() {
+        HotseatSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_hotseat_file_case_1))
+    }
+}
diff --git a/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt b/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt
new file mode 100644
index 0000000..8ca07c6
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.AbstractDeviceProfileTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SizeSpecTest : AbstractDeviceProfileTest() {
+    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+    @Before
+    fun setup() {
+        initializeVarsForPhone(deviceSpecs["phone"]!!)
+    }
+
+    @Test
+    fun valid_values() {
+        val combinations =
+            listOf(
+                SizeSpec(100f, 0f, 0f, false),
+                SizeSpec(0f, 1f, 0f, false),
+                SizeSpec(0f, 0f, 1f, false),
+                SizeSpec(0f, 0f, 0f, false),
+                SizeSpec(0f, 0f, 0f, true),
+                SizeSpec(100f, 0f, 0f, false, 100),
+                SizeSpec(0f, 1f, 0f, false, 100),
+                SizeSpec(0f, 0f, 1f, false, 100),
+                SizeSpec(0f, 0f, 0f, false, 100),
+                SizeSpec(0f, 0f, 0f, true, 100)
+            )
+
+        for (instance in combinations) {
+            assertThat(instance.isValid()).isEqualTo(true)
+        }
+    }
+
+    @Test
+    fun validate_getCalculatedValue() {
+        val availableSpace = 100
+        val matchWorkspaceValue = 101
+        val combinations =
+            listOf(
+                SizeSpec(100f) to 100,
+                SizeSpec(ofAvailableSpace = .5f) to (availableSpace * .5f).roundToInt(),
+                SizeSpec(ofRemainderSpace = .5f) to 0,
+                SizeSpec(matchWorkspace = true) to matchWorkspaceValue,
+                // Restricts max size up to 10 (calculated value > 10)
+                SizeSpec(100f, maxSize = 10) to 10,
+                SizeSpec(ofAvailableSpace = .5f, maxSize = 10) to 10,
+                SizeSpec(ofRemainderSpace = .5f, maxSize = 10) to 0,
+                SizeSpec(matchWorkspace = true, maxSize = 10) to 10
+            )
+
+        for ((sizeSpec, expectedValue) in combinations) {
+            val value = sizeSpec.getCalculatedValue(availableSpace, matchWorkspaceValue)
+            assertThat(value).isEqualTo(expectedValue)
+        }
+    }
+
+    @Test
+    fun validate_getRemainderSpaceValue() {
+        val remainderSpace = 100
+        val defaultValue = 50
+        val combinations =
+            listOf(
+                SizeSpec(100f) to defaultValue,
+                SizeSpec(ofAvailableSpace = .5f) to defaultValue,
+                SizeSpec(ofRemainderSpace = .5f) to (remainderSpace * .5f).roundToInt(),
+                SizeSpec(matchWorkspace = true) to defaultValue,
+                // Restricts max size up to 10 (defaultValue > 10)
+                SizeSpec(100f, maxSize = 10) to 10,
+                SizeSpec(ofAvailableSpace = .5f, maxSize = 10) to 10,
+                SizeSpec(ofRemainderSpace = .5f, maxSize = 10) to 10,
+                SizeSpec(matchWorkspace = true, maxSize = 10) to 10,
+            )
+
+        for ((sizeSpec, expectedValue) in combinations) {
+            val value = sizeSpec.getRemainderSpaceValue(remainderSpace, defaultValue)
+            assertThat(value).isEqualTo(expectedValue)
+        }
+    }
+
+    @Test
+    fun multiple_values_assigned() {
+        val combinations =
+            listOf(
+                SizeSpec(1f, 1f, 0f, false),
+                SizeSpec(1f, 0f, 1f, false),
+                SizeSpec(1f, 0f, 0f, true),
+                SizeSpec(0f, 1f, 1f, false),
+                SizeSpec(0f, 1f, 0f, true),
+                SizeSpec(0f, 0f, 1f, true),
+                SizeSpec(1f, 1f, 1f, true)
+            )
+
+        for (instance in combinations) {
+            assertThat(instance.isValid()).isEqualTo(false)
+        }
+    }
+
+    @Test
+    fun invalid_values() {
+        val combinations =
+            listOf(
+                SizeSpec(-1f, 0f, 0f, false),
+                SizeSpec(0f, 1.1f, 0f, false),
+                SizeSpec(0f, -0.1f, 0f, false),
+                SizeSpec(0f, 0f, 1.1f, false),
+                SizeSpec(0f, 0f, -0.1f, false),
+                SizeSpec(0f, 0f, 0f, false, -10),
+                SizeSpec(50f, 0f, 0f, false, 10)
+            )
+
+        for (instance in combinations) {
+            assertThat(instance.isValid()).isEqualTo(false)
+        }
+    }
+
+    @Test
+    fun onlyFixedSize() {
+        assertThat(SizeSpec(fixedSize = 16f).onlyFixedSize()).isEqualTo(true)
+
+        val combinations =
+            listOf(
+                SizeSpec(0f, 1.1f, 0f, false),
+                SizeSpec(0f, 0f, 1.1f, false),
+                SizeSpec(0f, 0f, 0f, true)
+            )
+
+        for (instance in combinations) {
+            assertThat(instance.onlyFixedSize()).isEqualTo(false)
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt b/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
new file mode 100644
index 0000000..0364069
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.AbstractDeviceProfileTest
+import com.android.launcher3.tests.R as TestR
+import com.android.launcher3.util.TestResourceHelper
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WorkspaceSpecsTest : AbstractDeviceProfileTest() {
+    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+    @Before
+    fun setup() {
+        initializeVarsForPhone(deviceSpecs["phone"]!!)
+    }
+
+    @Test
+    fun parseValidFile() {
+        val workspaceSpecs =
+            WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+        assertThat(workspaceSpecs.heightSpecs.size).isEqualTo(3)
+        assertThat(workspaceSpecs.heightSpecs[0].toString())
+            .isEqualTo(
+                "WorkspaceSpec(" +
+                    "maxAvailableSize=1533, " +
+                    "specType=HEIGHT, " +
+                    "startPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "endPadding=SizeSpec(fixedSize=84.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "gutter=SizeSpec(fixedSize=42.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "cellSize=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.15808, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647)" +
+                    ")"
+            )
+        assertThat(workspaceSpecs.heightSpecs[1].toString())
+            .isEqualTo(
+                "WorkspaceSpec(" +
+                    "maxAvailableSize=1607, " +
+                    "specType=HEIGHT, " +
+                    "startPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "endPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=1.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "gutter=SizeSpec(fixedSize=42.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "cellSize=SizeSpec(fixedSize=273.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647)" +
+                    ")"
+            )
+        assertThat(workspaceSpecs.heightSpecs[2].toString())
+            .isEqualTo(
+                "WorkspaceSpec(" +
+                    "maxAvailableSize=26247, " +
+                    "specType=HEIGHT, " +
+                    "startPadding=SizeSpec(fixedSize=21.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "endPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=1.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "gutter=SizeSpec(fixedSize=42.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "cellSize=SizeSpec(fixedSize=273.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647)" +
+                    ")"
+            )
+        assertThat(workspaceSpecs.widthSpecs.size).isEqualTo(1)
+        assertThat(workspaceSpecs.widthSpecs[0].toString())
+            .isEqualTo(
+                "WorkspaceSpec(" +
+                    "maxAvailableSize=26247, " +
+                    "specType=WIDTH, " +
+                    "startPadding=SizeSpec(fixedSize=58.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "endPadding=SizeSpec(fixedSize=58.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "gutter=SizeSpec(fixedSize=42.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "cellSize=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.25, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647)" +
+                    ")"
+            )
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_missingTag_throwsError() {
+        WorkspaceSpecs.create(
+            TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_1)
+        )
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
+        WorkspaceSpecs.create(
+            TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_2)
+        )
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_valueBiggerThan1_throwsError() {
+        WorkspaceSpecs.create(
+            TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_3)
+        )
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_matchWorkspace_true_throwsError() {
+        WorkspaceSpecs.create(
+            TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_4)
+        )
+    }
+}
diff --git a/tests/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncherTest.java b/tests/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncherTest.java
index 7e9d9da..c7431f2 100644
--- a/tests/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncherTest.java
+++ b/tests/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncherTest.java
@@ -184,7 +184,7 @@
         mStartPoint = icon.getVisibleCenter();
         mEndPoint = new Point(mStartPoint.x, mStartPoint.y);
         mLauncher.sendPointer(mDownTime, mDownTime, ACTION_DOWN, mStartPoint,
-                LauncherInstrumentation.GestureScope.INSIDE);
+                LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
         assertThat(findObjectByResourceName("popup_container")).isNotNull();
         return appName;
     }
@@ -206,7 +206,7 @@
         mStartPoint = icon.getVisibleCenter();
         mEndPoint = new Point(mStartPoint.x, mStartPoint.y);
         mLauncher.sendPointer(mDownTime, mDownTime, ACTION_DOWN, mStartPoint,
-                LauncherInstrumentation.GestureScope.INSIDE);
+                LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
         assertThat(findObjectByResourceName("popup_container")).isNotNull();
         return appName;
     }
@@ -214,12 +214,12 @@
     private void moveAppToCenterOfScreen() {
         mEndPoint.set(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight() / 2);
         mLauncher.movePointer(mDownTime, SystemClock.uptimeMillis(), DRAG_TIME_MS, true,
-                mStartPoint, mEndPoint, LauncherInstrumentation.GestureScope.INSIDE);
+                mStartPoint, mEndPoint, LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
     }
 
     private void dropApp() {
         mLauncher.sendPointer(mDownTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP,
-                mEndPoint, LauncherInstrumentation.GestureScope.INSIDE);
+                mEndPoint, LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
     }
 
     private void removeAppByName(String appName) {
diff --git a/tests/src/com/android/launcher3/tapl/TaplUtilityTests.java b/tests/src/com/android/launcher3/tapl/TaplUtilityTests.java
new file mode 100644
index 0000000..15db1d8
--- /dev/null
+++ b/tests/src/com/android/launcher3/tapl/TaplUtilityTests.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class TaplUtilityTests {
+
+    @Test
+    public void testNewStringWithRegex() {
+        assertTrue(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("Play Store has 7 notifications").matches());
+        assertTrue(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("Play  Store!").matches());
+        assertFalse(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("play  store").matches());
+        assertFalse(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("").matches());
+        assertTrue(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("Play \n Store").matches());
+    }
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java b/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java
index d3ce67c..81a59b9 100644
--- a/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java
+++ b/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java
@@ -69,7 +69,10 @@
         mView.setBackgroundColor(Color.BLUE);
         setContentView(mView);
 
-        registerReceiver(mCommandReceiver, new IntentFilter(mAction + SUFFIX_COMMAND));
+        registerReceiver(
+                mCommandReceiver,
+                new IntentFilter(mAction + SUFFIX_COMMAND),
+                RECEIVER_EXPORTED);
     }
 
     protected void addButton(String title, String method) {
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 604fe42..5240e6a 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -17,6 +17,7 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
 import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
@@ -49,11 +50,8 @@
 import androidx.test.uiautomator.Until;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.tapl.HomeAllApps;
 import com.android.launcher3.tapl.HomeAppIcon;
@@ -64,10 +62,9 @@
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
+import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.Wait;
-import com.android.launcher3.util.WidgetUtils;
 import com.android.launcher3.util.rule.FailureWatcher;
-import com.android.launcher3.util.rule.LauncherActivityRule;
 import com.android.launcher3.util.rule.SamplerRule;
 import com.android.launcher3.util.rule.ScreenRecordRule;
 import com.android.launcher3.util.rule.ShellCommandRule;
@@ -82,10 +79,6 @@
 import org.junit.rules.TestRule;
 
 import java.io.IOException;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -102,12 +95,12 @@
     public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
     public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5;
 
-    public static final long DEFAULT_UI_TIMEOUT = 10000;
+    public static final long DEFAULT_UI_TIMEOUT = TestUtil.DEFAULT_UI_TIMEOUT;
     private static final String TAG = "AbstractLauncherUiTest";
 
     private static boolean sDumpWasGenerated = false;
     private static boolean sActivityLeakReported = false;
-    private static boolean sSeenKeygard = false;
+    private static boolean sSeenKeyguard = false;
 
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
 
@@ -188,8 +181,6 @@
         mLauncher.setOnLauncherCrashed(() -> mLauncherPid = 0);
     }
 
-    protected final LauncherActivityRule mActivityMonitor = new LauncherActivityRule();
-
     @Rule
     public ShellCommandRule mDisableHeadsUpNotification =
             ShellCommandRule.disableHeadsUpNotification();
@@ -209,18 +200,13 @@
         mTargetContext.unregisterReceiver(broadcastReceiver);
     }
 
-    // Annotation for tests that need to be run in portrait and landscape modes.
-    @Retention(RetentionPolicy.RUNTIME)
-    @Target(ElementType.METHOD)
-    protected @interface PortraitLandscape {
-    }
-
     protected TestRule getRulesInsideActivityMonitor() {
-        final ViewCaptureRule viewCaptureRule = new ViewCaptureRule();
+        final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(
+                Launcher.ACTIVITY_TRACKER::getCreatedActivity);
         final RuleChain inner = RuleChain
                 .outerRule(new PortraitLandscapeRunner(this))
-                .around(viewCaptureRule)
-                .around(new FailureWatcher(mDevice, mLauncher, viewCaptureRule.getViewCapture()));
+                .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
+                .around(viewCaptureRule);
 
         return TestHelpers.isInLauncherProcess()
                 ? RuleChain.outerRule(ShellCommandRule.setDefaultLauncher()).around(inner)
@@ -231,7 +217,6 @@
     public TestRule mOrderSensitiveRules = RuleChain
             .outerRule(new SamplerRule())
             .around(new TestStabilityRule())
-            .around(mActivityMonitor)
             .around(getRulesInsideActivityMonitor());
 
     public UiDevice getDevice() {
@@ -276,9 +261,9 @@
     }
 
     private static void verifyKeyguardInvisible() {
-        final boolean keyguardAlreadyVisible = sSeenKeygard;
+        final boolean keyguardAlreadyVisible = sSeenKeyguard;
 
-        sSeenKeygard = sSeenKeygard
+        sSeenKeyguard = sSeenKeyguard
                 || !TestHelpers.wait(
                 Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000);
 
@@ -286,7 +271,7 @@
                 "Keyguard is visible, which is likely caused by a crash in SysUI, seeing keyguard"
                         + " for the first time = "
                         + !keyguardAlreadyVisible,
-                sSeenKeygard);
+                sSeenKeyguard);
     }
 
     @After
@@ -303,46 +288,20 @@
         }
     }
 
-    protected void clearLauncherData() {
-        mLauncher.clearLauncherData();
-        mLauncher.waitForLauncherInitialized();
+    protected void reinitializeLauncherData() {
+        reinitializeLauncherData(false);
     }
 
-    /**
-     * Removes all icons from homescreen and hotseat.
-     */
-    public void clearHomescreen() {
-        LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
-                LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
-        LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
-                LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
-        resetLoaderState();
-    }
-
-    protected void resetLoaderState() {
-        try {
-            mMainThreadExecutor.execute(
-                    () -> LauncherAppState.getInstance(
-                            mTargetContext).getModel().forceReload());
-        } catch (Throwable t) {
-            throw new IllegalArgumentException(t);
+    protected void reinitializeLauncherData(boolean clearWorkspace) {
+        if (clearWorkspace) {
+            mLauncher.clearLauncherData();
+        } else {
+            mLauncher.reinitializeLauncherData();
         }
         mLauncher.waitForLauncherInitialized();
     }
 
     /**
-     * Adds {@param item} on the homescreen on the 0th screen
-     */
-    public void addItemToScreen(ItemInfo item) {
-        WidgetUtils.addItemToScreen(item, mTargetContext);
-        resetLoaderState();
-
-        // Launch the home activity
-        mDevice.pressHome();
-        mLauncher.waitForLauncherInitialized();
-    }
-
-    /**
      * Runs the callback on the UI thread and returns the result.
      */
     protected <T> T getOnUiThread(final Callable<T> callback) {
@@ -360,7 +319,7 @@
 
     protected <T> T getFromLauncher(Function<Launcher, T> f) {
         if (!TestHelpers.isInLauncherProcess()) return null;
-        return getOnUiThread(() -> f.apply(mActivityMonitor.getActivity()));
+        return getOnUiThread(() -> f.apply(Launcher.ACTIVITY_TRACKER.getCreatedActivity()));
     }
 
     protected void executeOnLauncher(Consumer<Launcher> f) {
@@ -648,6 +607,8 @@
 
     protected HomeAppIcon createShortcutIfNotExist(String name, int cellX, int cellY) {
         HomeAppIcon homeAppIcon = mLauncher.getWorkspace().tryGetWorkspaceAppIcon(name);
+        Log.d(ICON_MISSING, "homeAppIcon: " + homeAppIcon + " name: " + name +
+                " cell: " + cellX + ", " + cellY);
         if (homeAppIcon == null) {
             HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
             allApps.freeze();
diff --git a/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java b/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
index fdba4eb..ba17fdc 100644
--- a/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
+++ b/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
@@ -18,6 +18,8 @@
 
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 
+import static com.android.launcher3.BubbleTextView.DISPLAY_ALL_APPS;
+import static com.android.launcher3.BubbleTextView.DISPLAY_PREDICTION_ROW;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TWOLINE_ALLAPPS;
 
 import static org.junit.Assert.assertEquals;
@@ -79,7 +81,6 @@
         mContext = new ActivityContextWrapper(getApplicationContext());
         mBubbleTextView = new BubbleTextView(mContext);
         mBubbleTextView.reset();
-        mBubbleTextView.setDisplayAllApps();
 
         BubbleTextView testView = new BubbleTextView(mContext);
         testView.setTypeface(Typeface.MONOSPACE);
@@ -104,6 +105,7 @@
     public void testEmptyString_flagOn() {
         try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true)) {
             mItemInfoWithIcon.title = EMPTY_STRING;
+            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
             mBubbleTextView.applyLabel(mItemInfoWithIcon);
             mBubbleTextView.setTypeface(Typeface.MONOSPACE);
             mBubbleTextView.measure(mLimitedWidth, 0);
@@ -118,6 +120,7 @@
     public void testEmptyString_flagOff() {
         try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false)) {
             mItemInfoWithIcon.title = EMPTY_STRING;
+            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
             mBubbleTextView.applyLabel(mItemInfoWithIcon);
             mBubbleTextView.setTypeface(Typeface.MONOSPACE);
             mBubbleTextView.measure(mLimitedWidth, 0);
@@ -134,6 +137,7 @@
             // test string: "Battery Stats"
             mItemInfoWithIcon.title = TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
             mBubbleTextView.applyLabel(mItemInfoWithIcon);
+            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
             mBubbleTextView.setTypeface(Typeface.MONOSPACE);
             mBubbleTextView.measure(mLimitedWidth, 0);
             mBubbleTextView.onPreDraw();
@@ -149,6 +153,7 @@
             // test string: "Battery Stats"
             mItemInfoWithIcon.title = TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
             mBubbleTextView.applyLabel(mItemInfoWithIcon);
+            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
             mBubbleTextView.setTypeface(Typeface.MONOSPACE);
             mBubbleTextView.measure(mLimitedWidth, 0);
             mBubbleTextView.onPreDraw();
@@ -164,6 +169,7 @@
             // test string: "flutterappflorafy"
             mItemInfoWithIcon.title = TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT;
             mBubbleTextView.applyLabel(mItemInfoWithIcon);
+            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
             mBubbleTextView.setTypeface(Typeface.MONOSPACE);
             mBubbleTextView.measure(mLimitedWidth, 0);
             mBubbleTextView.onPreDraw();
@@ -179,6 +185,7 @@
             // test string: "flutterappflorafy"
             mItemInfoWithIcon.title = TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT;
             mBubbleTextView.applyLabel(mItemInfoWithIcon);
+            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
             mBubbleTextView.setTypeface(Typeface.MONOSPACE);
             mBubbleTextView.measure(mLimitedWidth, 0);
             mBubbleTextView.onPreDraw();
@@ -194,6 +201,7 @@
             // test string: "System UWB Field Test"
             mItemInfoWithIcon.title = TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
             mBubbleTextView.applyLabel(mItemInfoWithIcon);
+            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
             mBubbleTextView.setTypeface(Typeface.MONOSPACE);
             mBubbleTextView.measure(mLimitedWidth, 0);
             mBubbleTextView.onPreDraw();
@@ -209,6 +217,7 @@
             // test string: "System UWB Field Test"
             mItemInfoWithIcon.title = TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
             mBubbleTextView.applyLabel(mItemInfoWithIcon);
+            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
             mBubbleTextView.setTypeface(Typeface.MONOSPACE);
             mBubbleTextView.measure(mLimitedWidth, 0);
             mBubbleTextView.onPreDraw();
@@ -224,6 +233,7 @@
             // test string: "LEGO®Builder"
             mItemInfoWithIcon.title = TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT;
             mBubbleTextView.applyLabel(mItemInfoWithIcon);
+            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
             mBubbleTextView.setTypeface(Typeface.MONOSPACE);
             mBubbleTextView.measure(mLimitedWidth, 0);
             mBubbleTextView.onPreDraw();
@@ -239,6 +249,7 @@
             // test string: "LEGO®Builder"
             mItemInfoWithIcon.title = TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT;
             mBubbleTextView.applyLabel(mItemInfoWithIcon);
+            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
             mBubbleTextView.setTypeface(Typeface.MONOSPACE);
             mBubbleTextView.measure(mLimitedWidth, 0);
             mBubbleTextView.onPreDraw();
@@ -291,4 +302,20 @@
                 breakPoints);
         assertEquals(TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT_RESULT, newString);
     }
+
+    @Test
+    public void testEnsurePredictionRowIsOneLine() {
+        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true)) {
+            // test string: "Battery Stats"
+            mItemInfoWithIcon.title = TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
+            mBubbleTextView.setDisplay(DISPLAY_PREDICTION_ROW);
+            mBubbleTextView.applyLabel(mItemInfoWithIcon);
+            mBubbleTextView.setTypeface(Typeface.MONOSPACE);
+            mBubbleTextView.measure(mLimitedWidth, 0);
+            mBubbleTextView.onPreDraw();
+            assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
diff --git a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
index 266f0ae..f0875f8 100644
--- a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
+++ b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
@@ -9,10 +9,21 @@
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
 
-class PortraitLandscapeRunner implements TestRule {
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+public class PortraitLandscapeRunner implements TestRule {
     private static final String TAG = "PortraitLandscapeRunner";
     private AbstractLauncherUiTest mTest;
 
+    // Annotation for tests that need to be run in portrait and landscape modes.
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.METHOD)
+    public @interface PortraitLandscape {
+    }
+
     public PortraitLandscapeRunner(AbstractLauncherUiTest test) {
         mTest = test;
     }
@@ -20,7 +31,7 @@
     @Override
     public Statement apply(Statement base, Description description) {
         if (!TestHelpers.isInLauncherProcess() ||
-                description.getAnnotation(AbstractLauncherUiTest.PortraitLandscape.class) == null) {
+                description.getAnnotation(PortraitLandscape.class) == null) {
             return base;
         }
 
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 0b10603..45b01f4 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -18,12 +18,15 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
@@ -54,10 +57,13 @@
 import com.android.launcher3.tapl.HomeAppIconMenuItem;
 import com.android.launcher3.tapl.Widgets;
 import com.android.launcher3.tapl.Workspace;
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.TestUtil;
+import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 import com.android.launcher3.util.rule.TISBindRule;
+import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 import com.android.launcher3.widget.picker.WidgetsRecyclerView;
 
@@ -94,7 +100,12 @@
     }
 
     public static void initialize(AbstractLauncherUiTest test) throws Exception {
-        test.clearLauncherData();
+        initialize(test, false);
+    }
+
+    public static void initialize(
+            AbstractLauncherUiTest test, boolean clearWorkspace) throws Exception {
+        test.reinitializeLauncherData(clearWorkspace);
         test.mDevice.pressHome();
         test.waitForLauncherCondition("Launcher didn't start", launcher -> launcher != null);
         test.waitForState("Launcher internal state didn't switch to Home",
@@ -147,7 +158,6 @@
     }
 
     @Test
-    @ScreenRecord
     public void testPressHomeOnAllAppsContextMenu() throws Exception {
         final AllApps allApps = mLauncher.getWorkspace().switchToAllApps();
         allApps.freeze();
@@ -221,7 +231,18 @@
     @PortraitLandscape
     public void testAllAppsSwitchToWorkspace() {
         assertNotNull("switchToWorkspace() returned null",
-                mLauncher.getWorkspace().switchToAllApps().switchToWorkspace());
+                mLauncher.getWorkspace().switchToAllApps()
+                        .switchToWorkspace(/* swipeDown= */ true));
+        assertTrue("Launcher internal state is not Workspace",
+                isInState(() -> LauncherState.NORMAL));
+    }
+
+    @Test
+    @PortraitLandscape
+    public void testAllAppsSwipeUpToWorkspace() {
+        assertNotNull("testAllAppsSwipeUpToWorkspace() returned null",
+                mLauncher.getWorkspace().switchToAllApps()
+                        .switchToWorkspace(/* swipeDown= */ false));
         assertTrue("Launcher internal state is not Workspace",
                 isInState(() -> LauncherState.NORMAL));
     }
@@ -245,7 +266,7 @@
         LauncherLayoutBuilder builder = new LauncherLayoutBuilder()
                 .atHotseat(0).putApp("com.android.chrome", "com.google.android.apps.chrome.Main");
         mLauncherLayout = TestUtil.setLauncherDefaultLayout(mTargetContext, builder);
-        clearLauncherData();
+        reinitializeLauncherData();
 
         final Workspace workspace = mLauncher.getWorkspace();
 
@@ -310,6 +331,8 @@
     }
 
     @Test
+    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/293191790
+    @ScreenRecord
     @PortraitLandscape
     public void testWidgets() throws Exception {
         // Test opening widgets.
@@ -371,6 +394,28 @@
         }
     }
 
+    @Test
+    public void testLaunchHomeScreenMenuItem() {
+        // Drag the test app icon to home screen and open short cut menu from the icon
+        final HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
+        allApps.freeze();
+        try {
+            allApps.getAppIcon(APP_NAME).dragToWorkspace(false, false);
+            final AppIconMenu menu = mLauncher.getWorkspace().getWorkspaceAppIcon(
+                    APP_NAME).openDeepShortcutMenu();
+
+            executeOnLauncher(
+                    launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
+                            isOptionsPopupVisible(launcher)));
+
+            final AppIconMenuItem menuItem = menu.getMenuItem(1);
+            assertEquals("Wrong menu item", "Shortcut 2", menuItem.getText());
+            menuItem.launch(getAppPackageName());
+        } finally {
+            allApps.unfreeze();
+        }
+    }
+
     @PlatinumTest(focusArea = "launcher")
     @Test
     @PortraitLandscape
@@ -503,19 +548,16 @@
 
     private void verifyAppUninstalledFromAllApps(Workspace workspace, String appName) {
         final HomeAllApps allApps = workspace.switchToAllApps();
-        allApps.freeze();
-        try {
-            assertNull(appName + " app was found on all apps after being uninstalled",
-                    allApps.tryGetAppIcon(appName));
-        } finally {
-            allApps.unfreeze();
-        }
+        Wait.atMost(appName + " app was found on all apps after being uninstalled",
+                () -> allApps.tryGetAppIcon(appName) == null,
+                DEFAULT_UI_TIMEOUT, mLauncher);
     }
 
-    @Ignore("b/256615483")
     @Test
     @PortraitLandscape
-    @PlatinumTest(focusArea = "launcher")
+    // TODO(b/293944634): Remove Screenrecord after flaky debug, and add
+    // @PlatinumTest(focusArea = "launcher") back
+    @ScreenRecord
     public void testUninstallFromWorkspace() throws Exception {
         installDummyAppAndWaitForUIUpdate();
         try {
@@ -527,7 +569,6 @@
     }
 
     @Test
-    @ScreenRecord // b/258071914
     @PortraitLandscape
     @PlatinumTest(focusArea = "launcher")
     public void testUninstallFromAllApps() throws Exception {
@@ -536,7 +577,6 @@
             Workspace workspace = mLauncher.getWorkspace();
             final HomeAllApps allApps = workspace.switchToAllApps();
             workspace = allApps.getAppIcon(DUMMY_APP_NAME).uninstall();
-            waitForLauncherUIUpdate();
             verifyAppUninstalledFromAllApps(workspace, DUMMY_APP_NAME);
         } finally {
             TestUtil.uninstallDummyApp();
@@ -560,7 +600,7 @@
                 allApps.unfreeze();
             }
             // Reset the workspace for the next shortcut creation.
-            initialize(this);
+            initialize(this, true);
             endTime = SystemClock.uptimeMillis();
             elapsedTime = endTime - startTime;
             Log.d("testDragAppIconToWorkspaceCellTime",
@@ -579,11 +619,19 @@
         }
     }
 
+    /**
+     * Adds three icons to the workspace and removes one of them by dragging to uninstall.
+     */
     @Test
     @ScreenRecord // b/241821721
     @PlatinumTest(focusArea = "launcher")
-    public void getIconsPosition_afterIconRemoved_notContained() throws IOException {
+    public void uninstallWorkspaceIcon() throws IOException {
         Point[] gridPositions = getCornersAndCenterPositions();
+        StringBuilder sb = new StringBuilder();
+        for (Point p : gridPositions) {
+            sb.append(p).append(", ");
+        }
+        Log.d(ICON_MISSING, "allGridPositions: " + sb);
         createShortcutIfNotExist(STORE_APP_NAME, gridPositions[0]);
         createShortcutIfNotExist(MAPS_APP_NAME, gridPositions[1]);
         installDummyAppAndWaitForUIUpdate();
@@ -598,6 +646,10 @@
             mLauncher.getWorkspace().verifyWorkspaceAppIconIsGone(
                     DUMMY_APP_NAME + " was expected to disappear after uninstall.", DUMMY_APP_NAME);
 
+            // Debug for b/288944469 I want to test if we are not waiting enough after removing
+            // the icon to request the list of icons again, since the items are not removed
+            // immediately. This should reduce the flake rate
+            SystemClock.sleep(500);
             Map<String, Point> finalPositions =
                     mLauncher.getWorkspace().getWorkspaceIconsPositions();
             assertThat(finalPositions).doesNotContainKey(DUMMY_APP_NAME);
@@ -677,8 +729,8 @@
         HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
         allApps.freeze();
         try {
-            HomeAppIcon icon = allApps.getAppIcon(APP_NAME);
-            assertEquals("Wrong app icon name.", icon.getIconName(), APP_NAME);
+            // getAppIcon() already verifies that the icon is not null and is the correct icon name.
+            allApps.getAppIcon(APP_NAME);
         } finally {
             allApps.unfreeze();
         }
diff --git a/tests/src/com/android/launcher3/ui/TestViewHelpers.java b/tests/src/com/android/launcher3/ui/TestViewHelpers.java
index 083f580..4b2bade 100644
--- a/tests/src/com/android/launcher3/ui/TestViewHelpers.java
+++ b/tests/src/com/android/launcher3/ui/TestViewHelpers.java
@@ -15,11 +15,14 @@
  */
 package com.android.launcher3.ui;
 
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
-import static androidx.test.InstrumentationRegistry.getTargetContext;
+import static android.os.Process.myUserHandle;
 
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.launcher3.util.TestUtil.getOnUiThread;
+
+import android.app.Instrumentation;
 import android.content.ComponentName;
-import android.os.Process;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
@@ -29,7 +32,6 @@
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
-import java.util.concurrent.Callable;
 import java.util.function.Function;
 
 public class TestViewHelpers {
@@ -38,23 +40,16 @@
     /**
      * Finds a widget provider which can fit on the home screen.
      *
-     * @param test               test suite.
      * @param hasConfigureScreen if true, a provider with a config screen is returned.
      */
-    public static LauncherAppWidgetProviderInfo findWidgetProvider(AbstractLauncherUiTest test,
-            final boolean hasConfigureScreen) {
-        LauncherAppWidgetProviderInfo info =
-                test.getOnUiThread(new Callable<LauncherAppWidgetProviderInfo>() {
-                    @Override
-                    public LauncherAppWidgetProviderInfo call() throws Exception {
-                        ComponentName cn = new ComponentName(getInstrumentation().getContext(),
-                                hasConfigureScreen ? AppWidgetWithConfig.class
-                                        : AppWidgetNoConfig.class);
-                        Log.d(TAG, "findWidgetProvider componentName=" + cn.flattenToString());
-                        return new WidgetManagerHelper(getTargetContext())
-                                .findProvider(cn, Process.myUserHandle());
-                    }
-                });
+    public static LauncherAppWidgetProviderInfo findWidgetProvider(boolean hasConfigureScreen) {
+        LauncherAppWidgetProviderInfo info = getOnUiThread(() -> {
+            Instrumentation i = getInstrumentation();
+            ComponentName cn = new ComponentName(i.getContext(),
+                    hasConfigureScreen ? AppWidgetWithConfig.class : AppWidgetNoConfig.class);
+            Log.d(TAG, "findWidgetProvider componentName=" + cn.flattenToString());
+            return new WidgetManagerHelper(i.getTargetContext()).findProvider(cn, myUserHandle());
+        });
         if (info == null) {
             throw new IllegalArgumentException("No valid widget provider");
         }
diff --git a/tests/src/com/android/launcher3/ui/WorkProfileTest.java b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
index 026766c..5b9adcd 100644
--- a/tests/src/com/android/launcher3/ui/WorkProfileTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
@@ -18,7 +18,6 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST;
-import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
 import static com.android.launcher3.util.TestUtil.installDummyAppForUser;
 import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
 import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
@@ -39,7 +38,6 @@
 import com.android.launcher3.allapps.WorkPausedCard;
 import com.android.launcher3.allapps.WorkProfileManager;
 import com.android.launcher3.tapl.LauncherInstrumentation;
-import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 
@@ -78,8 +76,6 @@
         installDummyAppForUser(mProfileUserId);
         updateWorkProfileSetupSuccessful("am start-user", output);
 
-        Log.d(WORK_TAB_MISSING, "workProfileSuccessful? " + mWorkProfileSetupSuccessful +
-                " shellCmd: " + logStr);
         if (!mWorkProfileSetupSuccessful) {
             return; // no need to setup launcher since all tests will skip.
         }
@@ -89,14 +85,13 @@
         waitForStateTransitionToEnd("Launcher internal state didn't switch to Normal",
                 () -> NORMAL);
         waitForResumed("Launcher internal state is still Background");
-        executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
+        mLauncher.getWorkspace().switchToAllApps();
         waitForStateTransitionToEnd("Launcher internal state didn't switch to All Apps",
                 () -> ALL_APPS);
     }
 
     @After
     public void removeWorkProfile() throws Exception {
-        Log.d(TestProtocol.WORK_TAB_MISSING, "WorkProfileTest teardown");
         executeOnLauncher(launcher -> {
             if (launcher == null || launcher.getAppsView() == null) {
                 return;
@@ -112,7 +107,6 @@
         mLauncher.getAllApps();
         waitForLauncherCondition("Work tab not setup", launcher -> {
             if (launcher.getAppsView().getContentView() instanceof AllAppsPagedView) {
-                Log.d(WORK_TAB_MISSING, "Deferring AppsStore updates");
                 launcher.getAppsView().getAppsStore().enableDeferUpdates(DEFER_UPDATES_TEST);
                 return true;
             }
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index e9a2b0f..b2ce400 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -30,10 +30,13 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.launcher3.Launcher;
+import com.android.launcher3.celllayout.FavoriteItemsTransaction;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.testcomponent.WidgetConfigActivity;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ShellCommandRule;
@@ -63,7 +66,7 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mWidgetInfo = TestViewHelpers.findWidgetProvider(this, true /* hasConfigureScreen */);
+        mWidgetInfo = TestViewHelpers.findWidgetProvider(true /* hasConfigureScreen */);
         mAppWidgetManager = AppWidgetManager.getInstance(mTargetContext);
     }
 
@@ -84,8 +87,7 @@
      * @param acceptConfig accept the config activity
      */
     private void runTest(boolean acceptConfig) throws Throwable {
-        clearHomescreen();
-        mDevice.pressHome();
+        new FavoriteItemsTransaction(mTargetContext).commitAndLoadHome(mLauncher);
 
         // Drag widget to homescreen
         WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
@@ -123,7 +125,10 @@
 
         @Override
         public boolean isTrue() throws Throwable {
-            return mMainThreadExecutor.submit(mActivityMonitor.itemExists(this)).get();
+            return mMainThreadExecutor.submit(() -> {
+                Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+                return l != null && l.getWorkspace().getFirstMatch(this) != null;
+            }).get();
         }
 
         @Override
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index 3eb20e3..9dca24b 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -18,17 +18,18 @@
 import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
 
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
 
 import android.platform.test.annotations.PlatinumTest;
+import android.platform.test.rule.ScreenRecordRule;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.celllayout.FavoriteItemsTransaction;
 import com.android.launcher3.tapl.Widget;
 import com.android.launcher3.tapl.WidgetResizeFrame;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.rule.ShellCommandRule;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
@@ -51,14 +52,15 @@
     @PlatinumTest(focusArea = "launcher")
     @Test
     @PortraitLandscape
+    @ScreenRecordRule.ScreenRecord // b/289161193
     public void testDragIcon() throws Throwable {
-        clearHomescreen();
-        mDevice.pressHome();
+        mLauncher.enableDebugTracing(); // b/289161193
+        new FavoriteItemsTransaction(mTargetContext).commitAndLoadHome(mLauncher);
 
         waitForLauncherCondition("Workspace didn't finish loading", l -> !l.isWorkspaceLoading());
 
         final LauncherAppWidgetProviderInfo widgetInfo =
-                TestViewHelpers.findWidgetProvider(this, false /* hasConfigureScreen */);
+                TestViewHelpers.findWidgetProvider(false /* hasConfigureScreen */);
 
         WidgetResizeFrame resizeFrame = mLauncher
                 .getWorkspace()
@@ -66,11 +68,6 @@
                 .getWidget(widgetInfo.getLabel(mTargetContext.getPackageManager()))
                 .dragWidgetToWorkspace();
 
-        assertTrue(mActivityMonitor.itemExists(
-                (info, view) -> info instanceof LauncherAppWidgetInfo &&
-                        ((LauncherAppWidgetInfo) info).providerName.getClassName().equals(
-                                widgetInfo.provider.getClassName())).call());
-
         assertNotNull("Widget resize frame not shown after widget add", resizeFrame);
         resizeFrame.dismiss();
 
@@ -78,6 +75,7 @@
                 DEFAULT_UI_TIMEOUT);
         assertNotNull("Widget not found on the workspace", widget);
         widget.launch(getAppPackageName());
+        mLauncher.disableDebugTracing(); // b/289161193
     }
 
     /**
@@ -90,8 +88,8 @@
     @Test
     @PortraitLandscape
     public void testDragCustomShortcut() throws Throwable {
-        clearHomescreen();
-        mDevice.pressHome();
+        new FavoriteItemsTransaction(mTargetContext).commitAndLoadHome(mLauncher);
+
         mLauncher.getWorkspace().openAllWidgets()
                 .getWidget("com.android.launcher3.testcomponent.CustomShortcutConfigActivity")
                 .dragToWorkspace(false, true);
diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index 0f861eb..7db3161 100644
--- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -15,8 +15,13 @@
  */
 package com.android.launcher3.ui.widget;
 
-import static androidx.test.InstrumentationRegistry.getTargetContext;
-
+import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
+import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID;
+import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
+import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
+import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
+import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.WidgetUtils.createWidgetInfo;
 
 import static org.junit.Assert.assertEquals;
@@ -26,7 +31,6 @@
 
 import android.appwidget.AppWidgetManager;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionParams;
 import android.content.pm.PackageManager;
@@ -34,11 +38,14 @@
 import android.os.Bundle;
 import android.widget.RemoteViews;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
 
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
+import com.android.launcher3.celllayout.FavoriteItemsTransaction;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.tapl.Widget;
@@ -57,6 +64,7 @@
 
 import java.util.HashSet;
 import java.util.Set;
+import java.util.function.Consumer;
 
 /**
  * Tests for bind widget flow.
@@ -70,24 +78,18 @@
     @Rule
     public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
 
-    private ContentResolver mResolver;
-
     // Objects created during test, which should be cleaned up in the end.
     private Cursor mCursor;
     // App install session id.
     private int mSessionId = -1;
 
+    private LauncherModel mModel;
+
     @Override
     @Before
     public void setUp() throws Exception {
         super.setUp();
-
-        mResolver = mTargetContext.getContentResolver();
-
-        // Clear all existing data
-        LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
-        LauncherSettings.Settings.call(mResolver,
-                LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
+        mModel = LauncherAppState.getInstance(mTargetContext).getModel();
     }
 
     @After
@@ -103,34 +105,24 @@
 
     @Test
     public void testBindNormalWidget_withConfig() {
-        LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, true);
-        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);
-
-        addItemToScreen(item);
+        LauncherAppWidgetProviderInfo info = addWidgetToScreen(true, true, i -> { });
         verifyWidgetPresent(info);
     }
 
     @Test
     public void testBindNormalWidget_withoutConfig() {
-        LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
-        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);
-
-        addItemToScreen(item);
+        LauncherAppWidgetProviderInfo info = addWidgetToScreen(false, true, i -> { });
         verifyWidgetPresent(info);
     }
 
     @Test
     public void testUnboundWidget_removed() {
-        LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
-        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), false);
-        item.appWidgetId = -33;
-
-        addItemToScreen(item);
+        LauncherAppWidgetProviderInfo info = addWidgetToScreen(false, false,
+                item -> item.appWidgetId = -33);
 
         final Workspace workspace = mLauncher.getWorkspace();
         // Item deleted from db
-        mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
-                null, null, null, null, null);
+        mCursor = queryItem();
         assertEquals(0, mCursor.getCount());
 
         // The view does not exist
@@ -140,36 +132,26 @@
     @Test
     public void testPendingWidget_autoRestored() {
         // A non-restored widget with no config screen gets restored automatically.
-        LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
-
         // Do not bind the widget
-        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), false);
-        item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
-
-        addItemToScreen(item);
+        LauncherAppWidgetProviderInfo info = addWidgetToScreen(false, false,
+                item -> item.restoreStatus = FLAG_ID_NOT_VALID);
         verifyWidgetPresent(info);
     }
 
     @Test
     public void testPendingWidget_withConfigScreen() {
         // A non-restored widget with config screen get bound and shows a 'Click to setup' UI.
-        LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, true);
-
         // Do not bind the widget
-        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), false);
-        item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
-
-        addItemToScreen(item);
+        LauncherAppWidgetProviderInfo info = addWidgetToScreen(true, false,
+                item -> item.restoreStatus = FLAG_ID_NOT_VALID);
         verifyPendingWidgetPresent();
 
-        // Item deleted from db
-        mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
-                null, null, null, null, null);
+        mCursor = queryItem();
         mCursor.moveToNext();
 
         // Widget has a valid Id now.
         assertEquals(0, mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
-                & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
+                & FLAG_ID_NOT_VALID);
         assertNotNull(AppWidgetManager.getInstance(mTargetContext)
                 .getAppWidgetInfo(mCursor.getInt(mCursor.getColumnIndex(
                         LauncherSettings.Favorites.APPWIDGET_ID))));
@@ -185,7 +167,6 @@
         appWidgetManager.updateAppWidgetOptions(appWidgetId, b);
         appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
 
-
         // verify changes are reflected
         waitForLauncherCondition("App widget options did not update",
                 l -> appWidgetManager.getAppWidgetOptions(appWidgetId).getBoolean(
@@ -197,17 +178,12 @@
 
     @Test
     public void testPendingWidget_notRestored_removed() {
-        LauncherAppWidgetInfo item = getInvalidWidgetInfo();
-        item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
-                | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
-
-        addItemToScreen(item);
+        addPendingItemToScreen(getInvalidWidgetInfo(), FLAG_ID_NOT_VALID | FLAG_PROVIDER_NOT_READY);
 
         assertTrue("Pending widget exists",
                 mLauncher.getWorkspace().tryGetPendingWidget(0) == null);
         // Item deleted from db
-        mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
-                null, null, null, null, null);
+        mCursor = queryItem();
         assertEquals(0, mCursor.getCount());
     }
 
@@ -215,32 +191,25 @@
     public void testPendingWidget_notRestored_brokenInstall() {
         // A widget which is was being installed once, even if its not being
         // installed at the moment is not removed.
-        LauncherAppWidgetInfo item = getInvalidWidgetInfo();
-        item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
-                | LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
-                | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
-
-        addItemToScreen(item);
+        addPendingItemToScreen(getInvalidWidgetInfo(),
+                FLAG_ID_NOT_VALID | FLAG_RESTORE_STARTED | FLAG_PROVIDER_NOT_READY);
         verifyPendingWidgetPresent();
 
         // Verify item still exists in db
-        mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
-                null, null, null, null, null);
+        mCursor = queryItem();
         assertEquals(1, mCursor.getCount());
 
         // Widget still has an invalid id.
         mCursor.moveToNext();
-        assertEquals(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID,
+        assertEquals(FLAG_ID_NOT_VALID,
                 mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
-                        & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
+                        & FLAG_ID_NOT_VALID);
     }
 
     @Test
     public void testPendingWidget_notRestored_activeInstall() throws Exception {
         // A widget which is being installed is not removed
         LauncherAppWidgetInfo item = getInvalidWidgetInfo();
-        item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
-                | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
 
         // Create an active installer session
         SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
@@ -248,19 +217,18 @@
         PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller();
         mSessionId = installer.createSession(params);
 
-        addItemToScreen(item);
+        addPendingItemToScreen(item, FLAG_ID_NOT_VALID | FLAG_PROVIDER_NOT_READY);
         verifyPendingWidgetPresent();
 
         // Verify item still exists in db
-        mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
-                null, null, null, null, null);
+        mCursor = queryItem();
         assertEquals(1, mCursor.getCount());
 
         // Widget still has an invalid id.
         mCursor.moveToNext();
-        assertEquals(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID,
+        assertEquals(FLAG_ID_NOT_VALID,
                 mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
-                        & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
+                        & FLAG_ID_NOT_VALID);
     }
 
     private void verifyWidgetPresent(LauncherAppWidgetProviderInfo info) {
@@ -275,6 +243,28 @@
                 widget != null);
     }
 
+    private void addPendingItemToScreen(LauncherAppWidgetInfo item, int restoreStatus) {
+        item.restoreStatus = restoreStatus;
+        item.screenId = FIRST_SCREEN_ID;
+        new FavoriteItemsTransaction(mTargetContext)
+                .addItem(() -> item)
+                .commitAndLoadHome(mLauncher);
+    }
+
+    private LauncherAppWidgetProviderInfo addWidgetToScreen(boolean hasConfigureScreen,
+            boolean bindWidget, Consumer<LauncherAppWidgetInfo> itemOverride) {
+        LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(hasConfigureScreen);
+        new FavoriteItemsTransaction(mTargetContext)
+                .addItem(() -> {
+                    LauncherAppWidgetInfo item = createWidgetInfo(info, mTargetContext, bindWidget);
+                    item.screenId = FIRST_SCREEN_ID;
+                    itemOverride.accept(item);
+                    return item;
+                })
+                .commitAndLoadHome(mLauncher);
+        return info;
+    }
+
     /**
      * Returns a LauncherAppWidgetInfo with package name which is not present on the device
      */
@@ -312,4 +302,14 @@
         item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
         return item;
     }
+
+    private Cursor queryItem() {
+        try {
+            return MODEL_EXECUTOR.submit(() ->
+                mModel.getModelDbController().query(
+                                TABLE_NAME, null, itemIdMatch(0), null, null)).get();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index bf9eb5a..a6b5369 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -32,7 +32,9 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.celllayout.FavoriteItemsTransaction;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -134,8 +136,7 @@
 
     private void runTest(String activityMethod, boolean isWidget, ItemOperator itemMatcher,
             Intent... commandIntents) throws Throwable {
-        clearHomescreen();
-        mDevice.pressHome();
+        new FavoriteItemsTransaction(mTargetContext).commitAndLoadHome(mLauncher);
 
         // Open Pin item activity
         BlockingBroadcastReceiver openMonitor = new BlockingBroadcastReceiver(
@@ -193,7 +194,10 @@
 
         @Override
         public boolean isTrue() throws Throwable {
-            return mMainThreadExecutor.submit(mActivityMonitor.itemExists(mOp)).get();
+            return mMainThreadExecutor.submit(() -> {
+                Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+                return l != null && l.getWorkspace().getFirstMatch(mOp) != null;
+            }).get();
         }
     }
 }
diff --git a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
index 7ba0b53..8e5e9cc 100644
--- a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
@@ -147,9 +147,8 @@
                 for (int i = parent.getChildCount() - 1; i >= 0; i--) {
                     viewQueue.add(parent.getChildAt(i));
                 }
-            } else if (view instanceof BubbleTextView) {
-                BubbleTextView btv = (BubbleTextView) view;
-                if (title.equals(btv.getText())) {
+            } else if (view instanceof BubbleTextView btv) {
+                if (title.equals(btv.getContentDescription().toString())) {
                     icon = btv;
                     break;
                 }
diff --git a/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java
index c4b6d43..62a8179 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java
@@ -29,6 +29,7 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.tapl.Workspace;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.ui.TaplTestsLauncher3;
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.TestUtil;
@@ -177,14 +178,15 @@
     public void testDragIconToPage3() {
         Workspace workspace = mLauncher.getWorkspace();
 
-        workspace.dragIcon(workspace.getHotseatAppIcon("Phone"), 3);
+        // b/299522368 sometimes the phone app is not present in the hotseat.
+        workspace.dragIcon(workspace.getHotseatAppIcon("Chrome"), 3);
 
         executeOnLauncher(launcher -> {
             assertPagesExist(launcher, 0, 1, 2, 3);
             assertItemsOnPage(launcher, 0, "Play Store", "Maps");
             assertPageEmpty(launcher, 1);
             assertPageEmpty(launcher, 2);
-            assertItemsOnPage(launcher, 3, "Phone");
+            assertItemsOnPage(launcher, 3, "Chrome");
         });
     }
 
diff --git a/tests/src/com/android/launcher3/util/DisplayControllerTest.kt b/tests/src/com/android/launcher3/util/DisplayControllerTest.kt
new file mode 100644
index 0000000..8e4e998
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/DisplayControllerTest.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util
+
+import android.content.Context
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.graphics.Point
+import android.graphics.Rect
+import android.hardware.display.DisplayManager
+import android.util.ArrayMap
+import android.util.DisplayMetrics
+import android.view.Display
+import android.view.Surface
+import androidx.test.annotation.UiThreadTest
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.util.DisplayController.CHANGE_DENSITY
+import com.android.launcher3.util.DisplayController.CHANGE_ROTATION
+import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
+import com.android.launcher3.util.window.CachedDisplayInfo
+import com.android.launcher3.util.window.WindowManagerProxy
+import kotlin.math.min
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+import org.mockito.stubbing.Answer
+
+/** Unit tests for {@link DisplayController} */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DisplayControllerTest {
+
+    private val appContext: Context = ApplicationProvider.getApplicationContext()
+
+    @Mock private lateinit var context: SandboxContext
+    @Mock private lateinit var windowManagerProxy: WindowManagerProxy
+    @Mock private lateinit var launcherPrefs: LauncherPrefs
+    @Mock private lateinit var displayManager: DisplayManager
+    @Mock private lateinit var display: Display
+    @Mock private lateinit var resources: Resources
+    @Mock private lateinit var displayInfoChangeListener: DisplayInfoChangeListener
+
+    private lateinit var displayController: DisplayController
+
+    private val width = 2208
+    private val height = 1840
+    private val inset = 110
+    private val densityDpi = 420
+    private val density = densityDpi / DisplayMetrics.DENSITY_DEFAULT.toFloat()
+    private val bounds =
+        arrayOf(
+            WindowBounds(Rect(0, 0, width, height), Rect(0, inset, 0, 0), Surface.ROTATION_0),
+            WindowBounds(Rect(0, 0, height, width), Rect(0, inset, 0, 0), Surface.ROTATION_90),
+            WindowBounds(Rect(0, 0, width, height), Rect(0, inset, 0, 0), Surface.ROTATION_180),
+            WindowBounds(Rect(0, 0, height, width), Rect(0, inset, 0, 0), Surface.ROTATION_270)
+        )
+    private val configuration =
+        Configuration(appContext.resources.configuration).apply {
+            densityDpi = this@DisplayControllerTest.densityDpi
+            screenWidthDp = (bounds[0].bounds.width() / density).toInt()
+            screenHeightDp = (bounds[0].bounds.height() / density).toInt()
+            smallestScreenWidthDp = min(screenWidthDp, screenHeightDp)
+        }
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(context.getObject(eq(WindowManagerProxy.INSTANCE))).thenReturn(windowManagerProxy)
+        whenever(context.getObject(eq(LauncherPrefs.INSTANCE))).thenReturn(launcherPrefs)
+
+        // Mock WindowManagerProxy
+        val displayInfo =
+            CachedDisplayInfo(Point(width, height), Surface.ROTATION_0, Rect(0, 0, 0, 0))
+        whenever(windowManagerProxy.getDisplayInfo(any())).thenReturn(displayInfo)
+        whenever(windowManagerProxy.estimateInternalDisplayBounds(any()))
+            .thenAnswer(
+                Answer {
+                    // Always create a new copy of bounds
+                    val perDisplayBounds = ArrayMap<CachedDisplayInfo, List<WindowBounds>>()
+                    perDisplayBounds[displayInfo] = bounds.toList()
+                    return@Answer perDisplayBounds
+                }
+            )
+        whenever(windowManagerProxy.getRealBounds(any(), any())).thenAnswer { i ->
+            bounds[i.getArgument<CachedDisplayInfo>(1).rotation]
+        }
+
+        // Mock context
+        whenever(context.createWindowContext(any(), any(), nullable())).thenReturn(context)
+        whenever(context.getSystemService(eq(DisplayManager::class.java)))
+            .thenReturn(displayManager)
+        doNothing().`when`(context).registerComponentCallbacks(any())
+
+        // Mock display
+        whenever(display.rotation).thenReturn(displayInfo.rotation)
+        whenever(context.display).thenReturn(display)
+        whenever(displayManager.getDisplay(any())).thenReturn(display)
+
+        // Mock resources
+        whenever(resources.configuration).thenReturn(configuration)
+        whenever(context.resources).thenReturn(resources)
+
+        // Initialize DisplayController
+        displayController = DisplayController(context)
+        displayController.addChangeListener(displayInfoChangeListener)
+    }
+
+    @Test
+    @UiThreadTest
+    fun testRotation() {
+        val displayInfo =
+            CachedDisplayInfo(Point(height, width), Surface.ROTATION_90, Rect(0, 0, 0, 0))
+        whenever(windowManagerProxy.getDisplayInfo(any())).thenReturn(displayInfo)
+        whenever(display.rotation).thenReturn(displayInfo.rotation)
+        val configuration =
+            Configuration(configuration).apply {
+                screenWidthDp = configuration.screenHeightDp
+                screenHeightDp = configuration.screenWidthDp
+            }
+        whenever(resources.configuration).thenReturn(configuration)
+
+        displayController.onConfigurationChanged(configuration)
+
+        verify(displayInfoChangeListener).onDisplayInfoChanged(any(), any(), eq(CHANGE_ROTATION))
+    }
+
+    @Test
+    @UiThreadTest
+    fun testFontScale() {
+        val configuration = Configuration(configuration).apply { fontScale = 1.2f }
+        whenever(resources.configuration).thenReturn(configuration)
+
+        displayController.onConfigurationChanged(configuration)
+
+        verify(displayInfoChangeListener).onDisplayInfoChanged(any(), any(), eq(CHANGE_DENSITY))
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/IconSizeStepsTest.kt b/tests/src/com/android/launcher3/util/IconSizeStepsTest.kt
new file mode 100644
index 0000000..d9e3377
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/IconSizeStepsTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util
+
+import android.content.Context
+import android.content.res.Configuration
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IconSizeStepsTest {
+    private var context: Context? = null
+    private val runningContext: Context = ApplicationProvider.getApplicationContext()
+    private lateinit var iconSizeSteps: IconSizeSteps
+
+    @Before
+    fun setup() {
+        // 160dp makes 1px = 1dp
+        val config =
+            Configuration(runningContext.resources.configuration).apply { this.densityDpi = 160 }
+        context = runningContext.createConfigurationContext(config)
+        iconSizeSteps = IconSizeSteps(context!!.resources)
+    }
+
+    @Test
+    fun minimumIconSize() {
+        assertThat(iconSizeSteps.minimumIconSize()).isEqualTo(52)
+    }
+
+    @Test
+    fun getNextLowerIconSize() {
+        assertThat(iconSizeSteps.getNextLowerIconSize(66)).isEqualTo(63)
+
+        // Assert that never goes below minimum
+        assertThat(iconSizeSteps.getNextLowerIconSize(52)).isEqualTo(52)
+        assertThat(iconSizeSteps.getNextLowerIconSize(30)).isEqualTo(52)
+    }
+
+    @Test
+    fun getIconSmallerThan() {
+        assertThat(iconSizeSteps.getIconSmallerThan(60)).isEqualTo(59)
+
+        // Assert that never goes below minimum
+        assertThat(iconSizeSteps.getIconSmallerThan(52)).isEqualTo(52)
+        assertThat(iconSizeSteps.getIconSmallerThan(30)).isEqualTo(52)
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/src/com/android/launcher3/util/LauncherModelHelper.java
index 976afcd..261436b 100644
--- a/tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -15,34 +15,31 @@
  */
 package com.android.launcher3.util;
 
-import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL;
+
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
-import static com.android.launcher3.LauncherSettings.Favorites.CONTENT_URI;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
 
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
 
-import android.content.ComponentName;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionParams;
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
-import android.content.res.Resources;
-import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Color;
 import android.net.Uri;
 import android.os.ParcelFileDescriptor;
 import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
-import android.os.Process;
 import android.provider.Settings;
 import android.test.mock.MockContentResolver;
 import android.util.ArrayMap;
@@ -56,14 +53,10 @@
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.LauncherPrefs;
-import com.android.launcher3.LauncherProvider;
-import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.model.AllAppsList;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.ItemInstallQueue;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.testing.TestInformationProvider;
@@ -72,37 +65,25 @@
 import com.android.launcher3.util.window.WindowManagerProxy;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
-import org.mockito.ArgumentCaptor;
-
-import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileNotFoundException;
-import java.io.InputStreamReader;
+import java.io.IOException;
 import java.io.OutputStreamWriter;
-import java.lang.reflect.Field;
-import java.util.HashMap;
-import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
-import java.util.function.Function;
 
 /**
  * Utility class to help manage Launcher Model and related objects for test.
  */
 public class LauncherModelHelper {
 
-    public static final int DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP;
-    public static final int HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
-
-    public static final int APP_ICON = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
-    public static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
-    public static final int NO__ICON = -1;
-
-    public static final String TEST_PACKAGE = testContext().getPackageName();
+    public static final String TEST_PACKAGE = getInstrumentation().getContext().getPackageName();
     public static final String TEST_ACTIVITY = "com.android.launcher3.tests.Activity2";
+    public static final String TEST_ACTIVITY2 = "com.android.launcher3.tests.Activity3";
+    public static final String TEST_ACTIVITY3 = "com.android.launcher3.tests.Activity4";
 
     // Authority for providing a test default-workspace-layout data.
     private static final String TEST_PROVIDER_AUTHORITY =
@@ -110,39 +91,18 @@
     private static final int DEFAULT_BITMAP_SIZE = 10;
     private static final int DEFAULT_GRID_SIZE = 4;
 
-    private final HashMap<Class, HashMap<String, Field>> mFieldCache = new HashMap<>();
-    private final MockContentResolver mMockResolver = new MockContentResolver();
-    public final TestLauncherProvider provider;
     public final SandboxModelContext sandboxContext;
 
-    public final long defaultProfileId;
+    private final RunnableList mDestroyTask = new RunnableList();
 
     private BgDataModel mDataModel;
-    private AllAppsList mAllAppsList;
 
     public LauncherModelHelper() {
-        Context context = getApplicationContext();
-        // System settings cache content provider. Ensure that they are statically initialized
-        Settings.Secure.getString(context.getContentResolver(), "test");
-        Settings.System.getString(context.getContentResolver(), "test");
-        Settings.Global.getString(context.getContentResolver(), "test");
-
-        provider = new TestLauncherProvider();
         sandboxContext = new SandboxModelContext();
-        defaultProfileId = UserCache.INSTANCE.get(sandboxContext)
-                .getSerialNumberForUser(Process.myUserHandle());
-        setupProvider(LauncherProvider.AUTHORITY, provider);
     }
 
     public void setupProvider(String authority, ContentProvider provider) {
-        ProviderInfo providerInfo = new ProviderInfo();
-        providerInfo.authority = authority;
-        providerInfo.applicationInfo = sandboxContext.getApplicationInfo();
-        provider.attachInfo(sandboxContext, providerInfo);
-        mMockResolver.addProvider(providerInfo.authority, provider);
-        doReturn(providerInfo)
-                .when(sandboxContext.mPm)
-                .resolveContentProvider(eq(authority), anyInt());
+        sandboxContext.setupProvider(authority, provider);
     }
 
     public LauncherModel getModel() {
@@ -151,16 +111,35 @@
 
     public synchronized BgDataModel getBgDataModel() {
         if (mDataModel == null) {
-            mDataModel = ReflectionHelpers.getField(getModel(), "mBgDataModel");
+            getModel().enqueueModelUpdateTask(new ModelUpdateTask() {
+                @Override
+                public void init(@NonNull LauncherAppState app, @NonNull LauncherModel model,
+                        @NonNull BgDataModel dataModel, @NonNull AllAppsList allAppsList,
+                        @NonNull Executor uiExecutor) {
+                    mDataModel = dataModel;
+                }
+
+                @Override
+                public void run() { }
+            });
         }
         return mDataModel;
     }
 
-    public synchronized AllAppsList getAllAppsList() {
-        if (mAllAppsList == null) {
-            mAllAppsList = ReflectionHelpers.getField(getModel(), "mBgAllAppsList");
-        }
-        return mAllAppsList;
+    /**
+     * Creates a installer session for the provided package.
+     */
+    public int createInstallerSession(String pkg) throws IOException {
+        SessionParams sp = new SessionParams(MODE_FULL_INSTALL);
+        sp.setAppPackageName(pkg);
+        Bitmap icon = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+        icon.eraseColor(Color.RED);
+        sp.setAppIcon(icon);
+        sp.setAppLabel(pkg);
+        PackageInstaller pi = sandboxContext.getPackageManager().getPackageInstaller();
+        int sessionId = pi.createSession(sp);
+        mDestroyTask.add(() -> pi.abandonSession(sessionId));
+        return sessionId;
     }
 
     public void destroy() {
@@ -175,6 +154,8 @@
         waitOrThrow(l1);
         sandboxContext.onDestroy();
         l2.countDown();
+
+        mDestroyTask.executeAllAndDestroy();
     }
 
     private void waitOrThrow(CountDownLatch latch) {
@@ -186,184 +167,6 @@
     }
 
     /**
-     * Synchronously executes the task and returns all the UI callbacks posted.
-     */
-    public List<Runnable> executeTaskForTest(ModelUpdateTask task) throws Exception {
-        LauncherModel model = getModel();
-        if (!model.isModelLoaded()) {
-            ReflectionHelpers.setField(model, "mModelLoaded", true);
-        }
-        Executor mockExecutor = mock(Executor.class);
-        model.enqueueModelUpdateTask(new ModelUpdateTask() {
-            @Override
-            public void init(@NonNull final LauncherAppState app,
-                    @NonNull final LauncherModel model, @NonNull final BgDataModel dataModel,
-                    @NonNull final AllAppsList allAppsList, @NonNull final Executor uiExecutor) {
-                task.init(app, model, dataModel, allAppsList, mockExecutor);
-            }
-
-            @Override
-            public void run() {
-                task.run();
-            }
-        });
-        MODEL_EXECUTOR.submit(() -> null).get();
-
-        ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
-        verify(mockExecutor, atLeast(0)).execute(captor.capture());
-        return captor.getAllValues();
-    }
-
-    /**
-     * Synchronously executes a task on the model
-     */
-    public <T> T executeSimpleTask(Function<BgDataModel, T> task) throws Exception {
-        BgDataModel dataModel = getBgDataModel();
-        return MODEL_EXECUTOR.submit(() -> task.apply(dataModel)).get();
-    }
-
-    /**
-     * Initializes mock data for the test.
-     */
-    public void initializeData(String resourceName) throws Exception {
-        BgDataModel bgDataModel = getBgDataModel();
-        AllAppsList allAppsList = getAllAppsList();
-
-        MODEL_EXECUTOR.submit(() -> {
-            // Copy apk from resources to a local file and install from there.
-            Resources resources = testContext().getResources();
-            int resId = resources.getIdentifier(
-                    resourceName, "raw", testContext().getPackageName());
-            try (BufferedReader reader = new BufferedReader(new InputStreamReader(
-                    resources.openRawResource(resId)))) {
-                String line;
-                HashMap<String, Class> classMap = new HashMap<>();
-                while ((line = reader.readLine()) != null) {
-                    line = line.trim();
-                    if (line.startsWith("#") || line.isEmpty()) {
-                        continue;
-                    }
-                    String[] commands = line.split(" ");
-                    switch (commands[0]) {
-                        case "classMap":
-                            classMap.put(commands[1], Class.forName(commands[2]));
-                            break;
-                        case "bgItem":
-                            bgDataModel.addItem(sandboxContext,
-                                    (ItemInfo) initItem(classMap.get(commands[1]), commands, 2),
-                                    false);
-                            break;
-                        case "allApps":
-                            allAppsList.add((AppInfo) initItem(AppInfo.class, commands, 1), null);
-                            break;
-                    }
-                }
-            } catch (Exception e) {
-                throw new RuntimeException(e);
-            }
-        }).get();
-    }
-
-    private Object initItem(Class clazz, String[] fieldDef, int startIndex) throws Exception {
-        HashMap<String, Field> cache = mFieldCache.get(clazz);
-        if (cache == null) {
-            cache = new HashMap<>();
-            Class c = clazz;
-            while (c != null) {
-                for (Field f : c.getDeclaredFields()) {
-                    f.setAccessible(true);
-                    cache.put(f.getName(), f);
-                }
-                c = c.getSuperclass();
-            }
-            mFieldCache.put(clazz, cache);
-        }
-
-        Object item = clazz.newInstance();
-        for (int i = startIndex; i < fieldDef.length; i++) {
-            String[] fieldData = fieldDef[i].split("=", 2);
-            Field f = cache.get(fieldData[0]);
-            Class type = f.getType();
-            if (type == int.class || type == long.class) {
-                f.set(item, Integer.parseInt(fieldData[1]));
-            } else if (type == CharSequence.class || type == String.class) {
-                f.set(item, fieldData[1]);
-            } else if (type == Intent.class) {
-                if (!fieldData[1].startsWith("#Intent")) {
-                    fieldData[1] = "#Intent;" + fieldData[1] + ";end";
-                }
-                f.set(item, Intent.parseUri(fieldData[1], 0));
-            } else if (type == ComponentName.class) {
-                f.set(item, ComponentName.unflattenFromString(fieldData[1]));
-            } else {
-                throw new Exception("Added parsing logic for "
-                        + f.getName() + " of type " + f.getType());
-            }
-        }
-        return item;
-    }
-
-    public int addItem(int type, int screen, int container, int x, int y) {
-        return addItem(type, screen, container, x, y, defaultProfileId, TEST_PACKAGE);
-    }
-
-    public int addItem(int type, int screen, int container, int x, int y, long profileId) {
-        return addItem(type, screen, container, x, y, profileId, TEST_PACKAGE);
-    }
-
-    public int addItem(int type, int screen, int container, int x, int y, String packageName) {
-        return addItem(type, screen, container, x, y, defaultProfileId, packageName);
-    }
-
-    public int addItem(int type, int screen, int container, int x, int y, String packageName,
-            int id, Uri contentUri) {
-        addItem(type, screen, container, x, y, defaultProfileId, packageName, id, contentUri);
-        return id;
-    }
-
-    /**
-     * Adds a mock item in the DB.
-     * @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for
-     *             folder (where the type represents the number of items in the folder).
-     */
-    public int addItem(int type, int screen, int container, int x, int y, long profileId,
-            String packageName) {
-        int id = LauncherSettings.Settings.call(sandboxContext.getContentResolver(),
-                LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
-                .getInt(LauncherSettings.Settings.EXTRA_VALUE);
-        addItem(type, screen, container, x, y, profileId, packageName, id, CONTENT_URI);
-        return id;
-    }
-
-    public void addItem(int type, int screen, int container, int x, int y, long profileId,
-            String packageName, int id, Uri contentUri) {
-        ContentValues values = new ContentValues();
-        values.put(LauncherSettings.Favorites._ID, id);
-        values.put(LauncherSettings.Favorites.CONTAINER, container);
-        values.put(LauncherSettings.Favorites.SCREEN, screen);
-        values.put(LauncherSettings.Favorites.CELLX, x);
-        values.put(LauncherSettings.Favorites.CELLY, y);
-        values.put(LauncherSettings.Favorites.SPANX, 1);
-        values.put(LauncherSettings.Favorites.SPANY, 1);
-        values.put(LauncherSettings.Favorites.PROFILE_ID, profileId);
-
-        if (type == APP_ICON || type == SHORTCUT) {
-            values.put(LauncherSettings.Favorites.ITEM_TYPE, type);
-            values.put(LauncherSettings.Favorites.INTENT,
-                    new Intent(Intent.ACTION_MAIN).setPackage(packageName).toUri(0));
-        } else {
-            values.put(LauncherSettings.Favorites.ITEM_TYPE,
-                    LauncherSettings.Favorites.ITEM_TYPE_FOLDER);
-            // Add folder items.
-            for (int i = 0; i < type; i++) {
-                addItem(APP_ICON, 0, id, 0, 0, profileId);
-            }
-        }
-
-        sandboxContext.getContentResolver().insert(contentUri, values);
-    }
-
-    /**
      * Sets up a mock provider to load the provided layout by default, next time the layout loads
      */
     public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder)
@@ -394,6 +197,9 @@
             }
         };
         setupProvider(TEST_PROVIDER_AUTHORITY, cp);
+        mDestroyTask.add(() -> runOnExecutorSync(MODEL_EXECUTOR, () ->
+                UiDevice.getInstance(getInstrumentation()).executeShellCommand(
+                        "settings delete secure launcher3.layout.provider")));
         return this;
     }
 
@@ -402,46 +208,16 @@
      */
     public void loadModelSync() throws ExecutionException, InterruptedException {
         Callbacks mockCb = new Callbacks() { };
-        Executors.MAIN_EXECUTOR.submit(() -> getModel().addCallbacksAndLoad(mockCb)).get();
+        MAIN_EXECUTOR.submit(() -> getModel().addCallbacksAndLoad(mockCb)).get();
 
         Executors.MODEL_EXECUTOR.submit(() -> { }).get();
-        Executors.MAIN_EXECUTOR.submit(() -> { }).get();
-        Executors.MAIN_EXECUTOR.submit(() -> getModel().removeCallbacks(mockCb)).get();
+        MAIN_EXECUTOR.submit(() -> { }).get();
+        MAIN_EXECUTOR.submit(() -> getModel().removeCallbacks(mockCb)).get();
     }
 
-    /**
-     * An extension of LauncherProvider backed up by in-memory database.
-     */
-    public static class TestLauncherProvider extends LauncherProvider {
+    public static class SandboxModelContext extends SandboxContext {
 
-        @Override
-        public boolean onCreate() {
-            return true;
-        }
-
-        public SQLiteDatabase getDb() {
-            return getModelDbController().getDb();
-        }
-    }
-
-    public static boolean deleteContents(File dir) {
-        File[] files = dir.listFiles();
-        boolean success = true;
-        if (files != null) {
-            for (File file : files) {
-                if (file.isDirectory()) {
-                    success &= deleteContents(file);
-                }
-                if (!file.delete()) {
-                    success = false;
-                }
-            }
-        }
-        return success;
-    }
-
-    public class SandboxModelContext extends SandboxContext {
-
+        private final MockContentResolver mMockResolver = new MockContentResolver();
         private final ArrayMap<String, Object> mSpiedServices = new ArrayMap<>();
         private final PackageManager mPm;
         private final File mDbDir;
@@ -451,12 +227,30 @@
                     UserCache.INSTANCE, InstallSessionHelper.INSTANCE, LauncherPrefs.INSTANCE,
                     LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE,
                     DisplayController.INSTANCE, CustomWidgetManager.INSTANCE,
-                    SettingsCache.INSTANCE, PluginManagerWrapper.INSTANCE, LockedUserState.INSTANCE,
+                    SettingsCache.INSTANCE, PluginManagerWrapper.INSTANCE,
+                    LockedUserState.INSTANCE, WallpaperColorHints.INSTANCE,
                     ItemInstallQueue.INSTANCE, WindowManagerProxy.INSTANCE);
+
+            // System settings cache content provider. Ensure that they are statically initialized
+            Settings.Secure.getString(
+                    ApplicationProvider.getApplicationContext().getContentResolver(), "test");
+            Settings.System.getString(
+                    ApplicationProvider.getApplicationContext().getContentResolver(), "test");
+            Settings.Global.getString(
+                    ApplicationProvider.getApplicationContext().getContentResolver(), "test");
+
             mPm = spy(getBaseContext().getPackageManager());
             mDbDir = new File(getCacheDir(), UUID.randomUUID().toString());
         }
 
+        @Override
+        protected <T> T createObject(MainThreadInitializedObject<T> object) {
+            if (object == LauncherAppState.INSTANCE) {
+                return (T) new LauncherAppState(this, null /* iconCacheFileName */);
+            }
+            return super.createObject(object);
+        }
+
         public SandboxModelContext allow(MainThreadInitializedObject object) {
             mAllowedObjects.add(object);
             return this;
@@ -505,9 +299,30 @@
             mSpiedServices.put(name, result);
             return result;
         }
-    }
 
-    private static Context testContext() {
-        return getInstrumentation().getContext();
+        public void setupProvider(String authority, ContentProvider provider) {
+            ProviderInfo providerInfo = new ProviderInfo();
+            providerInfo.authority = authority;
+            providerInfo.applicationInfo = getApplicationInfo();
+            provider.attachInfo(this, providerInfo);
+            mMockResolver.addProvider(providerInfo.authority, provider);
+            doReturn(providerInfo).when(mPm).resolveContentProvider(eq(authority), anyInt());
+        }
+
+        private static boolean deleteContents(File dir) {
+            File[] files = dir.listFiles();
+            boolean success = true;
+            if (files != null) {
+                for (File file : files) {
+                    if (file.isDirectory()) {
+                        success &= deleteContents(file);
+                    }
+                    if (!file.delete()) {
+                        success = false;
+                    }
+                }
+            }
+            return success;
+        }
     }
 }
diff --git a/tests/src/com/android/launcher3/util/LockedUserStateTest.kt b/tests/src/com/android/launcher3/util/LockedUserStateTest.kt
index 84156e7..92ab2cb 100644
--- a/tests/src/com/android/launcher3/util/LockedUserStateTest.kt
+++ b/tests/src/com/android/launcher3/util/LockedUserStateTest.kt
@@ -32,7 +32,7 @@
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
-/** Unit tests for {@link LockedUserUtil} */
+/** Unit tests for {@link LockedUserState} */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class LockedUserStateTest {
@@ -49,40 +49,31 @@
     @Test
     fun runOnUserUnlocked_runs_action_immediately_if_already_unlocked() {
         `when`(userManager.isUserUnlocked(Process.myUserHandle())).thenReturn(true)
-        LockedUserState.INSTANCE.initializeForTesting(LockedUserState(context))
         val action: Runnable = mock()
-
-        LockedUserState.get(context).runOnUserUnlocked(action)
+        LockedUserState(context).runOnUserUnlocked(action)
         verify(action).run()
     }
 
     @Test
     fun runOnUserUnlocked_waits_to_run_action_until_user_is_unlocked() {
         `when`(userManager.isUserUnlocked(Process.myUserHandle())).thenReturn(false)
-        LockedUserState.INSTANCE.initializeForTesting(LockedUserState(context))
         val action: Runnable = mock()
-
-        LockedUserState.get(context).runOnUserUnlocked(action)
+        val state = LockedUserState(context)
+        state.runOnUserUnlocked(action)
         verifyZeroInteractions(action)
-
-        LockedUserState.get(context)
-            .mUserUnlockedReceiver
-            .onReceive(context, Intent(Intent.ACTION_USER_UNLOCKED))
-
+        state.mUserUnlockedReceiver.onReceive(context, Intent(Intent.ACTION_USER_UNLOCKED))
         verify(action).run()
     }
 
     @Test
     fun isUserUnlocked_returns_true_when_user_is_unlocked() {
         `when`(userManager.isUserUnlocked(Process.myUserHandle())).thenReturn(true)
-        LockedUserState.INSTANCE.initializeForTesting(LockedUserState(context))
-        assertThat(LockedUserState.get(context).isUserUnlocked).isTrue()
+        assertThat(LockedUserState(context).isUserUnlocked).isTrue()
     }
 
     @Test
     fun isUserUnlocked_returns_false_when_user_is_locked() {
         `when`(userManager.isUserUnlocked(Process.myUserHandle())).thenReturn(false)
-        LockedUserState.INSTANCE.initializeForTesting(LockedUserState(context))
-        assertThat(LockedUserState.get(context).isUserUnlocked).isFalse()
+        assertThat(LockedUserState(context).isUserUnlocked).isFalse()
     }
 }
diff --git a/tests/src/com/android/launcher3/util/RaceConditionReproducer.java b/tests/src/com/android/launcher3/util/RaceConditionReproducer.java
deleted file mode 100644
index ed2ec7b..0000000
--- a/tests/src/com/android/launcher3/util/RaceConditionReproducer.java
+++ /dev/null
@@ -1,500 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util;
-
-import static com.android.launcher3.util.Executors.createAndStartNewLooper;
-
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.os.Handler;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Event processor for reliably reproducing multithreaded apps race conditions in tests.
- *
- * The app notifies us about “events” that happen in its threads. The race condition test runs the
- * test action multiple times (aka iterations), trying to generate all possible permutations of
- * these events. It keeps a set of all seen event sequences and steers the execution towards
- * executing events in previously unseen order. It does it by postponing execution of threads that
- * would lead to an already seen sequence.
- *
- * If an event A occurs before event B in the sequence, this is how execution order looks like:
- * Events: ... A ... B ...
- * Events and instructions, guaranteed order:
- * (instructions executed prior to A) A ... B (instructions executed after B)
- *
- * Each iteration has 3 parts (phases).
- * Phase 1. Picking a previously seen event subsequence that we believe can have previously unseen
- * continuations. Reproducing this sequence by pausing threads that would lead to other sequences.
- * Phase 2. Trying to generate previously unseen continuation of the sequence from Phase 1. We need
- * one new event after that sequence. All threads leading to seen continuations will be postponed
- * for some short period of time. The phase ends once the new event is registered, or after the
- * period of time ends (in which case we declare that the sequence can’t have new continuations).
- * Phase 3. Releasing all threads and letting the test iteration run till its end.
- *
- * The iterations end when all seen paths have been declared “uncontinuable”.
- *
- * When we register event XXX:enter, we hold all other events until we register XXX:exit.
- */
-public class RaceConditionReproducer {
-    private static final String TAG = "RaceConditionReproducer";
-
-    private static final boolean ENTER = true;
-    private static final boolean EXIT = false;
-    private static final String ENTER_POSTFIX = "enter";
-    private static final String EXIT_POSTFIX = "exit";
-
-    private static final long SHORT_TIMEOUT_MS = 2000;
-    private static final long LONG_TIMEOUT_MS = 60000;
-    // Handler used to resume postponed events.
-    private static final Handler POSTPONED_EVENT_RESUME_HANDLER =
-            new Handler(createAndStartNewLooper("RaceConditionEventResumer"));
-
-    public static String enterExitEvt(String eventName, boolean isEnter) {
-        return eventName + ":" + (isEnter ? ENTER_POSTFIX : EXIT_POSTFIX);
-    }
-
-    public static String enterEvt(String eventName) {
-        return enterExitEvt(eventName, ENTER);
-    }
-
-    public static String exitEvt(String eventName) {
-        return enterExitEvt(eventName, EXIT);
-    }
-
-    /**
-     * Event in a particular sequence of events. A node in the prefix tree of all seen event
-     * sequences.
-     */
-    private class EventNode {
-        // Events that were seen just after this event.
-        private final Map<String, EventNode> mNextEvents = new HashMap<>();
-        // Whether we believe that further iterations will not be able to add more events to
-        // mNextEvents.
-        private boolean mStoppedAddingChildren = true;
-
-        private void debugDump(StringBuilder sb, int indent, String name) {
-            for (int i = 0; i < indent; ++i) sb.append('.');
-            sb.append(!mStoppedAddingChildren ? "+" : "-");
-            sb.append(" : ");
-            sb.append(name);
-            if (mLastRegisteredEvent == this) sb.append(" <");
-            sb.append('\n');
-
-            for (String key : mNextEvents.keySet()) {
-                mNextEvents.get(key).debugDump(sb, indent + 2, key);
-            }
-        }
-
-        /** Number of leaves in the subtree with this node as a root. */
-        private int numberOfLeafNodes() {
-            if (mNextEvents.isEmpty()) return 1;
-
-            int leaves = 0;
-            for (String event : mNextEvents.keySet()) {
-                leaves += mNextEvents.get(event).numberOfLeafNodes();
-            }
-            return leaves;
-        }
-
-        /**
-         * Whether we believe that further iterations will not be able add nodes to the subtree with
-         * this node as a root.
-         */
-        private boolean stoppedAddingChildrenToTree() {
-            if (!mStoppedAddingChildren) return false;
-
-            for (String event : mNextEvents.keySet()) {
-                if (!mNextEvents.get(event).stoppedAddingChildrenToTree()) return false;
-            }
-            return true;
-        }
-
-        /**
-         * In the subtree with this node as a root, tries finding a node where we may have a
-         * chance to add new children.
-         * If succeeds, returns true and fills 'path' with the sequence of events to that node;
-         * otherwise returns false.
-         */
-        private boolean populatePathToGrowthPoint(List<String> path) {
-            for (String event : mNextEvents.keySet()) {
-                if (mNextEvents.get(event).populatePathToGrowthPoint(path)) {
-                    path.add(0, event);
-                    return true;
-                }
-            }
-            if (!mStoppedAddingChildren) {
-                // Mark that we have finished adding children. It will remain true if no new
-                // children are added, or will be set to false upon adding a new child.
-                mStoppedAddingChildren = true;
-                return true;
-            }
-            return false;
-        }
-    }
-
-    // Starting point of all event sequences; the root of the prefix tree representation all
-    // sequences generated by test iterations. A test iteration can add nodes int it.
-    private EventNode mRoot = new EventNode();
-    // During a test iteration, the last event that was registered.
-    private EventNode mLastRegisteredEvent;
-    // Length of the current sequence of registered events for the current test iteration.
-    private int mRegisteredEventCount = 0;
-    // During the first part of a test iteration, we go to a specific node under mRoot by
-    // 'playing back' mSequenceToFollow. During this part, all events that don't belong to this
-    // sequence get postponed.
-    private List<String> mSequenceToFollow = new ArrayList<>();
-    // Collection of events that got postponed, with corresponding wait objects used to let them go.
-    private Map<String, Semaphore> mPostponedEvents = new HashMap<>();
-    // Callback to run by POSTPONED_EVENT_RESUME_HANDLER, used to let go of all currently
-    // postponed events.
-    private Runnable mResumeAllEventsCallback;
-    // String representation of the sequence of events registered so far for the current test
-    // iteration. After registering any event, we output it to the log. The last output before
-    // the test failure can be later played back to reliable reproduce the exact sequence of
-    // events that broke the test.
-    // Format: EV1|EV2|...\EVN
-    private StringBuilder mCurrentSequence;
-    // When not null, we are in a repro mode. We run only one test iteration, and are trying to
-    // reproduce the event sequence represented by this string. The format is same as for
-    // mCurrentSequence.
-    private final String mReproString;
-
-    /* Constructor for a normal test. */
-    public RaceConditionReproducer() {
-        mReproString = null;
-    }
-
-    /**
-     * Constructor for reliably reproducing a race condition failure. The developer should find in
-     * the log the latest "Repro sequence:" record and locally modify the test by passing that
-     * string to the constructor. Running the test will have only one iteration that will reliably
-     * "play back" that sequence.
-     */
-    public RaceConditionReproducer(String reproString) {
-        mReproString = reproString;
-    }
-
-    public RaceConditionReproducer(String... reproSequence) {
-        this(String.join("|", reproSequence));
-    }
-
-    public synchronized String getCurrentSequenceString() {
-        return mCurrentSequence.toString();
-    }
-
-    /**
-     * Starts a new test iteration. Events reported via RaceConditionTracker.onEvent before this
-     * call will be ignored.
-     */
-    public synchronized void startIteration() {
-        mLastRegisteredEvent = mRoot;
-        mRegisteredEventCount = 0;
-        mCurrentSequence = new StringBuilder();
-        Log.d(TAG, "Repro sequence: " + mCurrentSequence);
-        mSequenceToFollow = mReproString != null ?
-                parseReproString(mReproString) : generateSequenceToFollowLocked();
-        Log.e(TAG, "---- Start of iteration; state:\n" + dumpStateLocked());
-        checkIfCompletedSequenceToFollowLocked();
-
-        TraceHelperForTest.setRaceConditionReproducer(this);
-    }
-
-    /**
-     * Ends a new test iteration. Events reported via RaceConditionTracker.onEvent after this call
-     * will be ignored.
-     * Returns whether we need more iterations.
-     */
-    public synchronized boolean finishIteration() {
-        TraceHelperForTest.setRaceConditionReproducer(null);
-
-        runResumeAllEventsCallbackLocked();
-        assertTrue("Non-empty postponed events", mPostponedEvents.isEmpty());
-        assertTrue("Last registered event is :enter", lastEventAsEnter() == null);
-
-        // No events came after mLastRegisteredEvent. It doesn't make sense to come to it again
-        // because we won't see new continuations.
-        mLastRegisteredEvent.mStoppedAddingChildren = true;
-        Log.e(TAG, "---- End of iteration; state:\n" + dumpStateLocked());
-        if (mReproString != null) {
-            assertTrue("Repro mode: failed to reproduce the sequence",
-                    mCurrentSequence.toString().startsWith(mReproString));
-        }
-        // If we are in a repro mode, we need only one iteration. Otherwise, continue if the tree
-        // has prospective growth points.
-        return mReproString == null && !mRoot.stoppedAddingChildrenToTree();
-    }
-
-    private static List<String> parseReproString(String reproString) {
-        return Arrays.asList(reproString.split("\\|"));
-    }
-
-    /**
-     * Called when the app issues an event.
-     */
-    public void onEvent(String event) {
-        final Semaphore waitObject = tryRegisterEvent(event);
-        if (waitObject != null) {
-            waitUntilCanRegister(event, waitObject);
-        }
-    }
-
-    /**
-     * Returns whether the last event was not an XXX:enter, or this event is a matching XXX:exit.
-     */
-    private boolean canRegisterEventNowLocked(String event) {
-        final String lastEventAsEnter = lastEventAsEnter();
-        final String thisEventAsExit = eventAsExit(event);
-
-        if (lastEventAsEnter != null) {
-            if (!lastEventAsEnter.equals(thisEventAsExit)) {
-                assertTrue("YYY:exit after XXX:enter", thisEventAsExit == null);
-                // Last event was :enter, but this event is not :exit.
-                return false;
-            }
-        } else {
-            // Previous event was not :enter.
-            assertTrue(":exit after a non-enter event", thisEventAsExit == null);
-        }
-        return true;
-    }
-
-    /**
-     * Registers an event issued by the app and returns null or decides that the event must be
-     * postponed, and returns an object to wait on.
-     */
-    private synchronized Semaphore tryRegisterEvent(String event) {
-        Log.d(TAG, "Event issued by the app: " + event);
-
-        if (!canRegisterEventNowLocked(event)) {
-            return createWaitObjectForPostponedEventLocked(event);
-        }
-
-        if (mRegisteredEventCount < mSequenceToFollow.size()) {
-            // We are in the first part of the iteration. We only register events that follow the
-            // mSequenceToFollow and postponing all other events.
-            if (event.equals(mSequenceToFollow.get(mRegisteredEventCount))) {
-                // The event is the next one expected in the sequence. Register it.
-                registerEventLocked(event);
-
-                // If there are postponed events that could continue the sequence, register them.
-                while (mRegisteredEventCount < mSequenceToFollow.size() &&
-                        mPostponedEvents.containsKey(
-                                mSequenceToFollow.get(mRegisteredEventCount))) {
-                    registerPostponedEventLocked(mSequenceToFollow.get(mRegisteredEventCount));
-                }
-
-                // Perhaps we just completed the required sequence...
-                checkIfCompletedSequenceToFollowLocked();
-            } else {
-                // The event is not the next one in the sequence. Postpone it.
-                return createWaitObjectForPostponedEventLocked(event);
-            }
-        } else if (mRegisteredEventCount == mSequenceToFollow.size()) {
-            // The second phase of the iteration. We have just registered the whole
-            // mSequenceToFollow, and want to add previously not seen continuations for the last
-            // node in the sequence aka 'growth point'.
-            if (!mLastRegisteredEvent.mNextEvents.containsKey(event) || mReproString != null) {
-                // The event was never seen as a continuation for the current node.
-                // Or we are in repro mode, in which case we are not in business of generating
-                // new sequences after we've played back the required sequence.
-                // Register it immediately.
-                registerEventLocked(event);
-            } else {
-                // The event was seen as a continuation for the current node. Postpone it, hoping
-                // that a new event will come from other threads.
-                return createWaitObjectForPostponedEventLocked(event);
-            }
-        } else {
-            // The third phase of the iteration. We are past the growth point and register
-            // everything that comes.
-            registerEventLocked(event);
-            // Register events that may have been postponed while waiting for an :exit event
-            // during the third phase. We don't do this if just registered event is :enter.
-            if (eventAsEnter(event) == null && mRegisteredEventCount > mSequenceToFollow.size()) {
-                registerPostponedEventsLocked(new HashSet<>(mPostponedEvents.keySet()));
-            }
-        }
-        return null;
-    }
-
-    /** Called when there are chances that we just have registered the whole mSequenceToFollow. */
-    private void checkIfCompletedSequenceToFollowLocked() {
-        if (mRegisteredEventCount == mSequenceToFollow.size()) {
-            // We just entered the second phase of the iteration. We have just registered the
-            // whole mSequenceToFollow, and want to add previously not seen continuations for the
-            // last node in the sequence aka 'growth point'. All seen continuations will be
-            // postponed for SHORT_TIMEOUT_MS. At the end of this time period, we'll let them go.
-            scheduleResumeAllEventsLocked();
-
-            // Among the events that were postponed during the first stage, there may be an event
-            // that wasn't seen after the current. If so, register it immediately because this
-            // creates a new sequence.
-            final Set<String> keys = new HashSet<>(mPostponedEvents.keySet());
-            keys.removeAll(mLastRegisteredEvent.mNextEvents.keySet());
-            if (!keys.isEmpty()) {
-                registerPostponedEventLocked(keys.iterator().next());
-            }
-        }
-    }
-
-    private Semaphore createWaitObjectForPostponedEventLocked(String event) {
-        final Semaphore waitObject = new Semaphore(0);
-        assertTrue("Event already postponed: " + event, !mPostponedEvents.containsKey(event));
-        mPostponedEvents.put(event, waitObject);
-        return waitObject;
-    }
-
-    private void waitUntilCanRegister(String event, Semaphore waitObject) {
-        try {
-            assertTrue("Never registered event: " + event,
-                    waitObject.tryAcquire(LONG_TIMEOUT_MS, TimeUnit.MILLISECONDS));
-        } catch (InterruptedException e) {
-            fail("Wait was interrupted");
-        }
-    }
-
-    /** Schedules resuming all postponed events after SHORT_TIMEOUT_MS */
-    private void scheduleResumeAllEventsLocked() {
-        assertTrue(mResumeAllEventsCallback == null);
-        mResumeAllEventsCallback = this::allEventsResumeCallback;
-        POSTPONED_EVENT_RESUME_HANDLER.postDelayed(mResumeAllEventsCallback, SHORT_TIMEOUT_MS);
-    }
-
-    private synchronized void allEventsResumeCallback() {
-        assertTrue("In callback, but callback is not set", mResumeAllEventsCallback != null);
-        mResumeAllEventsCallback = null;
-        registerPostponedEventsLocked(new HashSet<>(mPostponedEvents.keySet()));
-    }
-
-    private void registerPostponedEventsLocked(Collection<String> events) {
-        for (String event : events) {
-            registerPostponedEventLocked(event);
-            if (eventAsEnter(event) != null) {
-                // Once :enter is registered, switch to waiting for :exit to come. Won't register
-                // other postponed events.
-                break;
-            }
-        }
-    }
-
-    private void registerPostponedEventLocked(String event) {
-        mPostponedEvents.remove(event).release();
-        registerEventLocked(event);
-    }
-
-    /**
-     * If the last registered event was XXX:enter, returns XXX, otherwise, null.
-     */
-    private String lastEventAsEnter() {
-        return eventAsEnter(mCurrentSequence.substring(mCurrentSequence.lastIndexOf("|") + 1));
-    }
-
-    /**
-     * If the event is XXX:postfix, returns XXX, otherwise, null.
-     */
-    private static String prefixFromPostfixedEvent(String event, String postfix) {
-        final int columnPos = event.indexOf(':');
-        if (columnPos != -1 && postfix.equals(event.substring(columnPos + 1))) {
-            return event.substring(0, columnPos);
-        }
-        return null;
-    }
-
-    /**
-     * If the event is XXX:enter, returns XXX, otherwise, null.
-     */
-    private static String eventAsEnter(String event) {
-        return prefixFromPostfixedEvent(event, ENTER_POSTFIX);
-    }
-
-    /**
-     * If the event is XXX:exit, returns XXX, otherwise, null.
-     */
-    private static String eventAsExit(String event) {
-        return prefixFromPostfixedEvent(event, EXIT_POSTFIX);
-    }
-
-    private void registerEventLocked(String event) {
-        assertTrue(canRegisterEventNowLocked(event));
-
-        Log.d(TAG, "Actually registering event: " + event);
-        EventNode next = mLastRegisteredEvent.mNextEvents.get(event);
-        if (next == null) {
-            // This event wasn't seen after mLastRegisteredEvent.
-            next = new EventNode();
-            mLastRegisteredEvent.mNextEvents.put(event, next);
-            // The fact that we've added a new event after the previous one means that the
-            // previous event is still a growth point, unless this event is :exit, which means
-            // that the previous event is :enter.
-            mLastRegisteredEvent.mStoppedAddingChildren = eventAsExit(event) != null;
-        }
-
-        mLastRegisteredEvent = next;
-        mRegisteredEventCount++;
-
-        if (mCurrentSequence.length() > 0) mCurrentSequence.append("|");
-        mCurrentSequence.append(event);
-        Log.d(TAG, "Repro sequence: " + mCurrentSequence);
-    }
-
-    private void runResumeAllEventsCallbackLocked() {
-        if (mResumeAllEventsCallback != null) {
-            POSTPONED_EVENT_RESUME_HANDLER.removeCallbacks(mResumeAllEventsCallback);
-            mResumeAllEventsCallback.run();
-        }
-    }
-
-    private CharSequence dumpStateLocked() {
-        StringBuilder sb = new StringBuilder();
-
-        sb.append("Sequence to follow: ");
-        for (String event : mSequenceToFollow) sb.append(" " + event);
-        sb.append(".\n");
-        sb.append("Registered event count: " + mRegisteredEventCount);
-
-        sb.append("\nPostponed events: ");
-        for (String event : mPostponedEvents.keySet()) sb.append(" " + event);
-        sb.append(".");
-
-        sb.append("\nNodes: \n");
-        mRoot.debugDump(sb, 0, "");
-        return sb;
-    }
-
-    public int numberOfLeafNodes() {
-        return mRoot.numberOfLeafNodes();
-    }
-
-    private List<String> generateSequenceToFollowLocked() {
-        ArrayList<String> sequence = new ArrayList<>();
-        mRoot.populatePathToGrowthPoint(sequence);
-        return sequence;
-    }
-}
diff --git a/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java b/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java
deleted file mode 100644
index 59f2173..0000000
--- a/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class RaceConditionReproducerTest {
-    private final static String SOME_VALID_SEQUENCE_3_3 = "B1|A1|A2|B2|A3|B3";
-
-    private static int factorial(int n) {
-        int res = 1;
-        for (int i = 2; i <= n; ++i) res *= i;
-        return res;
-    }
-
-    RaceConditionReproducer eventProcessor;
-
-    @Before
-    public void setup() {
-        eventProcessor = new RaceConditionReproducer();
-    }
-
-    @After
-    public void tearDown() {
-        TraceHelperForTest.cleanup();
-    }
-
-    private void run3_3_TestAction() throws InterruptedException {
-        Thread tb = new Thread(() -> {
-            eventProcessor.onEvent("B1");
-            eventProcessor.onEvent("B2");
-            eventProcessor.onEvent("B3");
-        });
-        tb.start();
-
-        eventProcessor.onEvent("A1");
-        eventProcessor.onEvent("A2");
-        eventProcessor.onEvent("A3");
-
-        tb.join();
-    }
-
-    @Test
-    @Ignore // The test is too long for continuous testing.
-    // 2 threads, 3 events each.
-    public void test3_3() throws Exception {
-        boolean sawTheValidSequence = false;
-
-        for (; ; ) {
-            eventProcessor.startIteration();
-            run3_3_TestAction();
-            final boolean needMoreIterations = eventProcessor.finishIteration();
-
-            sawTheValidSequence = sawTheValidSequence ||
-                    SOME_VALID_SEQUENCE_3_3.equals(eventProcessor.getCurrentSequenceString());
-
-            if (!needMoreIterations) break;
-        }
-
-        assertEquals("Wrong number of leaf nodes",
-                factorial(3 + 3) / (factorial(3) * factorial(3)),
-                eventProcessor.numberOfLeafNodes());
-        assertTrue(sawTheValidSequence);
-    }
-
-    @Test
-    @Ignore // The test is too long for continuous testing.
-    // 2 threads, 3 events, including enter-exit pairs each.
-    public void test3_3_enter_exit() throws Exception {
-        boolean sawTheValidSequence = false;
-
-        for (; ; ) {
-            eventProcessor.startIteration();
-            Thread tb = new Thread(() -> {
-                eventProcessor.onEvent("B1:enter");
-                eventProcessor.onEvent("B1:exit");
-                eventProcessor.onEvent("B2");
-                eventProcessor.onEvent("B3:enter");
-                eventProcessor.onEvent("B3:exit");
-            });
-            tb.start();
-
-            eventProcessor.onEvent("A1");
-            eventProcessor.onEvent("A2:enter");
-            eventProcessor.onEvent("A2:exit");
-            eventProcessor.onEvent("A3:enter");
-            eventProcessor.onEvent("A3:exit");
-
-            tb.join();
-            final boolean needMoreIterations = eventProcessor.finishIteration();
-
-            sawTheValidSequence = sawTheValidSequence ||
-                    "B1:enter|B1:exit|A1|A2:enter|A2:exit|B2|A3:enter|A3:exit|B3:enter|B3:exit".
-                            equals(eventProcessor.getCurrentSequenceString());
-
-            if (!needMoreIterations) break;
-        }
-
-        assertEquals("Wrong number of leaf nodes",
-                factorial(3 + 3) / (factorial(3) * factorial(3)),
-                eventProcessor.numberOfLeafNodes());
-        assertTrue(sawTheValidSequence);
-    }
-
-    @Test
-    // 2 threads, 3 events each; reproducing a particular event sequence.
-    public void test3_3_ReproMode() throws Exception {
-        eventProcessor = new RaceConditionReproducer(SOME_VALID_SEQUENCE_3_3);
-        eventProcessor.startIteration();
-        run3_3_TestAction();
-        assertTrue(!eventProcessor.finishIteration());
-        assertEquals(SOME_VALID_SEQUENCE_3_3, eventProcessor.getCurrentSequenceString());
-
-        assertEquals("Wrong number of leaf nodes", 1, eventProcessor.numberOfLeafNodes());
-    }
-
-    @Test
-    @Ignore // The test is too long for continuous testing.
-    // 2 threads with 2 events; 1 thread with 1 event.
-    public void test2_1_2() throws Exception {
-        for (; ; ) {
-            eventProcessor.startIteration();
-            Thread tb = new Thread(() -> {
-                eventProcessor.onEvent("B1");
-                eventProcessor.onEvent("B2");
-            });
-            tb.start();
-
-            Thread tc = new Thread(() -> {
-                eventProcessor.onEvent("C1");
-            });
-            tc.start();
-
-            eventProcessor.onEvent("A1");
-            eventProcessor.onEvent("A2");
-
-            tb.join();
-            tc.join();
-
-            if (!eventProcessor.finishIteration()) break;
-        }
-
-        assertEquals("Wrong number of leaf nodes",
-                factorial(2 + 2 + 1) / (factorial(2) * factorial(2) * factorial(1)),
-                eventProcessor.numberOfLeafNodes());
-    }
-
-    @Test
-    @Ignore // The test is too long for continuous testing.
-    // 2 threads with 2 events; 1 thread with 1 event. Includes enter-exit pairs.
-    public void test2_1_2_enter_exit() throws Exception {
-        for (; ; ) {
-            eventProcessor.startIteration();
-            Thread tb = new Thread(() -> {
-                eventProcessor.onEvent("B1:enter");
-                eventProcessor.onEvent("B1:exit");
-                eventProcessor.onEvent("B2:enter");
-                eventProcessor.onEvent("B2:exit");
-            });
-            tb.start();
-
-            Thread tc = new Thread(() -> {
-                eventProcessor.onEvent("C1:enter");
-                eventProcessor.onEvent("C1:exit");
-            });
-            tc.start();
-
-            eventProcessor.onEvent("A1:enter");
-            eventProcessor.onEvent("A1:exit");
-            eventProcessor.onEvent("A2:enter");
-            eventProcessor.onEvent("A2:exit");
-
-            tb.join();
-            tc.join();
-
-            if (!eventProcessor.finishIteration()) break;
-        }
-
-        assertEquals("Wrong number of leaf nodes",
-                factorial(2 + 2 + 1) / (factorial(2) * factorial(2) * factorial(1)),
-                eventProcessor.numberOfLeafNodes());
-    }
-}
diff --git a/tests/src/com/android/launcher3/util/TestResourceHelper.kt b/tests/src/com/android/launcher3/util/TestResourceHelper.kt
index fb03fe1..cf80ece 100644
--- a/tests/src/com/android/launcher3/util/TestResourceHelper.kt
+++ b/tests/src/com/android/launcher3/util/TestResourceHelper.kt
@@ -23,12 +23,18 @@
 import com.android.launcher3.tests.R as TestR
 import kotlin.IntArray
 
-class TestResourceHelper(private val context: Context, private val specsFileId: Int) :
+class TestResourceHelper(private val context: Context, specsFileId: Int) :
     ResourceHelper(context, specsFileId) {
     override fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray {
-        var clone = styleId.clone()
-        if (styleId == R.styleable.SpecSize) clone = TestR.styleable.SpecSize
-        else if (styleId == R.styleable.WorkspaceSpec) clone = TestR.styleable.WorkspaceSpec
+        val clone =
+            when {
+                styleId.contentEquals(R.styleable.SizeSpec) -> TestR.styleable.SizeSpec
+                styleId.contentEquals(R.styleable.WorkspaceSpec) -> TestR.styleable.WorkspaceSpec
+                styleId.contentEquals(R.styleable.FolderSpec) -> TestR.styleable.FolderSpec
+                styleId.contentEquals(R.styleable.AllAppsSpec) -> TestR.styleable.AllAppsSpec
+                else -> styleId.clone()
+            }
+
         return context.obtainStyledAttributes(attrs, clone)
     }
 }
diff --git a/tests/src/com/android/launcher3/util/TestUtil.java b/tests/src/com/android/launcher3/util/TestUtil.java
index f8cd995..21059e6 100644
--- a/tests/src/com/android/launcher3/util/TestUtil.java
+++ b/tests/src/com/android/launcher3/util/TestUtil.java
@@ -18,14 +18,13 @@
 import static android.util.Base64.NO_PADDING;
 import static android.util.Base64.NO_WRAP;
 
-import static androidx.test.InstrumentationRegistry.getContext;
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
-import static androidx.test.InstrumentationRegistry.getTargetContext;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG;
 
+import android.app.Instrumentation;
 import android.app.blob.BlobHandle;
 import android.app.blob.BlobStoreManager;
 import android.content.Context;
@@ -35,9 +34,12 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
+import android.os.Process;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.system.OsConstants;
 import android.util.Base64;
+import android.util.Log;
 
 import androidx.test.uiautomator.UiDevice;
 
@@ -52,26 +54,36 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.security.MessageDigest;
+import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.function.Predicate;
 import java.util.function.ToIntFunction;
 
 public class TestUtil {
+    private static final String TAG = "TestUtil";
+
     public static final String DUMMY_PACKAGE = "com.example.android.aardwolf";
-    public static final int DEFAULT_USER_ID = 0;
+    public static final String DUMMY_CLASS_NAME = "com.example.android.aardwolf.Activity1";
+    public static final long DEFAULT_UI_TIMEOUT = 10000;
 
     public static void installDummyApp() throws IOException {
-        installDummyAppForUser(DEFAULT_USER_ID);
+        final int defaultUserId = getMainUserId();
+        installDummyAppForUser(defaultUserId);
     }
 
     public static void installDummyAppForUser(int userId) throws IOException {
+        Instrumentation instrumentation = getInstrumentation();
         // Copy apk from resources to a local file and install from there.
-        final Resources resources = getContext().getResources();
+        final Resources resources = instrumentation.getContext().getResources();
         final InputStream in = resources.openRawResource(
                 resources.getIdentifier("aardwolf_dummy_app",
-                        "raw", getContext().getPackageName()));
-        final String apkFilename = getInstrumentation().getTargetContext().
-                getFilesDir().getPath() + "/dummy_app.apk";
+                        "raw", instrumentation.getContext().getPackageName()));
+        final String apkFilename = instrumentation.getTargetContext()
+                        .getFilesDir().getPath() + "/dummy_app.apk";
 
         try (PackageInstallCheck pic = new PackageInstallCheck()) {
             final FileOutputStream out = new FileOutputStream(apkFilename);
@@ -84,7 +96,7 @@
             in.close();
             out.close();
 
-            final String result = UiDevice.getInstance(getInstrumentation())
+            final String result = UiDevice.getInstance(instrumentation)
                     .executeShellCommand("pm install --user " + userId + " " + apkFilename);
             Assert.assertTrue(
                     "Failed to install wellbeing test apk; make sure the device is rooted",
@@ -96,6 +108,23 @@
     }
 
     /**
+     * Returns the main user ID. NOTE: For headless system it is NOT 0. Returns 0 by default, if
+     * there is no main user.
+     *
+     * @return a main user ID
+     */
+    public static int getMainUserId() throws IOException {
+        Instrumentation instrumentation = getInstrumentation();
+        final String result = UiDevice.getInstance(instrumentation)
+                .executeShellCommand("cmd user get-main-user");
+        try {
+            return Integer.parseInt(result.trim());
+        } catch (NumberFormatException e) {
+            return 0;
+        }
+    }
+
+    /**
      * Utility class to override a boolean flag during test. Note that the returned SafeCloseable
      * must be closed to restore the original state
      */
@@ -159,6 +188,50 @@
             Settings.Secure.putString(context.getContentResolver(), LAYOUT_DIGEST_KEY, null);
     }
 
+    /**
+     * Utility method to run a task synchronously which converts any exceptions to RuntimeException
+     */
+    public static void runOnExecutorSync(ExecutorService executor, UncheckedRunnable task) {
+        try {
+            executor.submit(() -> {
+                try {
+                    task.run();
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                }
+            }).get();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Runs the callback on the UI thread and returns the result.
+     */
+    public static <T> T getOnUiThread(final Callable<T> callback) {
+        try {
+            FutureTask<T> task = new FutureTask<>(callback);
+            if (Looper.myLooper() == Looper.getMainLooper()) {
+                task.run();
+            } else {
+                new Handler(Looper.getMainLooper()).post(task);
+            }
+            return task.get(DEFAULT_UI_TIMEOUT, TimeUnit.MILLISECONDS);
+        } catch (TimeoutException e) {
+            Log.e(TAG, "Timeout in getOnUiThread, sending SIGABRT", e);
+            Process.sendSignal(Process.myPid(), OsConstants.SIGABRT);
+            throw new RuntimeException(e);
+        } catch (Throwable e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /** Interface to indicate a runnable which can throw any exception. */
+    public interface UncheckedRunnable {
+        /** Method to run the task */
+        void run() throws Exception;
+    }
+
     private static class PackageInstallCheck extends LauncherApps.Callback
             implements AutoCloseable {
 
@@ -166,7 +239,8 @@
         final LauncherApps mLauncherApps;
 
         PackageInstallCheck() {
-            mLauncherApps = getTargetContext().getSystemService(LauncherApps.class);
+            mLauncherApps = getInstrumentation().getTargetContext()
+                    .getSystemService(LauncherApps.class);
             mLauncherApps.registerCallback(this, new Handler(Looper.getMainLooper()));
         }
 
diff --git a/tests/src/com/android/launcher3/util/TraceHelperForTest.java b/tests/src/com/android/launcher3/util/TraceHelperForTest.java
deleted file mode 100644
index f1c8a67..0000000
--- a/tests/src/com/android/launcher3/util/TraceHelperForTest.java
+++ /dev/null
@@ -1,116 +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 java.util.LinkedList;
-import java.util.function.IntConsumer;
-
-public class TraceHelperForTest extends TraceHelper {
-
-    private static final TraceHelperForTest INSTANCE_FOR_TEST = new TraceHelperForTest();
-
-    private final ThreadLocal<LinkedList<TraceInfo>> mStack =
-            ThreadLocal.withInitial(LinkedList::new);
-
-    private RaceConditionReproducer mRaceConditionReproducer;
-    private IntConsumer mFlagsChangeListener;
-
-    public static void setRaceConditionReproducer(RaceConditionReproducer reproducer) {
-        TraceHelper.INSTANCE = INSTANCE_FOR_TEST;
-        INSTANCE_FOR_TEST.mRaceConditionReproducer = reproducer;
-    }
-
-    public static void cleanup() {
-        INSTANCE_FOR_TEST.mRaceConditionReproducer = null;
-        INSTANCE_FOR_TEST.mFlagsChangeListener = null;
-    }
-
-    public static void setFlagsChangeListener(IntConsumer listener) {
-        TraceHelper.INSTANCE = INSTANCE_FOR_TEST;
-        INSTANCE_FOR_TEST.mFlagsChangeListener = listener;
-    }
-
-    private TraceHelperForTest() { }
-
-    @Override
-    public Object beginSection(String sectionName, int flags) {
-        LinkedList<TraceInfo> stack = mStack.get();
-        TraceInfo info = new TraceInfo(sectionName, flags);
-        stack.add(info);
-
-        if ((flags & TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS) != 0
-                 && mRaceConditionReproducer != null) {
-            mRaceConditionReproducer.onEvent(RaceConditionReproducer.enterEvt(sectionName));
-        }
-        updateBinderTracking(stack);
-
-        super.beginSection(sectionName, flags);
-        return info;
-    }
-
-    @Override
-    public void endSection(Object token) {
-        LinkedList<TraceInfo> stack = mStack.get();
-        if (stack.size() == 0) {
-            new Throwable().printStackTrace();
-        }
-        TraceInfo info = (TraceInfo) token;
-        stack.remove(info);
-        if ((info.flags & TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS) != 0
-                && mRaceConditionReproducer != null) {
-            mRaceConditionReproducer.onEvent(RaceConditionReproducer.exitEvt(info.sectionName));
-        }
-        updateBinderTracking(stack);
-
-        super.endSection(token);
-    }
-
-    @Override
-    public Object beginFlagsOverride(int flags) {
-        LinkedList<TraceInfo> stack = mStack.get();
-        TraceInfo info = new TraceInfo(null, flags);
-        stack.add(info);
-        updateBinderTracking(stack);
-        super.beginFlagsOverride(flags);
-        return info;
-    }
-
-    @Override
-    public void endFlagsOverride(Object token) {
-        super.endFlagsOverride(token);
-        LinkedList<TraceInfo> stack = mStack.get();
-        TraceInfo info = (TraceInfo) token;
-        stack.remove(info);
-        updateBinderTracking(stack);
-    }
-
-    private void updateBinderTracking(LinkedList<TraceInfo> stack) {
-        if (mFlagsChangeListener != null) {
-            mFlagsChangeListener.accept(stack.stream()
-                    .mapToInt(info -> info.flags).reduce(0, (a, b) -> a | b));
-        }
-    }
-
-    private static class TraceInfo {
-        public final String sectionName;
-        public final int flags;
-
-        TraceInfo(String sectionName, int flags) {
-            this.sectionName = sectionName;
-            this.flags = flags;
-        }
-    }
-}
diff --git a/tests/src/com/android/launcher3/util/WidgetUtils.java b/tests/src/com/android/launcher3/util/WidgetUtils.java
index b0df055..027a31a 100644
--- a/tests/src/com/android/launcher3/util/WidgetUtils.java
+++ b/tests/src/com/android/launcher3/util/WidgetUtils.java
@@ -18,18 +18,14 @@
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
-import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID;
-
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.os.Bundle;
 import android.os.Process;
 
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.LauncherWidgetHolder;
@@ -88,33 +84,6 @@
     }
 
     /**
-     * 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));
-    }
-
-
-    /**
      * Creates a {@link AppWidgetProviderInfo} for the provided component name
      */
     public static AppWidgetProviderInfo createAppWidgetProviderInfo(ComponentName cn) {
diff --git a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
index 7ca6a06..62d70ad 100644
--- a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
+++ b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
@@ -8,10 +8,9 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.test.core.app.ApplicationProvider;
 import androidx.test.uiautomator.UiDevice;
 
-import com.android.app.viewcapture.ViewCapture;
+import com.android.app.viewcapture.data.ExportedData;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 
@@ -24,22 +23,21 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.util.function.Supplier;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
 public class FailureWatcher extends TestWatcher {
     private static final String TAG = "FailureWatcher";
     private static boolean sSavedBugreport = false;
-    final private UiDevice mDevice;
     private final LauncherInstrumentation mLauncher;
     @NonNull
-    private final ViewCapture mViewCapture;
+    private final Supplier<ExportedData> mViewCaptureDataSupplier;
 
-    public FailureWatcher(UiDevice device, LauncherInstrumentation launcher,
-            @NonNull ViewCapture viewCapture) {
-        mDevice = device;
+    public FailureWatcher(LauncherInstrumentation launcher,
+            @NonNull Supplier<ExportedData> viewCaptureDataSupplier) {
         mLauncher = launcher;
-        mViewCapture = viewCapture;
+        mViewCaptureDataSupplier = viewCaptureDataSupplier;
     }
 
     @Override
@@ -71,7 +69,7 @@
 
     @Override
     protected void failed(Throwable e, Description description) {
-        onError(mLauncher, description, e, mViewCapture);
+        onError(mLauncher, description, e, mViewCaptureDataSupplier);
     }
 
     static File diagFile(Description description, String prefix, String ext) {
@@ -86,7 +84,7 @@
     }
 
     private static void onError(LauncherInstrumentation launcher, Description description,
-            Throwable e, @Nullable ViewCapture viewCapture) {
+            Throwable e, @Nullable Supplier<ExportedData> viewCaptureDataSupplier) {
 
         final File sceenshot = diagFile(description, "TestScreenshot", "png");
         final File hierarchy = diagFile(description, "Hierarchy", "zip");
@@ -103,9 +101,10 @@
             dumpCommand("cmd window dump-visible-window-views", out);
             out.closeEntry();
 
-            if (viewCapture != null) {
+            if (viewCaptureDataSupplier != null) {
                 out.putNextEntry(new ZipEntry("FS/data/misc/wmtrace/failed_test.vc"));
-                viewCapture.dumpTo(out, ApplicationProvider.getApplicationContext());
+                final ExportedData exportedData = viewCaptureDataSupplier.get();
+                if (exportedData != null) exportedData.writeTo(out);
                 out.closeEntry();
             }
         } catch (Exception ignored) {
diff --git a/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java b/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java
deleted file mode 100644
index e9a52f8..0000000
--- a/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.android.launcher3.util.rule;
-
-import android.app.Activity;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
-
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.util.concurrent.Callable;
-
-/**
- * Test rule to get the current Launcher activity.
- */
-public class LauncherActivityRule extends SimpleActivityRule<Launcher> {
-
-    public LauncherActivityRule() {
-        super(Launcher.class);
-    }
-
-    @Override
-    public Statement apply(Statement base, Description description) {
-
-        return new MyStatement(base) {
-            @Override
-            public void onActivityStarted(Activity activity) {
-                if (activity instanceof Launcher) {
-                    ((Launcher) activity).getRotationHelper().forceAllowRotationForTesting(true);
-                }
-            }
-        };
-    }
-
-    public Callable<Boolean> itemExists(final ItemOperator op) {
-        return () -> {
-            Launcher launcher = getActivity();
-            if (launcher == null) {
-                return false;
-            }
-            return launcher.getWorkspace().getFirstMatch(op) != null;
-        };
-    }
-}
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/util/rule/SimpleActivityRule.java b/tests/src/com/android/launcher3/util/rule/SimpleActivityRule.java
deleted file mode 100644
index 2eedec3..0000000
--- a/tests/src/com/android/launcher3/util/rule/SimpleActivityRule.java
+++ /dev/null
@@ -1,105 +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.rule;
-
-import android.app.Activity;
-import android.app.Application;
-import android.app.Application.ActivityLifecycleCallbacks;
-import android.os.Bundle;
-
-import androidx.test.InstrumentationRegistry;
-
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-/**
- * Test rule to get the current activity.
- */
-public class SimpleActivityRule<T extends Activity> implements TestRule {
-
-    private final Class<T> mClass;
-    private T mActivity;
-
-    public SimpleActivityRule(Class<T> clazz) {
-        mClass = clazz;
-    }
-
-    @Override
-    public Statement apply(Statement base, Description description) {
-        return new MyStatement(base);
-    }
-
-    public T getActivity() {
-        return mActivity;
-    }
-
-    protected class MyStatement extends Statement implements ActivityLifecycleCallbacks {
-
-        private final Statement mBase;
-
-        public MyStatement(Statement base) {
-            mBase = base;
-        }
-
-        @Override
-        public void evaluate() throws Throwable {
-            Application app = (Application)
-                    InstrumentationRegistry.getTargetContext().getApplicationContext();
-            app.registerActivityLifecycleCallbacks(this);
-            try {
-                mBase.evaluate();
-            } finally {
-                app.unregisterActivityLifecycleCallbacks(this);
-                mActivity = null;
-            }
-        }
-
-        @Override
-        public void onActivityCreated(Activity activity, Bundle bundle) {
-            if (activity != null && mClass.isInstance(activity)) {
-                mActivity = (T) activity;
-            }
-        }
-
-        @Override
-        public void onActivityStarted(Activity activity) {
-        }
-
-        @Override
-        public void onActivityResumed(Activity activity) {
-        }
-
-        @Override
-        public void onActivityPaused(Activity activity) {
-        }
-
-        @Override
-        public void onActivityStopped(Activity activity) {
-        }
-
-        @Override
-        public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
-        }
-
-        @Override
-        public void onActivityDestroyed(Activity activity) {
-            if (activity == mActivity) {
-                mActivity = null;
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index f33a50a..38de071 100644
--- a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
@@ -17,6 +17,8 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static org.junit.Assume.assumeTrue;
+
 import android.content.pm.PackageManager;
 import android.os.Build;
 import android.util.Log;
@@ -69,12 +71,9 @@
             return new Statement() {
                 @Override
                 public void evaluate() throws Throwable {
-                    if ((stability.flavors() & getRunFlavor()) != 0) {
-                        Log.d(TAG, "Running " + description.getDisplayName());
-                        base.evaluate();
-                    } else {
-                        Log.d(TAG, "Skipping " + description.getDisplayName());
-                    }
+                    assumeTrue("Ignoring the test due to @Stability annotation",
+                            (stability.flavors() & getRunFlavor()) != 0);
+                    base.evaluate();
                 }
             };
         } else {
diff --git a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
index 0c65539..ccbae4f 100644
--- a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
+++ b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
@@ -20,9 +20,21 @@
 import android.media.permission.SafeCloseable
 import android.os.Bundle
 import androidx.test.core.app.ApplicationProvider
+import androidx.test.platform.app.InstrumentationRegistry
 import com.android.app.viewcapture.SimpleViewCapture
 import com.android.app.viewcapture.ViewCapture.MAIN_EXECUTOR
+import com.android.app.viewcapture.data.ExportedData
+import com.android.launcher3.tapl.TestHelpers
 import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter
+import com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT
+import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer
+import java.io.BufferedOutputStream
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.OutputStreamWriter
+import java.util.function.Supplier
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
@@ -33,28 +45,33 @@
  *
  * This rule will not work in OOP tests that don't have access to the activity under test.
  */
-class ViewCaptureRule : TestRule {
-    val viewCapture = SimpleViewCapture("test-view-capture")
+class ViewCaptureRule(var alreadyOpenActivitySupplier: Supplier<Activity?>) : TestRule {
+    private val viewCapture = SimpleViewCapture("test-view-capture")
+    var viewCaptureData: ExportedData? = null
+        private set
 
     override fun apply(base: Statement, description: Description): Statement {
+        // Skip view capture collection in Launcher3 tests to avoid hidden API check exception.
+        if (
+            "com.android.launcher3.tests" ==
+                InstrumentationRegistry.getInstrumentation().context.packageName
+        )
+            return base
+
         return object : Statement() {
             override fun evaluate() {
+                viewCaptureData = null
                 val windowListenerCloseables = mutableListOf<SafeCloseable>()
 
+                startCapturingExistingActivity(windowListenerCloseables)
+
                 val lifecycleCallbacks =
                     object : ActivityLifecycleCallbacksAdapter {
                         override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
-                            super.onActivityCreated(activity, bundle)
-                            windowListenerCloseables.add(
-                                viewCapture.startCapture(
-                                    activity.window.decorView,
-                                    "${description.testClass?.simpleName}.${description.methodName}"
-                                )
-                            )
+                            startCapture(windowListenerCloseables, activity)
                         }
 
                         override fun onActivityDestroyed(activity: Activity) {
-                            super.onActivityDestroyed(activity)
                             viewCapture.stopCapture(activity.window.decorView)
                         }
                     }
@@ -67,6 +84,9 @@
                 } finally {
                     application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
 
+                    viewCaptureData =
+                        viewCapture.getExportedData(ApplicationProvider.getApplicationContext())
+
                     // Clean up ViewCapture references here rather than in onActivityDestroyed so
                     // test code can access view hierarchy capture. onActivityDestroyed would delete
                     // view capture data before FailureWatcher could output it as a test artifact.
@@ -74,7 +94,69 @@
                     // is removed while onDraw is running, resulting in an IllegalStateException.
                     MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) }
                 }
+
+                analyzeViewCapture(description)
             }
+
+            private fun startCapturingExistingActivity(
+                windowListenerCloseables: MutableCollection<SafeCloseable>
+            ) {
+                val alreadyOpenActivity = alreadyOpenActivitySupplier.get()
+                if (alreadyOpenActivity != null) {
+                    startCapture(windowListenerCloseables, alreadyOpenActivity)
+                }
+            }
+
+            private fun startCapture(
+                windowListenerCloseables: MutableCollection<SafeCloseable>,
+                activity: Activity
+            ) {
+                windowListenerCloseables.add(
+                    viewCapture.startCapture(
+                        activity.window.decorView,
+                        "${description.testClass?.simpleName}.${description.methodName}"
+                    )
+                )
+            }
+        }
+    }
+
+    private fun analyzeViewCapture(description: Description) {
+        // OOP tests don't produce ViewCapture data
+        if (!TestHelpers.isInLauncherProcess()) return
+
+        // Due to flakiness of ViewCapture verifier, don't run the check in presubmit
+        if (TestStabilityRule.getRunFlavor() != PLATFORM_POSTSUBMIT) return
+
+        var frameCount = 0
+        for (i in 0 until viewCaptureData!!.windowDataCount) {
+            frameCount += viewCaptureData!!.getWindowData(i).frameDataCount
+        }
+        assertTrue("Empty ViewCapture data", frameCount > 0)
+
+        val anomalies: Map<String, String> = ViewCaptureAnalyzer.getAnomalies(viewCaptureData)
+        if (!anomalies.isEmpty()) {
+            val diagFile = FailureWatcher.diagFile(description, "ViewAnomalies", "txt")
+            try {
+                OutputStreamWriter(BufferedOutputStream(FileOutputStream(diagFile))).use { writer ->
+                    writer.write("View animation anomalies detected.\r\n")
+                    writer.write(
+                        "To suppress an anomaly for a view, add its full path to the PATHS_TO_IGNORE list in the corresponding AnomalyDetector.\r\n"
+                    )
+                    writer.write("List of views with animation anomalies:\r\n")
+
+                    for ((viewPath, message) in anomalies) {
+                        writer.write("View: $viewPath\r\n        $message\r\n")
+                    }
+                }
+            } catch (ex: IOException) {
+                throw RuntimeException(ex)
+            }
+
+            val (viewPath, message) = anomalies.entries.first()
+            fail(
+                "${anomalies.size} view(s) had animation anomalies during the test, including view: $viewPath: $message\r\nSee ${diagFile.name} for details."
+            )
         }
     }
 }
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
new file mode 100644
index 0000000..4b65439
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.viewcapture_analysis;
+
+import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode;
+
+import java.util.List;
+
+/**
+ * Anomaly detector that triggers an error when alpha of a view changes too rapidly.
+ * Invisible views are treated as if they had zero alpha.
+ */
+final class AlphaJumpDetector extends AnomalyDetector {
+    // Commonly used parts of the paths to ignore.
+    private static final String CONTENT = "DecorView|LinearLayout|FrameLayout:id/content|";
+    private static final String DRAG_LAYER =
+            CONTENT + "LauncherRootView:id/launcher|DragLayer:id/drag_layer|";
+    private static final String RECENTS_DRAG_LAYER =
+            CONTENT + "LauncherRootView:id/launcher|RecentsDragLayer:id/drag_layer|";
+
+    private static final IgnoreNode IGNORED_NODES_ROOT = buildIgnoreNodesTree(List.of(
+            CONTENT
+                    + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+                    + "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
+                    + "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell"
+                    + "|WidgetCellPreview:id/widget_preview_container|ImageView:id/widget_badge",
+            CONTENT
+                    + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+                    + "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
+                    + "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell"
+                    + "|WidgetCellPreview:id/widget_preview_container|WidgetCell$1|FrameLayout"
+                    + "|ImageView:id/icon",
+            CONTENT + "AddItemDragLayer:id/add_item_drag_layer|View",
+            DRAG_LAYER
+                    + "AppWidgetResizeFrame|FrameLayout|ImageButton:id/widget_reconfigure_button",
+            DRAG_LAYER
+                    + "AppWidgetResizeFrame|FrameLayout|ImageView:id/widget_resize_bottom_handle",
+            DRAG_LAYER + "AppWidgetResizeFrame|FrameLayout|ImageView:id/widget_resize_frame",
+            DRAG_LAYER + "AppWidgetResizeFrame|FrameLayout|ImageView:id/widget_resize_left_handle",
+            DRAG_LAYER + "AppWidgetResizeFrame|FrameLayout|ImageView:id/widget_resize_right_handle",
+            DRAG_LAYER + "AppWidgetResizeFrame|FrameLayout|ImageView:id/widget_resize_top_handle",
+            DRAG_LAYER + "FloatingTaskView|FloatingTaskThumbnailView:id/thumbnail",
+            DRAG_LAYER + "FloatingTaskView|SplitPlaceholderView:id/split_placeholder",
+            DRAG_LAYER + "Folder|FolderPagedView:id/folder_content",
+            DRAG_LAYER + "LauncherAllAppsContainerView:id/apps_view",
+            DRAG_LAYER + "LauncherDragView",
+            DRAG_LAYER + "LauncherRecentsView:id/overview_panel",
+            DRAG_LAYER
+                    + "NexusOverviewActionsView:id/overview_actions_view|FrameLayout:id"
+                    + "/select_mode_buttons|ImageButton:id/close",
+            DRAG_LAYER
+                    + "NexusOverviewActionsView:id/overview_actions_view|LinearLayout:id"
+                    + "/action_buttons|Button:id/action_screenshot",
+            DRAG_LAYER
+                    + "NexusOverviewActionsView:id/overview_actions_view|LinearLayout:id"
+                    + "/action_buttons|Button:id/action_select",
+            DRAG_LAYER
+                    + "NexusOverviewActionsView:id/overview_actions_view|LinearLayout:id"
+                    + "/action_buttons|Button:id/action_split",
+            DRAG_LAYER
+                    + "NexusOverviewActionsView:id/overview_actions_view|LinearLayout:id"
+                    + "/action_buttons|Space:id/action_split_space",
+            DRAG_LAYER
+                    + "PopupContainerWithArrow:id/popup_container|LinearLayout:id"
+                    + "/deep_shortcuts_container|DeepShortcutView:id/deep_shortcut_material"
+                    + "|DeepShortcutTextView:id/bubble_text",
+            DRAG_LAYER
+                    + "PopupContainerWithArrow:id/popup_container|LinearLayout:id"
+                    + "/deep_shortcuts_container|DeepShortcutView:id/deep_shortcut_material|View"
+                    + ":id/icon",
+            DRAG_LAYER
+                    + "PopupContainerWithArrow:id/popup_container|LinearLayout:id"
+                    + "/system_shortcuts_container|DeepShortcutView:id/system_shortcut"
+                    + "|BubbleTextView:id/bubble_text",
+            DRAG_LAYER
+                    + "PopupContainerWithArrow:id/popup_container|LinearLayout:id"
+                    + "/system_shortcuts_container|DeepShortcutView:id/system_shortcut|View:id"
+                    + "/icon",
+            DRAG_LAYER
+                    + "PopupContainerWithArrow:id/popup_container|LinearLayout:id"
+                    + "/system_shortcuts_container|ImageView",
+            DRAG_LAYER
+                    + "PopupContainerWithArrow:id/popup_container|LinearLayout:id"
+                    + "/widget_shortcut_container|DeepShortcutView:id/system_shortcut"
+                    + "|BubbleTextView:id/bubble_text",
+            DRAG_LAYER
+                    + "PopupContainerWithArrow:id/popup_container|LinearLayout:id"
+                    + "/widget_shortcut_container|DeepShortcutView:id/system_shortcut|View:id/icon",
+            DRAG_LAYER + "SearchContainerView:id/apps_view",
+            DRAG_LAYER + "Snackbar|TextView:id/action",
+            DRAG_LAYER + "Snackbar|TextView:id/label",
+            DRAG_LAYER + "SplitInstructionsView|AppCompatTextView:id/split_instructions_text",
+            DRAG_LAYER + "TaskMenuView|LinearLayout:id/menu_option_layout",
+            DRAG_LAYER + "TaskMenuViewWithArrow|LinearLayout:id/menu_option_layout",
+            DRAG_LAYER + "TaskMenuView|TextView:id/task_name",
+            DRAG_LAYER + "View",
+            DRAG_LAYER + "WidgetsFullSheet|SpringRelativeLayout:id/container",
+            DRAG_LAYER + "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container",
+            CONTENT + "LauncherRootView:id/launcher|FloatingIconView",
+            RECENTS_DRAG_LAYER + "ArrowTipView",
+            DRAG_LAYER + "ArrowTipView",
+            DRAG_LAYER + "FallbackRecentsView:id/overview_panel",
+            RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel",
+            DRAG_LAYER
+                    + "NexusOverviewActionsView:id/overview_actions_view"
+                    + "|LinearLayout:id/action_buttons|Button:id/action_screenshot",
+            RECENTS_DRAG_LAYER
+                    + "NexusOverviewActionsView:id/overview_actions_view"
+                    + "|LinearLayout:id/action_buttons|Button:id/action_screenshot",
+            DRAG_LAYER
+                    + "NexusOverviewActionsView:id/overview_actions_view"
+                    + "|LinearLayout:id/action_buttons|Button:id/action_select",
+            RECENTS_DRAG_LAYER
+                    + "NexusOverviewActionsView:id/overview_actions_view"
+                    + "|LinearLayout:id/action_buttons|Button:id/action_select",
+            DRAG_LAYER
+                    + "NexusOverviewActionsView:id/overview_actions_view"
+                    + "|LinearLayout:id/action_buttons|Button:id/action_split",
+            RECENTS_DRAG_LAYER
+                    + "NexusOverviewActionsView:id/overview_actions_view"
+                    + "|LinearLayout:id/action_buttons|Button:id/action_split",
+            DRAG_LAYER + "IconView"
+    ));
+
+    // Minimal increase or decrease of view's alpha between frames that triggers the error.
+    private static final float ALPHA_JUMP_THRESHOLD = 1f;
+
+    // Per-AnalysisNode data that's specific to this detector.
+    private static class NodeData {
+        public boolean ignoreAlphaJumps;
+
+        // If ignoreNode is null, then this AnalysisNode node will be ignored if its parent is
+        // ignored.
+        // Otherwise, this AnalysisNode will be ignored if ignoreNode is a leaf i.e. has no
+        // children.
+        public IgnoreNode ignoreNode;
+    }
+
+    private NodeData getNodeData(AnalysisNode info) {
+        return (NodeData) info.detectorsData[detectorOrdinal];
+    }
+
+    @Override
+    void initializeNode(AnalysisNode info) {
+        final NodeData nodeData = new NodeData();
+        info.detectorsData[detectorOrdinal] = nodeData;
+
+        // If the parent view ignores alpha jumps, its descendants will too.
+        final boolean parentIgnoresAlphaJumps = info.parent != null && getNodeData(
+                info.parent).ignoreAlphaJumps;
+        if (parentIgnoresAlphaJumps) {
+            nodeData.ignoreAlphaJumps = true;
+            return;
+        }
+
+        // Parent view doesn't ignore alpha jumps.
+        // Initialize this AnalysisNode's ignore-node with the corresponding child of the
+        // ignore-node of the parent, if present.
+        final IgnoreNode parentIgnoreNode = info.parent != null
+                ? getNodeData(info.parent).ignoreNode
+                : IGNORED_NODES_ROOT;
+        nodeData.ignoreNode = parentIgnoreNode != null
+                ? parentIgnoreNode.children.get(info.nodeIdentity) : null;
+        // AnalysisNode will be ignored if the corresponding ignore-node is a leaf.
+        nodeData.ignoreAlphaJumps =
+                nodeData.ignoreNode != null && nodeData.ignoreNode.children.isEmpty();
+    }
+
+    @Override
+    String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN, long timestamp,
+            int windowSizePx) {
+        // If the view was previously seen, proceed with analysis only if it was present in the
+        // view hierarchy in the previous frame.
+        if (oldInfo != null && oldInfo.frameN != frameN) return null;
+
+        final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo;
+        final NodeData nodeData = getNodeData(latestInfo);
+        if (nodeData.ignoreAlphaJumps) return null;
+
+        final float oldAlpha = oldInfo != null ? oldInfo.alpha : 0;
+        final float newAlpha = newInfo != null ? newInfo.alpha : 0;
+        final float alphaDeltaAbs = Math.abs(newAlpha - oldAlpha);
+
+        if (alphaDeltaAbs >= ALPHA_JUMP_THRESHOLD) {
+            nodeData.ignoreAlphaJumps = true; // No need to report alpha jump in children.
+            return String.format(
+                    "Alpha jump detected: alpha change: %s (%s -> %s), threshold: %s",
+                    alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD);
+        }
+        return null;
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/AnomalyDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/AnomalyDetector.java
new file mode 100644
index 0000000..786791c
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/AnomalyDetector.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.viewcapture_analysis;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Detector of one kind of anomaly.
+ */
+abstract class AnomalyDetector {
+    // Index of this detector in ViewCaptureAnalyzer.ANOMALY_DETECTORS
+    public int detectorOrdinal;
+
+    /**
+     * Element of the tree of ignored nodes.
+     * If the "children" map is empty, then this node should be ignored, i.e. the analysis shouldn't
+     * run for it.
+     * I.e. ignored nodes correspond to the leaves in the ignored nodes tree.
+     */
+    protected static class IgnoreNode {
+        // Map from child node identities to ignore-nodes for these children.
+        public final Map<String, IgnoreNode> children = new HashMap<>();
+    }
+
+    // Converts the list of full paths of nodes to ignore to a more efficient tree of ignore-nodes.
+    protected static IgnoreNode buildIgnoreNodesTree(Iterable<String> pathsToIgnore) {
+        final IgnoreNode root = new IgnoreNode();
+        for (String pathToIgnore : pathsToIgnore) {
+            // Scan the diag path of an ignored node and add its elements into the tree.
+            IgnoreNode currentIgnoreNode = root;
+            for (String part : pathToIgnore.split("\\|")) {
+                // Ensure that the child of the node is added to the tree.
+                IgnoreNode child = currentIgnoreNode.children.get(part);
+                if (child == null) {
+                    currentIgnoreNode.children.put(part, child = new IgnoreNode());
+                }
+                currentIgnoreNode = child;
+            }
+        }
+        return root;
+    }
+
+    /**
+     * Initializes fields of the node that are specific to the anomaly detected by this
+     * detector.
+     */
+    abstract void initializeNode(@NonNull ViewCaptureAnalyzer.AnalysisNode info);
+
+    /**
+     * Detects anomalies by looking at the last occurrence of a view, and the current one.
+     * null value means that the view. 'oldInfo' and 'newInfo' cannot be both null.
+     * If an anomaly is detected, an exception will be thrown.
+     *
+     * @param oldInfo      the view, as seen in the last frame that contained it in the view
+     *                     hierarchy before 'currentFrame'. 'null' means that the view is first seen
+     *                     in the 'currentFrame'.
+     * @param newInfo      the view in the view hierarchy of the 'currentFrame'. 'null' means that
+     *                     the view is not present in the 'currentFrame', but was present in the
+     *                     previous frame.
+     * @param frameN       number of the current frame.
+     * @param windowSizePx maximum of the window width and height, in pixels.
+     * @return Anomaly diagnostic message if an anomaly has been detected; null otherwise.
+     */
+    abstract String detectAnomalies(
+            @Nullable ViewCaptureAnalyzer.AnalysisNode oldInfo,
+            @Nullable ViewCaptureAnalyzer.AnalysisNode newInfo, int frameN,
+            long frameTimeNs, int windowSizePx);
+}
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java
new file mode 100644
index 0000000..8b88ace
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.viewcapture_analysis;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode;
+
+import java.util.List;
+
+/**
+ * Anomaly detector that triggers an error when a view flashes, i.e. appears or disappears for a too
+ * short period of time.
+ */
+final class FlashDetector extends AnomalyDetector {
+    // Maximum time period of a view visibility or invisibility that is recognized as a flash.
+    private static final int FLASH_DURATION_MS = 300;
+
+    // Commonly used parts of the paths to ignore.
+    private static final String CONTENT = "DecorView|LinearLayout|FrameLayout:id/content|";
+    private static final String DRAG_LAYER =
+            CONTENT + "LauncherRootView:id/launcher|DragLayer:id/drag_layer|";
+    private static final String RECENTS_DRAG_LAYER =
+            CONTENT + "LauncherRootView:id/launcher|RecentsDragLayer:id/drag_layer|";
+
+    private static final IgnoreNode IGNORED_NODES_ROOT = buildIgnoreNodesTree(List.of(
+            CONTENT + "LauncherRootView:id/launcher|FloatingIconView",
+            DRAG_LAYER + "LauncherRecentsView:id/overview_panel|TaskView|TextView",
+            DRAG_LAYER
+                    + "LauncherAllAppsContainerView:id/apps_view|AllAppsRecyclerView:id"
+                    + "/apps_list_view|BubbleTextView:id/icon",
+            CONTENT
+                    + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+                    + "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
+                    + "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell"
+                    + "|WidgetCellPreview:id/widget_preview_container|WidgetImageView:id"
+                    + "/widget_preview",
+            CONTENT
+                    + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+                    + "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
+                    + "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell"
+                    + "|WidgetCellPreview:id/widget_preview_container|ImageView:id/widget_badge",
+            RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel|TaskView|IconView:id/icon",
+            DRAG_LAYER + "SearchContainerView:id/apps_view",
+            DRAG_LAYER + "LauncherDragView",
+            DRAG_LAYER + "FloatingTaskView|FloatingTaskThumbnailView:id/thumbnail",
+            DRAG_LAYER
+                    + "WidgetsFullSheet|SpringRelativeLayout:id/container|WidgetsRecyclerView:id"
+                    + "/primary_widgets_list_view|WidgetsListHeader:id/widgets_list_header",
+            DRAG_LAYER
+                    + "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container|LinearLayout:id"
+                    + "/linear_layout_container|FrameLayout:id/recycler_view_container"
+                    + "|FrameLayout:id/widgets_two_pane_sheet_recyclerview|WidgetsRecyclerView:id"
+                    + "/primary_widgets_list_view|WidgetsListHeader:id/widgets_list_header"
+    ));
+
+    // Per-AnalysisNode data that's specific to this detector.
+    private static class NodeData {
+        public boolean ignoreFlashes;
+
+        // If ignoreNode is null, then this AnalysisNode node will be ignored if its parent is
+        // ignored.
+        // Otherwise, this AnalysisNode will be ignored if ignoreNode is a leaf i.e. has no
+        // children.
+        public IgnoreNode ignoreNode;
+    }
+
+    private NodeData getNodeData(AnalysisNode info) {
+        return (NodeData) info.detectorsData[detectorOrdinal];
+    }
+
+    @Override
+    void initializeNode(AnalysisNode info) {
+        final NodeData nodeData = new NodeData();
+        info.detectorsData[detectorOrdinal] = nodeData;
+
+        // If the parent view ignores flashes, its descendants will too.
+        final boolean parentIgnoresFlashes = info.parent != null && getNodeData(
+                info.parent).ignoreFlashes;
+        if (parentIgnoresFlashes) {
+            nodeData.ignoreFlashes = true;
+            return;
+        }
+
+        // Parent view doesn't ignore flashes.
+        // Initialize this AnalysisNode's ignore-node with the corresponding child of the
+        // ignore-node of the parent, if present.
+        final IgnoreNode parentIgnoreNode = info.parent != null
+                ? getNodeData(info.parent).ignoreNode
+                : IGNORED_NODES_ROOT;
+        nodeData.ignoreNode = parentIgnoreNode != null
+                ? parentIgnoreNode.children.get(info.nodeIdentity) : null;
+        // AnalysisNode will be ignored if the corresponding ignore-node is a leaf.
+        nodeData.ignoreFlashes =
+                nodeData.ignoreNode != null && nodeData.ignoreNode.children.isEmpty();
+    }
+
+    @Override
+    String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN,
+            long frameTimeNs, int windowSizePx) {
+        // Should we check when a view was visible for a short period, then its alpha became 0?
+        // Then 'lastVisible' time should be the last one still visible?
+        // Check only transitions of alpha between 0 and 1?
+
+        // If this is the first time ever when we see the view, there have been no flashes yet.
+        if (oldInfo == null) return null;
+
+        // A flash requires a view to go from the full visibility to no-visibility and then back,
+        // or vice versa.
+        // If the last time the view was seen before the current frame, it didn't have full
+        // visibility; no flash can possibly be detected at the current frame.
+        if (oldInfo.alpha < 1) return null;
+
+        final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo;
+        final NodeData nodeData = getNodeData(latestInfo);
+        if (nodeData.ignoreFlashes) return null;
+
+        // Once the view becomes invisible, see for how long it was visible prior to that. If it
+        // was visible only for a short interval of time, it's a flash.
+        if (
+            // View is invisible in the current frame
+                newInfo == null
+                        // When the view became visible last time, it was a transition from
+                        // no-visibility to full visibility.
+                        && oldInfo.timeBecameVisibleNs != -1) {
+            final long wasVisibleTimeMs = (frameTimeNs - oldInfo.timeBecameVisibleNs) / 1000000;
+
+            if (wasVisibleTimeMs <= FLASH_DURATION_MS) {
+                nodeData.ignoreFlashes = true; // No need to report flashes in children.
+                return
+                        String.format(
+                                "View was visible for a too short period of time %dms, which is a"
+                                        + " flash",
+                                wasVisibleTimeMs
+                        );
+            }
+        }
+
+        // Once a view becomes visible, see for how long it was invisible prior to that. If it
+        // was invisible only for a short interval of time, it's a flash.
+        if (
+            // The view is fully visible now
+                newInfo != null && newInfo.alpha >= 1
+                        // The view wasn't visible in the previous frame
+                        && frameN != oldInfo.frameN + 1) {
+            // We can assert the below condition because at this point, we know that
+            // oldInfo.alpha >= 1, i.e. it disappeared abruptly.
+            assertTrue("oldInfo.timeBecameInvisibleNs must not be -1",
+                    oldInfo.timeBecameInvisibleNs != -1);
+
+            final long wasInvisibleTimeMs = (frameTimeNs - oldInfo.timeBecameInvisibleNs) / 1000000;
+            if (wasInvisibleTimeMs <= FLASH_DURATION_MS) {
+                nodeData.ignoreFlashes = true; // No need to report flashes in children.
+                return
+                        String.format(
+                                "View was invisible for a too short period of time %dms, which "
+                                        + "is a flash",
+                                wasInvisibleTimeMs);
+            }
+        }
+        return null;
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/PositionJumpDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/PositionJumpDetector.java
new file mode 100644
index 0000000..a1ddcb0
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/PositionJumpDetector.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.viewcapture_analysis;
+
+import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode;
+
+import java.util.List;
+
+/**
+ * Anomaly detector that triggers an error when a view position jumps.
+ */
+final class PositionJumpDetector extends AnomalyDetector {
+    // Maximum allowed jump in "milliwindows", i.e. a 1/1000's of the maximum of the window
+    // dimensions.
+    private static final float JUMP_MIW = 250;
+
+    private static final String[] BORDER_NAMES = {"left", "top", "right", "bottom"};
+
+    // Commonly used parts of the paths to ignore.
+    private static final String CONTENT = "DecorView|LinearLayout|FrameLayout:id/content|";
+    private static final String DRAG_LAYER =
+            CONTENT + "LauncherRootView:id/launcher|DragLayer:id/drag_layer|";
+    private static final String RECENTS_DRAG_LAYER =
+            CONTENT + "LauncherRootView:id/launcher|RecentsDragLayer:id/drag_layer|";
+
+    private static final IgnoreNode IGNORED_NODES_ROOT = buildIgnoreNodesTree(List.of(
+            DRAG_LAYER + "SearchContainerView:id/apps_view",
+            DRAG_LAYER + "AppWidgetResizeFrame",
+            DRAG_LAYER + "LauncherAllAppsContainerView:id/apps_view",
+            CONTENT
+                    + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+                    + "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content",
+            DRAG_LAYER + "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container",
+            DRAG_LAYER + "WidgetsFullSheet|SpringRelativeLayout:id/container",
+            DRAG_LAYER + "LauncherDragView",
+            RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel|TaskView",
+            CONTENT + "LauncherRootView:id/launcher|FloatingIconView",
+            DRAG_LAYER + "FloatingTaskView",
+            DRAG_LAYER + "LauncherRecentsView:id/overview_panel"
+    ));
+
+    // Per-AnalysisNode data that's specific to this detector.
+    private static class NodeData {
+        public boolean ignoreJumps;
+
+        // If ignoreNode is null, then this AnalysisNode node will be ignored if its parent is
+        // ignored.
+        // Otherwise, this AnalysisNode will be ignored if ignoreNode is a leaf i.e. has no
+        // children.
+        public IgnoreNode ignoreNode;
+    }
+
+    private NodeData getNodeData(AnalysisNode info) {
+        return (NodeData) info.detectorsData[detectorOrdinal];
+    }
+
+    @Override
+    void initializeNode(AnalysisNode info) {
+        final NodeData nodeData = new NodeData();
+        info.detectorsData[detectorOrdinal] = nodeData;
+
+        // If the parent view ignores jumps, its descendants will too.
+        final boolean parentIgnoresJumps = info.parent != null && getNodeData(
+                info.parent).ignoreJumps;
+        if (parentIgnoresJumps) {
+            nodeData.ignoreJumps = true;
+            return;
+        }
+
+        // Parent view doesn't ignore jumps.
+        // Initialize this AnalysisNode's ignore-node with the corresponding child of the
+        // ignore-node of the parent, if present.
+        final IgnoreNode parentIgnoreNode = info.parent != null
+                ? getNodeData(info.parent).ignoreNode
+                : IGNORED_NODES_ROOT;
+        nodeData.ignoreNode = parentIgnoreNode != null
+                ? parentIgnoreNode.children.get(info.nodeIdentity) : null;
+        // AnalysisNode will be ignored if the corresponding ignore-node is a leaf.
+        nodeData.ignoreJumps =
+                nodeData.ignoreNode != null && nodeData.ignoreNode.children.isEmpty();
+    }
+
+    @Override
+    String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN,
+            long frameTimeNs, int windowSizePx) {
+        // If the view is not present in the current frame, there can't be a jump detected in the
+        // current frame.
+        if (newInfo == null) return null;
+
+        // We only detect position jumps if the view was visible in the previous frame.
+        if (oldInfo == null || frameN != oldInfo.frameN + 1) return null;
+
+        final NodeData newNodeData = getNodeData(newInfo);
+        if (newNodeData.ignoreJumps) return null;
+
+        final float[] positionDiffs = {
+                newInfo.left - oldInfo.left,
+                newInfo.top - oldInfo.top,
+                newInfo.right - oldInfo.right,
+                newInfo.bottom - oldInfo.bottom
+        };
+
+        for (int i = 0; i < 4; ++i) {
+            final float positionDiffAbs = Math.abs(positionDiffs[i]);
+            if (positionDiffAbs * 1000 > JUMP_MIW * windowSizePx) {
+                newNodeData.ignoreJumps = true;
+                return String.format("Position jump: %s jumped by %s",
+                        BORDER_NAMES[i], positionDiffAbs);
+            }
+        }
+        return null;
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
new file mode 100644
index 0000000..9459cc2
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.viewcapture_analysis;
+
+import static android.view.View.VISIBLE;
+
+import com.android.app.viewcapture.data.ExportedData;
+import com.android.app.viewcapture.data.FrameData;
+import com.android.app.viewcapture.data.ViewNode;
+import com.android.app.viewcapture.data.WindowData;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utility that analyzes ViewCapture data and finds anomalies such as views appearing or
+ * disappearing without alpha-fading.
+ */
+public class ViewCaptureAnalyzer {
+    private static final String SCRIM_VIEW_CLASS = "com.android.launcher3.views.ScrimView";
+
+    // All detectors. They will be invoked in the order listed here.
+    private static final AnomalyDetector[] ANOMALY_DETECTORS = {
+            new AlphaJumpDetector(),
+            new FlashDetector(),
+            new PositionJumpDetector()
+    };
+
+    static {
+        for (int i = 0; i < ANOMALY_DETECTORS.length; ++i) ANOMALY_DETECTORS[i].detectorOrdinal = i;
+    }
+
+    // A view from view capture data converted to a form that's convenient for detecting anomalies.
+    static class AnalysisNode {
+        public String className;
+        public String resourceId;
+        public AnalysisNode parent;
+
+        // Window coordinates of the view.
+        public float left;
+        public float top;
+        public float right;
+        public float bottom;
+
+        // Visible scale and alpha, build recursively from the ancestor list.
+        public float scaleX;
+        public float scaleY;
+        public float alpha; // Always > 0
+
+        public int frameN;
+
+        // Timestamp of the frame when this view became abruptly visible, i.e. its alpha became 1
+        // the next frame after it was 0 or the view wasn't visible.
+        // If the view is currently invisible or the last appearance wasn't abrupt, the value is -1.
+        public long timeBecameVisibleNs;
+
+        // Timestamp of the frame when this view became abruptly invisible last time, i.e. its
+        // alpha became 0, or view disappeared, after being 1 in the previous frame.
+        // If the view is currently visible or the last disappearance wasn't abrupt, the value is
+        // -1.
+        public long timeBecameInvisibleNs;
+
+        public ViewNode viewCaptureNode;
+
+        // Class name + resource id
+        public String nodeIdentity;
+
+        // Collection of detector-specific data for this node.
+        public final Object[] detectorsData = new Object[ANOMALY_DETECTORS.length];
+
+        @Override
+        public String toString() {
+            return String.format("view window coordinates: (%s, %s, %s, %s)",
+                    left, top, right, bottom);
+        }
+    }
+
+    /**
+     * Scans a view capture record and searches for view animation anomalies. Can find anomalies for
+     * multiple views.
+     * Returns a map from the view path to the anomaly message for the view. Non-empty map means
+     * that anomalies were detected.
+     */
+    public static Map<String, String> getAnomalies(ExportedData viewCaptureData) {
+        final Map<String, String> anomalies = new HashMap<>();
+
+        final int scrimClassIndex = viewCaptureData.getClassnameList().indexOf(SCRIM_VIEW_CLASS);
+
+        final int windowDataCount = viewCaptureData.getWindowDataCount();
+        for (int i = 0; i < windowDataCount; ++i) {
+            analyzeWindowData(
+                    viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex, anomalies);
+        }
+        return anomalies;
+    }
+
+    private static void analyzeWindowData(ExportedData viewCaptureData, WindowData windowData,
+            int scrimClassIndex, Map<String, String> anomalies) {
+        // View hash code => Last seen node with this hash code.
+        // The view is added when we analyze the first frame where it's visible.
+        // After that, it gets updated for every frame where it's visible.
+        // As we go though frames, if a view becomes invisible, it stays in the map.
+        final Map<Integer, AnalysisNode> lastSeenNodes = new HashMap<>();
+
+        int windowWidthPx = -1;
+        int windowHeightPx = -1;
+
+        for (int frameN = 0; frameN < windowData.getFrameDataCount(); ++frameN) {
+            final FrameData frame = windowData.getFrameData(frameN);
+            final ViewNode rootNode = frame.getNode();
+
+            // If the rotation or window size has changed, reset the analyzer state.
+            final boolean isFirstFrame = windowWidthPx != rootNode.getWidth()
+                    || windowHeightPx != rootNode.getHeight();
+            if (isFirstFrame) {
+                windowWidthPx = rootNode.getWidth();
+                windowHeightPx = rootNode.getHeight();
+                lastSeenNodes.clear();
+            }
+
+            final int windowSizePx = Math.max(rootNode.getWidth(), rootNode.getHeight());
+
+            analyzeFrame(frameN, isFirstFrame, frame, viewCaptureData, lastSeenNodes,
+                    scrimClassIndex, anomalies, windowSizePx);
+        }
+    }
+
+    private static void analyzeFrame(int frameN, boolean isFirstFrame, FrameData frame,
+            ExportedData viewCaptureData,
+            Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex,
+            Map<String, String> anomalies, int windowSizePx) {
+        // Analyze the node tree starting from the root.
+        long frameTimeNs = frame.getTimestamp();
+        analyzeView(
+                frameTimeNs,
+                frame.getNode(),
+                /* parent = */ null,
+                frameN,
+                isFirstFrame,
+                /* leftShift = */ 0,
+                /* topShift = */ 0,
+                viewCaptureData,
+                lastSeenNodes,
+                scrimClassIndex,
+                anomalies,
+                windowSizePx);
+
+        // Analyze transitions when a view visible in the previous frame became invisible in the
+        // current one.
+        for (AnalysisNode info : lastSeenNodes.values()) {
+            if (info.frameN == frameN - 1) {
+                if (!info.viewCaptureNode.getWillNotDraw()) {
+                    Arrays.stream(ANOMALY_DETECTORS).forEach(
+                            detector ->
+                                    detectAnomaly(
+                                            detector,
+                                            frameN,
+                                            /* oldInfo = */ info,
+                                            /* newInfo = */ null,
+                                            anomalies,
+                                            frameTimeNs,
+                                            windowSizePx)
+                    );
+                }
+                info.timeBecameInvisibleNs = info.alpha == 1 ? frameTimeNs : -1;
+                info.timeBecameVisibleNs = -1;
+            }
+        }
+    }
+
+    private static void analyzeView(long frameTimeNs, ViewNode viewCaptureNode, AnalysisNode parent,
+            int frameN,
+            boolean isFirstFrame, float leftShift, float topShift, ExportedData viewCaptureData,
+            Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex,
+            Map<String, String> anomalies, int windowSizePx) {
+        // Skip analysis of invisible views
+        final float parentAlpha = parent != null ? parent.alpha : 1;
+        final float alpha = getVisibleAlpha(viewCaptureNode, parentAlpha);
+        if (alpha <= 0.0) return;
+
+        // Calculate analysis node parameters
+        final int hashcode = viewCaptureNode.getHashcode();
+        final int classIndex = viewCaptureNode.getClassnameIndex();
+
+        final float parentScaleX = parent != null ? parent.scaleX : 1;
+        final float parentScaleY = parent != null ? parent.scaleY : 1;
+        final float scaleX = parentScaleX * viewCaptureNode.getScaleX();
+        final float scaleY = parentScaleY * viewCaptureNode.getScaleY();
+
+        final float left = leftShift
+                + (viewCaptureNode.getLeft() + viewCaptureNode.getTranslationX()) * parentScaleX
+                + viewCaptureNode.getWidth() * (parentScaleX - scaleX) / 2;
+        final float top = topShift
+                + (viewCaptureNode.getTop() + viewCaptureNode.getTranslationY()) * parentScaleY
+                + viewCaptureNode.getHeight() * (parentScaleY - scaleY) / 2;
+        final float width = viewCaptureNode.getWidth() * scaleX;
+        final float height = viewCaptureNode.getHeight() * scaleY;
+
+        // Initialize new analysis node
+        final AnalysisNode newAnalysisNode = new AnalysisNode();
+        newAnalysisNode.className = viewCaptureData.getClassname(classIndex);
+        newAnalysisNode.resourceId = viewCaptureNode.getId();
+        newAnalysisNode.nodeIdentity =
+                getNodeIdentity(newAnalysisNode.className, newAnalysisNode.resourceId);
+        newAnalysisNode.parent = parent;
+        newAnalysisNode.left = left;
+        newAnalysisNode.top = top;
+        newAnalysisNode.right = left + width;
+        newAnalysisNode.bottom = top + height;
+        newAnalysisNode.scaleX = scaleX;
+        newAnalysisNode.scaleY = scaleY;
+        newAnalysisNode.alpha = alpha;
+        newAnalysisNode.frameN = frameN;
+        newAnalysisNode.timeBecameInvisibleNs = -1;
+        newAnalysisNode.viewCaptureNode = viewCaptureNode;
+        Arrays.stream(ANOMALY_DETECTORS).forEach(
+                detector -> detector.initializeNode(newAnalysisNode));
+
+        final AnalysisNode oldAnalysisNode = lastSeenNodes.get(hashcode); // may be null
+
+        if (oldAnalysisNode != null && oldAnalysisNode.frameN + 1 == frameN) {
+            // If this view was present in the previous frame, keep the time when it became visible.
+            newAnalysisNode.timeBecameVisibleNs = oldAnalysisNode.timeBecameVisibleNs;
+        } else {
+            // If the view is becoming visible after being invisible, initialize the time when it
+            // became visible with a new value.
+            // If the view became visible abruptly, i.e. alpha jumped from 0 to 1 between the
+            // previous and the current frames, then initialize with the time of the current
+            // frame. Otherwise, use -1.
+            newAnalysisNode.timeBecameVisibleNs = newAnalysisNode.alpha >= 1 ? frameTimeNs : -1;
+        }
+
+        // Detect anomalies for the view.
+        if (!isFirstFrame && !viewCaptureNode.getWillNotDraw()) {
+            Arrays.stream(ANOMALY_DETECTORS).forEach(
+                    detector ->
+                            detectAnomaly(detector, frameN, oldAnalysisNode, newAnalysisNode,
+                                    anomalies, frameTimeNs, windowSizePx)
+            );
+        }
+        lastSeenNodes.put(hashcode, newAnalysisNode);
+
+        // Enumerate children starting from the topmost one. Stop at ScrimView, if present.
+        final float leftShiftForChildren = left - viewCaptureNode.getScrollX();
+        final float topShiftForChildren = top - viewCaptureNode.getScrollY();
+        for (int i = viewCaptureNode.getChildrenCount() - 1; i >= 0; --i) {
+            final ViewNode child = viewCaptureNode.getChildren(i);
+
+            // Don't analyze anything under scrim view because we don't know whether it's
+            // transparent.
+            if (child.getClassnameIndex() == scrimClassIndex) break;
+
+            analyzeView(frameTimeNs, child, newAnalysisNode, frameN, isFirstFrame,
+                    leftShiftForChildren,
+                    topShiftForChildren,
+                    viewCaptureData, lastSeenNodes, scrimClassIndex, anomalies, windowSizePx);
+        }
+    }
+
+    private static void detectAnomaly(AnomalyDetector detector, int frameN,
+            AnalysisNode oldAnalysisNode, AnalysisNode newAnalysisNode,
+            Map<String, String> anomalies, long frameTimeNs, int windowSizePx) {
+        final String maybeAnomaly =
+                detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN, frameTimeNs,
+                        windowSizePx);
+        if (maybeAnomaly != null) {
+            AnalysisNode latestInfo = newAnalysisNode != null ? newAnalysisNode : oldAnalysisNode;
+            final String viewDiagPath = diagPathFromRoot(latestInfo);
+            if (!anomalies.containsKey(viewDiagPath)) {
+                anomalies.put(viewDiagPath, String.format("%s, %s", maybeAnomaly, latestInfo));
+            }
+        }
+    }
+
+    private static float getVisibleAlpha(ViewNode node, float parenVisibleAlpha) {
+        return node.getVisibility() == VISIBLE
+                ? parenVisibleAlpha * Math.max(0, Math.min(node.getAlpha(), 1))
+                : 0f;
+    }
+
+    private static String classNameToSimpleName(String className) {
+        return className.substring(className.lastIndexOf(".") + 1);
+    }
+
+    private static String diagPathFromRoot(AnalysisNode analysisNode) {
+        final StringBuilder path = new StringBuilder(analysisNode.nodeIdentity);
+        for (AnalysisNode ancestor = analysisNode.parent;
+                ancestor != null;
+                ancestor = ancestor.parent) {
+            path.insert(0, ancestor.nodeIdentity + "|");
+        }
+        return path.toString();
+    }
+
+    private static String getNodeIdentity(String className, String resourceId) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(classNameToSimpleName(className));
+        if (!"NO_ID".equals(resourceId)) sb.append(":" + resourceId);
+        return sb.toString();
+    }
+}
diff --git a/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt b/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
deleted file mode 100644
index 9cd0a2e..0000000
--- a/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.workspace
-
-import android.content.Context
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import androidx.test.platform.app.InstrumentationRegistry
-import com.android.launcher3.AbstractDeviceProfileTest
-import com.android.launcher3.tests.R as TestR
-import com.android.launcher3.util.TestResourceHelper
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class WorkspaceSpecsTest : AbstractDeviceProfileTest() {
-    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
-
-    @Before
-    fun setup() {
-        initializeVarsForPhone(deviceSpecs["phone"]!!)
-    }
-
-    @Test
-    fun parseValidFile() {
-        val workspaceSpecs =
-            WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
-        assertThat(workspaceSpecs.workspaceHeightSpecList.size).isEqualTo(3)
-        assertThat(workspaceSpecs.workspaceHeightSpecList[0].toString())
-            .isEqualTo(
-                "WorkspaceSpec(" +
-                    "maxAvailableSize=1533, " +
-                    "specType=HEIGHT, " +
-                    "startPadding=SizeSpec(fixedSize=0.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
-                    "endPadding=SizeSpec(fixedSize=84.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
-                    "gutter=SizeSpec(fixedSize=42.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
-                    "cellSize=SizeSpec(fixedSize=0.0, " +
-                    "ofAvailableSpace=0.15808, " +
-                    "ofRemainderSpace=0.0)" +
-                    ")"
-            )
-        assertThat(workspaceSpecs.workspaceHeightSpecList[1].toString())
-            .isEqualTo(
-                "WorkspaceSpec(" +
-                    "maxAvailableSize=1607, " +
-                    "specType=HEIGHT, " +
-                    "startPadding=SizeSpec(fixedSize=0.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
-                    "endPadding=SizeSpec(fixedSize=0.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=1.0), " +
-                    "gutter=SizeSpec(fixedSize=42.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
-                    "cellSize=SizeSpec(fixedSize=273.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0)" +
-                    ")"
-            )
-        assertThat(workspaceSpecs.workspaceHeightSpecList[2].toString())
-            .isEqualTo(
-                "WorkspaceSpec(" +
-                    "maxAvailableSize=26247, " +
-                    "specType=HEIGHT, " +
-                    "startPadding=SizeSpec(fixedSize=21.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
-                    "endPadding=SizeSpec(fixedSize=0.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=1.0), " +
-                    "gutter=SizeSpec(fixedSize=42.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
-                    "cellSize=SizeSpec(fixedSize=273.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0)" +
-                    ")"
-            )
-        assertThat(workspaceSpecs.workspaceWidthSpecList.size).isEqualTo(1)
-        assertThat(workspaceSpecs.workspaceWidthSpecList[0].toString())
-            .isEqualTo(
-                "WorkspaceSpec(" +
-                    "maxAvailableSize=26247, " +
-                    "specType=WIDTH, " +
-                    "startPadding=SizeSpec(fixedSize=58.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
-                    "endPadding=SizeSpec(fixedSize=58.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
-                    "gutter=SizeSpec(fixedSize=42.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
-                    "cellSize=SizeSpec(fixedSize=0.0, " +
-                    "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.25)" +
-                    ")"
-            )
-    }
-
-    @Test(expected = IllegalStateException::class)
-    fun parseInvalidFile_missingTag_throwsError() {
-        WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_1))
-    }
-
-    @Test(expected = IllegalStateException::class)
-    fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
-        WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_2))
-    }
-
-    @Test(expected = IllegalStateException::class)
-    fun parseInvalidFile_valueBiggerThan1_throwsError() {
-        WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_3))
-    }
-}
diff --git a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
index 10afe13..425c3c0 100644
--- a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
+++ b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
@@ -45,8 +45,8 @@
             mLauncher.clickObject(
                     mLauncher.waitForObjectInContainer(
                             mWidgetCell.getParent().getParent().getParent().getParent(),
-                            By.text(ADD_AUTOMATICALLY)),
-                    LauncherInstrumentation.GestureScope.OUTSIDE_WITHOUT_PILFER);
+                            By.text(ADD_AUTOMATICALLY))
+            );
             mLauncher.waitUntilLauncherObjectGone(getSelector());
         }
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 399abc7..fb08ea4 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -210,6 +210,9 @@
     public AppIcon getAppIcon(String appName) {
         AppIcon appIcon = tryGetAppIcon(appName);
         mLauncher.assertNotNull("Unable to scroll to a clickable icon: " + appName, appIcon);
+        // appIcon.getAppName() checks for content description, so it is possible that it can have
+        // trailing words. So check if the content description contains the appName.
+        mLauncher.assertTrue("Wrong app icon name.", appIcon.getAppName().contains(appName));
         return appIcon;
     }
 
@@ -333,4 +336,11 @@
         final Bundle testInfo = mLauncher.getTestInfo(TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS);
         return testInfo == null ? 0 : testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
+
+    /**
+     * Return the QSB UI object on the AllApps screen.
+     * @return the QSB UI object.
+     */
+    @NonNull
+    public abstract Qsb getQsb();
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java b/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java
index c4744a1..0e0291f 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java
@@ -62,4 +62,10 @@
         return mLauncher.getTestInfo(TestProtocol.REQUEST_TASKBAR_APPS_LIST_SCROLL_Y)
                 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
+
+    @NonNull
+    @Override
+    public TaskbarAllAppsQsb getQsb() {
+        return new TaskbarAllAppsQsb(mLauncher, verifyActiveContainer());
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsQsb.java b/tests/tapl/com/android/launcher3/tapl/AllAppsQsb.java
index 0931cd4..1692351 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllAppsQsb.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllAppsQsb.java
@@ -22,16 +22,7 @@
  */
 class AllAppsQsb extends Qsb {
 
-    private final UiObject2 mAllAppsContainer;
-
     AllAppsQsb(LauncherInstrumentation launcher, UiObject2 allAppsContainer) {
-        super(launcher);
-        mAllAppsContainer = allAppsContainer;
-        waitForQsbObject();
-    }
-
-    @Override
-    protected UiObject2 waitForQsbObject() {
-        return mLauncher.waitForObjectInContainer(mAllAppsContainer, "search_container_all_apps");
+        super(launcher, allAppsContainer, "search_container_all_apps");
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 0a0cf07..85098c8 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -37,10 +37,14 @@
     }
 
     static BySelector getAppIconSelector(String appName, LauncherInstrumentation launcher) {
-        return By.clazz(TextView.class).textContains(appName)
+        return By.clazz(TextView.class).desc(makeMultilinePattern(appName))
                 .pkg(launcher.getLauncherPackageName());
     }
 
+    static BySelector getMenuItemSelector(String text, LauncherInstrumentation launcher) {
+        return By.clazz(TextView.class).text(text).pkg(launcher.getLauncherPackageName());
+    }
+
     static BySelector getAnyAppIconSelector() {
         return By.clazz(TextView.class);
     }
@@ -94,4 +98,24 @@
     public String getIconName() {
         return getObject().getText();
     }
+
+    /**
+     * Return the app name of a icon by the content description. This should be used when trying to
+     * get the name of an app where the text of it is multiline.
+     */
+    @NonNull
+    String getAppName() {
+        return getObject().getContentDescription();
+    }
+
+    /**
+     * Create a regular expression pattern that matches strings starting with the app name, where
+     * spaces in the app name are replaced with zero or more occurrences of the "\s" character
+     * (which represents a whitespace character in regular expressions), followed by any characters
+     * after the app name.
+     */
+    static Pattern makeMultilinePattern(String appName) {
+        return Pattern.compile(appName.replaceAll("\\s+", "\\\\s*") + ".*",
+                Pattern.DOTALL);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java b/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java
index 667290f..bbcc6a8 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java
@@ -50,7 +50,7 @@
      */
     public AppIconMenuItem getMenuItem(String shortcutText) {
         final UiObject2 menuItem = mLauncher.waitForObjectInContainer(mDeepShortcutsContainer,
-                AppIcon.getAppIconSelector(shortcutText, mLauncher));
+                AppIcon.getMenuItemSelector(shortcutText, mLauncher));
         return createMenuItem(menuItem);
     }
 
@@ -59,7 +59,7 @@
      */
     public SplitScreenMenuItem getSplitScreenMenuItem() {
         final UiObject2 menuItem = mLauncher.waitForObjectInContainer(mDeepShortcutsContainer,
-                AppIcon.getAppIconSelector("Split screen", mLauncher));
+                AppIcon.getMenuItemSelector("Split screen", mLauncher));
         return new SplitScreenMenuItem(mLauncher, menuItem);
     }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 5a96d95..677f204 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -28,6 +28,8 @@
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.UiObject2;
 
+import com.android.launcher3.tapl.LauncherInstrumentation.NavigationModel;
+import com.android.launcher3.tapl.LauncherInstrumentation.TrackpadGestureType;
 import com.android.launcher3.testing.shared.TestProtocol;
 
 import java.util.List;
@@ -64,85 +66,71 @@
     }
 
 
-    protected boolean zeroButtonToOverviewGestureStartsInLauncher() {
-        return mLauncher.isTablet();
-    }
-
     protected boolean zeroButtonToOverviewGestureStateTransitionWhileHolding() {
         return false;
     }
 
     protected void goToOverviewUnchecked() {
-        switch (mLauncher.getNavigationModel()) {
-            case ZERO_BUTTON: {
-                final long downTime = SystemClock.uptimeMillis();
-                sendDownPointerToEnterOverviewToLauncher(downTime);
-                String swipeAndHoldToEnterOverviewActionName =
-                        "swiping and holding to enter overview";
-                // If swiping from an app (e.g. Overview is in Background), we pause and hold on
-                // swipe up to make overview appear, or else swiping without holding would take
-                // us to the Home state. If swiping up from Home (e.g. Overview in Home or
-                // Workspace state where the below condition is true), there is no need to pause,
-                // and we will not test for an intermediate carousel as one will not exist.
-                if (zeroButtonToOverviewGestureStateTransitionWhileHolding()) {
-                    mLauncher.runToState(
-                            () -> sendSwipeUpAndHoldToEnterOverviewGestureToLauncher(downTime),
-                            OVERVIEW_STATE_ORDINAL, swipeAndHoldToEnterOverviewActionName);
-                    sendUpPointerToEnterOverviewToLauncher(downTime);
-                } else {
-                    // If swiping up from an app to overview, pause on intermediate carousel
-                    // until snapshots are visible. No intermediate carousel when swiping from
-                    // Home. The task swiped up is not a snapshot but the TaskViewSimulator. If
-                    // only a single task exists, no snapshots will be available during swipe up.
-                    mLauncher.executeAndWaitForLauncherEvent(
-                            () -> sendSwipeUpAndHoldToEnterOverviewGestureToLauncher(downTime),
-                            event -> TestProtocol.PAUSE_DETECTED_MESSAGE.equals(
-                                    event.getClassName().toString()),
-                            () -> "Pause wasn't detected",
-                            swipeAndHoldToEnterOverviewActionName);
-                    try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                            "paused on swipe up to overview")) {
-                        if (mLauncher.getRecentTasks().size() > 1) {
-                            // When swiping up to grid-overview for tablets, the swiped tab will be
-                            // in the middle of the screen (TaskViewSimulator, not a snapshot), and
-                            // all remaining snapshots will be to the left of that task. In
-                            // non-tablet overview, snapshots can be on either side of the swiped
-                            // task, but we still check that they become visible after swiping and
-                            // pausing.
-                            mLauncher.waitForOverviewObject("snapshot");
-                            if (mLauncher.isTablet()) {
-                                List<UiObject2> tasks = mLauncher.getDevice().findObjects(
-                                        mLauncher.getOverviewObjectSelector("snapshot"));
-                                final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
-                                mLauncher.assertTrue(
-                                        "All tasks not to the left of the swiped task",
-                                        tasks.stream()
-                                                .allMatch(
-                                                        t -> t.getVisibleBounds().right < centerX));
-                            }
-
-                        }
-                        String upPointerToEnterOverviewActionName =
-                                "sending UP pointer to enter overview";
-                        mLauncher.runToState(() -> sendUpPointerToEnterOverviewToLauncher(downTime),
-                                OVERVIEW_STATE_ORDINAL, upPointerToEnterOverviewActionName);
-                    }
-                }
-                break;
-            }
-
-            case THREE_BUTTON:
-                if (mLauncher.isTablet()) {
-                    mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
-                            LauncherInstrumentation.EVENT_TOUCH_DOWN);
-                    mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
-                            LauncherInstrumentation.EVENT_TOUCH_UP);
-                }
-                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+        if (mLauncher.getNavigationModel() == NavigationModel.ZERO_BUTTON
+                || mLauncher.getTrackpadGestureType() == TrackpadGestureType.THREE_FINGER) {
+            final long downTime = SystemClock.uptimeMillis();
+            sendDownPointerToEnterOverviewToLauncher(downTime);
+            String swipeAndHoldToEnterOverviewActionName =
+                    "swiping and holding to enter overview";
+            // If swiping from an app (e.g. Overview is in Background), we pause and hold on
+            // swipe up to make overview appear, or else swiping without holding would take
+            // us to the Home state. If swiping up from Home (e.g. Overview in Home or
+            // Workspace state where the below condition is true), there is no need to pause,
+            // and we will not test for an intermediate carousel as one will not exist.
+            if (zeroButtonToOverviewGestureStateTransitionWhileHolding()) {
                 mLauncher.runToState(
-                        () -> mLauncher.waitForNavigationUiObject("recent_apps").click(),
-                        OVERVIEW_STATE_ORDINAL, "clicking Recents button");
-                break;
+                        () -> sendSwipeUpAndHoldToEnterOverviewGestureToLauncher(downTime),
+                        OVERVIEW_STATE_ORDINAL, swipeAndHoldToEnterOverviewActionName);
+                sendUpPointerToEnterOverviewToLauncher(downTime);
+            } else {
+                // If swiping up from an app to overview, pause on intermediate carousel
+                // until snapshots are visible. No intermediate carousel when swiping from
+                // Home. The task swiped up is not a snapshot but the TaskViewSimulator. If
+                // only a single task exists, no snapshots will be available during swipe up.
+                mLauncher.executeAndWaitForLauncherEvent(
+                        () -> sendSwipeUpAndHoldToEnterOverviewGestureToLauncher(downTime),
+                        event -> TestProtocol.PAUSE_DETECTED_MESSAGE.equals(
+                                event.getClassName().toString()),
+                        () -> "Pause wasn't detected",
+                        swipeAndHoldToEnterOverviewActionName);
+                try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                        "paused on swipe up to overview")) {
+                    if (mLauncher.getRecentTasks().size() > 1) {
+                        // When swiping up to grid-overview for tablets, the swiped tab will be
+                        // in the middle of the screen (TaskViewSimulator, not a snapshot), and
+                        // all remaining snapshots will be to the left of that task. In
+                        // non-tablet overview, snapshots can be on either side of the swiped
+                        // task, but we still check that they become visible after swiping and
+                        // pausing.
+                        mLauncher.waitForOverviewObject("snapshot");
+                        if (mLauncher.isTablet()) {
+                            List<UiObject2> tasks = mLauncher.getDevice().findObjects(
+                                    mLauncher.getOverviewObjectSelector("snapshot"));
+                            final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
+                            mLauncher.assertTrue(
+                                    "All tasks not to the left of the swiped task",
+                                    tasks.stream()
+                                            .allMatch(
+                                                    t -> t.getVisibleBounds().right < centerX));
+                        }
+
+                    }
+                    String upPointerToEnterOverviewActionName =
+                            "sending UP pointer to enter overview";
+                    mLauncher.runToState(() -> sendUpPointerToEnterOverviewToLauncher(downTime),
+                            OVERVIEW_STATE_ORDINAL, upPointerToEnterOverviewActionName);
+                }
+            }
+        } else {
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+            mLauncher.runToState(
+                    () -> mLauncher.waitForNavigationUiObject("recent_apps").click(),
+                    OVERVIEW_STATE_ORDINAL, "clicking Recents button");
         }
         expectSwitchToOverviewEvents();
     }
@@ -154,12 +142,9 @@
         final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
         final int startY = getSwipeStartY();
         final Point start = new Point(centerX, startY);
-        final LauncherInstrumentation.GestureScope gestureScope =
-                zeroButtonToOverviewGestureStartsInLauncher()
-                        ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
-                        : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER;
 
-        mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
+        mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start,
+                LauncherInstrumentation.GestureScope.EXPECT_PILFER);
 
         if (!mLauncher.isLauncher3()) {
             mLauncher.expectEvent(TestProtocol.SEQUENCE_PILFER,
@@ -175,10 +160,6 @@
         final Point start = new Point(centerX, startY);
         final Point end =
                 new Point(centerX, startY - swipeHeight - mLauncher.getTouchSlop());
-        final LauncherInstrumentation.GestureScope gestureScope =
-                zeroButtonToOverviewGestureStartsInLauncher()
-                        ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
-                        : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER;
 
         mLauncher.movePointer(
                 downTime,
@@ -186,7 +167,7 @@
                 ZERO_BUTTON_SWIPE_UP_GESTURE_DURATION,
                 start,
                 end,
-                gestureScope);
+                LauncherInstrumentation.GestureScope.EXPECT_PILFER);
     }
 
     private void sendUpPointerToEnterOverviewToLauncher(long downTime) {
@@ -197,13 +178,9 @@
         final Point end =
                 new Point(centerX, startY - swipeHeight - mLauncher.getTouchSlop());
 
-        final LauncherInstrumentation.GestureScope gestureScope =
-                zeroButtonToOverviewGestureStartsInLauncher()
-                        ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE_WITHOUT_PILFER
-                        : LauncherInstrumentation.GestureScope.OUTSIDE_WITHOUT_PILFER;
-
         mLauncher.sendPointer(downTime, SystemClock.uptimeMillis(),
-                MotionEvent.ACTION_UP, end, gestureScope);
+                MotionEvent.ACTION_UP, end,
+                LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
     }
 
     /**
@@ -235,71 +212,48 @@
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "want to quick switch to the previous app")) {
             verifyActiveContainer();
-            final boolean launcherWasVisible = mLauncher.isLauncherVisible();
-            switch (mLauncher.getNavigationModel()) {
-                case ZERO_BUTTON: {
-                    final int startX;
-                    final int startY;
-                    final int endX;
-                    final int endY;
-                    final int cornerRadius = (int) Math.ceil(mLauncher.getWindowCornerRadius());
-                    if (toRight) {
-                        // Swipe from the bottom left to the bottom right of the screen.
-                        startX = cornerRadius;
-                        startY = getSwipeStartY();
-                        endX = mLauncher.getDevice().getDisplayWidth() - cornerRadius;
-                        endY = startY;
-                    } else {
-                        // Swipe from the bottom right to the bottom left of the screen.
-                        startX = mLauncher.getDevice().getDisplayWidth() - cornerRadius;
-                        startY = getSwipeStartY();
-                        endX = cornerRadius;
-                        endY = startY;
-                    }
-
-                    final boolean isZeroButton = mLauncher.getNavigationModel()
-                            == LauncherInstrumentation.NavigationModel.ZERO_BUTTON;
-                    LauncherInstrumentation.GestureScope gestureScope =
-                            launcherWasVisible && isZeroButton
-                                    ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
-                                    : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER;
-                    mLauncher.executeAndWaitForEvent(
-                            () -> mLauncher.linearGesture(
-                                    startX, startY, endX, endY, 20, false, gestureScope),
-                            event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
-                            () -> "Quick switch gesture didn't change window state", "swiping");
-                    break;
+            if (mLauncher.getNavigationModel() == NavigationModel.ZERO_BUTTON
+                    || mLauncher.getTrackpadGestureType() == TrackpadGestureType.FOUR_FINGER) {
+                final int startX;
+                final int startY;
+                final int endX;
+                final int endY;
+                final int cornerRadius = (int) Math.ceil(mLauncher.getWindowCornerRadius());
+                if (toRight) {
+                    // Swipe from the bottom left to the bottom right of the screen.
+                    startX = cornerRadius;
+                    startY = getSwipeStartY();
+                    endX = mLauncher.getDevice().getDisplayWidth() - cornerRadius;
+                    endY = startY;
+                } else {
+                    // Swipe from the bottom right to the bottom left of the screen.
+                    startX = mLauncher.getDevice().getDisplayWidth() - cornerRadius;
+                    startY = getSwipeStartY();
+                    endX = cornerRadius;
+                    endY = startY;
                 }
 
-                case THREE_BUTTON:
-                    // Double press the recents button.
-                    UiObject2 recentsButton = mLauncher.waitForNavigationUiObject("recent_apps");
-                    if (mLauncher.isTablet()) {
-                        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
-                                LauncherInstrumentation.EVENT_TOUCH_DOWN);
-                        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
-                                LauncherInstrumentation.EVENT_TOUCH_UP);
-                    }
-                    mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
-                    mLauncher.runToState(() -> recentsButton.click(), OVERVIEW_STATE_ORDINAL,
-                            "clicking Recents button for the first time");
-                    mLauncher.getOverview();
-                    if (mLauncher.isTablet()) {
-                        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
-                                LauncherInstrumentation.EVENT_TOUCH_DOWN);
-                        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
-                                LauncherInstrumentation.EVENT_TOUCH_UP);
-                    }
-                    mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
-                    mLauncher.executeAndWaitForEvent(
-                            () -> recentsButton.click(),
-                            event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
-                            () -> "Pressing recents button didn't change window state",
-                            "clicking Recents button for the second time");
-                    break;
+                mLauncher.executeAndWaitForEvent(
+                        () -> mLauncher.linearGesture(
+                                startX, startY, endX, endY, 20, false,
+                                LauncherInstrumentation.GestureScope.EXPECT_PILFER),
+                        event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
+                        () -> "Quick switch gesture didn't change window state", "swiping");
+            } else {
+                // Double press the recents button.
+                UiObject2 recentsButton = mLauncher.waitForNavigationUiObject("recent_apps");
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+                mLauncher.runToState(() -> recentsButton.click(), OVERVIEW_STATE_ORDINAL,
+                        "clicking Recents button for the first time");
+                mLauncher.getOverview();
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+                mLauncher.executeAndWaitForEvent(
+                        () -> recentsButton.click(),
+                        event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
+                        () -> "Pressing recents button didn't change window state",
+                        "clicking Recents button for the second time");
             }
             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
-            return;
         }
     }
 
@@ -312,6 +266,8 @@
     }
 
     protected int getSwipeStartY() {
-        return mLauncher.getRealDisplaySize().y - 1;
+        return mLauncher.getTrackpadGestureType() == TrackpadGestureType.THREE_FINGER
+                ? mLauncher.getDevice().getDisplayHeight() * 3 / 4
+                : mLauncher.getRealDisplaySize().y - 1;
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 2c3c028..aa5c770 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -324,13 +324,16 @@
         if (!hasTasks() || isClearAllVisible()) {
             return false;
         }
-        OverviewTask task = mLauncher.isTablet() ? getFocusedTaskForTablet() : getCurrentTask();
+        boolean isTablet = mLauncher.isTablet();
+        if (isTablet && mLauncher.isGridOnlyOverviewEnabled()) {
+            return false;
+        }
+        OverviewTask task = isTablet ? getFocusedTaskForTablet() : getCurrentTask();
         if (task == null) {
             return false;
         }
         // In tablets, if focused task is not in center, overview actions aren't visible.
-        if (mLauncher.isTablet()
-                && Math.abs(task.getExactCenterX() - mLauncher.getExactScreenCenterX()) >= 1) {
+        if (isTablet && Math.abs(task.getExactCenterX() - mLauncher.getExactScreenCenterX()) >= 1) {
             return false;
         }
         // Overview actions aren't visible for split screen tasks.
diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java
index ee9dd1a..252435b 100644
--- a/tests/tapl/com/android/launcher3/tapl/Home.java
+++ b/tests/tapl/com/android/launcher3/tapl/Home.java
@@ -59,11 +59,6 @@
     }
 
     @Override
-    protected boolean zeroButtonToOverviewGestureStartsInLauncher() {
-        return true;
-    }
-
-    @Override
     protected boolean zeroButtonToOverviewGestureStateTransitionWhileHolding() {
         return true;
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
index 1239d7a..2951901 100644
--- a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
@@ -30,23 +30,28 @@
     }
 
     /**
-     * Swipes down to Workspace.
+     * Swipes up or down to dismiss to Workspace.
+     * @param swipeDown Swipe all apps down to dismiss, otherwise swipe up to dismiss by going home.
      *
      * @return the Workspace object.
      */
     @NonNull
-    public Workspace switchToWorkspace() {
+    public Workspace switchToWorkspace(boolean swipeDown) {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to switch from all apps to workspace")) {
             UiObject2 allAppsContainer = verifyActiveContainer();
 
             final int startX = allAppsContainer.getVisibleCenter().x;
-            final int startY = getTopVisibleIconBounds(allAppsContainer).centerY();
-            final int endY = mLauncher.getDevice().getDisplayHeight();
+            final int startY = swipeDown ? getTopVisibleIconBounds(allAppsContainer).centerY()
+                    : mLauncher.getDevice().getDisplayHeight();
+            final int endY =
+                    swipeDown ? mLauncher.getDevice().getDisplayHeight() : getTopVisibleIconBounds(
+                            allAppsContainer).centerY();
             LauncherInstrumentation.log(
                     "switchToWorkspace: startY = " + startY + ", endY = " + endY
-                            + ", slop = " + mLauncher.getTouchSlop());
+                            + ", slop = " + mLauncher.getTouchSlop()
+                            + ", swipeDown = " + swipeDown);
 
             mLauncher.swipeToState(
                     startX,
@@ -54,7 +59,9 @@
                     startX,
                     endY,
                     5 /* steps */,
-                    NORMAL_STATE_ORDINAL, LauncherInstrumentation.GestureScope.INSIDE);
+                    NORMAL_STATE_ORDINAL,
+                    swipeDown ? LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER
+                            : LauncherInstrumentation.GestureScope.EXPECT_PILFER);
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
                     "swiped to workspace")) {
@@ -110,11 +117,8 @@
         }
     }
 
-    /**
-     * Return the QSB UI object on the AllApps screen.
-     * @return the QSB UI object.
-     */
     @NonNull
+    @Override
     public Qsb getQsb() {
         return new AllAppsQsb(mLauncher, verifyActiveContainer());
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/HomeQsb.java b/tests/tapl/com/android/launcher3/tapl/HomeQsb.java
index 20d09a1..5385c65 100644
--- a/tests/tapl/com/android/launcher3/tapl/HomeQsb.java
+++ b/tests/tapl/com/android/launcher3/tapl/HomeQsb.java
@@ -22,16 +22,7 @@
  */
 class HomeQsb extends Qsb {
 
-    private final UiObject2 mHotSeat;
-
     HomeQsb(LauncherInstrumentation launcher, UiObject2 hotseat) {
-        super(launcher);
-        mHotSeat = hotseat;
-        waitForQsbObject();
-    }
-
-    @Override
-    protected UiObject2 waitForQsbObject() {
-        return mLauncher.waitForObjectInContainer(mHotSeat, "search_container_hotseat");
+        super(launcher, hotseat, "search_container_hotseat");
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 48e327f..a953fa9 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -152,7 +152,7 @@
                 downTime,
                 MotionEvent.ACTION_DOWN,
                 iconCenter,
-                LauncherInstrumentation.GestureScope.INSIDE);
+                LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
         LauncherInstrumentation.log("movePointerForStartDrag: sent down");
         expectLongClickEvents.run();
         waitForLongPressConfirmation();
@@ -165,7 +165,7 @@
                 downTime,
                 downTime,
                 /* slowDown= */ true,
-                LauncherInstrumentation.GestureScope.INSIDE);
+                LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
     }
 
     private int getStartDragThreshold() {
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index a59eff7..30417c0 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -30,6 +30,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.SystemClock;
+import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
@@ -37,6 +38,7 @@
 import androidx.test.uiautomator.Condition;
 import androidx.test.uiautomator.UiDevice;
 
+import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.testing.shared.TestProtocol;
 
 /**
@@ -50,6 +52,8 @@
     // UNSTASHED_TASKBAR_HANDLE_HINT_SCALE value from TaskbarStashController.
     private static final float UNSTASHED_TASKBAR_HANDLE_HINT_SCALE = 1.1f;
 
+    private static final int STASHED_TASKBAR_BOTTOM_EDGE_DP = 1;
+
     private final Condition<UiDevice, Boolean> mStashedTaskbarHintScaleCondition =
             device -> mLauncher.getTestInfo(REQUEST_STASHED_TASKBAR_SCALE).getFloat(
                     TestProtocol.TEST_INFO_RESPONSE_FIELD) - UNSTASHED_TASKBAR_HANDLE_HINT_SCALE
@@ -124,13 +128,13 @@
                     mLauncher.getRealDisplaySize().x / 2, unstashTargetY);
 
             mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, unstashTarget,
-                    LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER);
+                    LauncherInstrumentation.GestureScope.EXPECT_PILFER);
             LauncherInstrumentation.log("showTaskbar: sent down");
 
             try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("pressed down")) {
                 mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
                 mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, unstashTarget,
-                        LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER);
+                        LauncherInstrumentation.GestureScope.EXPECT_PILFER);
 
                 return new Taskbar(mLauncher);
             }
@@ -179,7 +183,7 @@
                         downTime,
                         SystemClock.uptimeMillis(),
                         /* slowDown= */ false,
-                        LauncherInstrumentation.GestureScope.INSIDE);
+                        LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
 
                 try (LauncherInstrumentation.Closable c3 = launcher.addContextLayer(
                         "moved pointer to drop point")) {
@@ -190,7 +194,7 @@
                             SystemClock.uptimeMillis(),
                             MotionEvent.ACTION_UP,
                             endPoint,
-                            LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE_WITHOUT_PILFER);
+                            LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
                     LauncherInstrumentation.log("SplitscreenDragSource.dragToSplitscreen: "
                             + "after drop");
 
@@ -209,7 +213,7 @@
      *
      * <p>This unstashing occurs when not actively hovering the taskbar.
      */
-    public void hoverScreenBottomEdgeToUnstashTaskbar() {
+    public Taskbar hoverScreenBottomEdgeToUnstashTaskbar() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "cursor hover entering screen edge to unstash taskbar")) {
@@ -220,19 +224,23 @@
             int leftEdge = 10;
             Point taskbarUnstashArea = new Point(leftEdge, mLauncher.getRealDisplaySize().y - 1);
             mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_ENTER,
-                    new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null);
+                    new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null,
+                    InputDevice.SOURCE_MOUSE);
 
             mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
 
             mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
-                    new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null);
+                    new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null,
+                    InputDevice.SOURCE_MOUSE);
+
+            return new Taskbar(mLauncher);
         }
     }
 
     /**
      * Emulate the cursor hovering the taskbar to get unstash hint, then hovering below to unstash.
      */
-    public void hoverBelowHintedTaskbarToUnstash() {
+    public Taskbar hoverBelowHintedTaskbarToUnstash() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "cursor hover entering stashed taskbar")) {
@@ -240,7 +248,8 @@
             Point stashedTaskbarHintArea = new Point(mLauncher.getRealDisplaySize().x / 2,
                     mLauncher.getRealDisplaySize().y - 1);
             mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_ENTER,
-                    new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y), null);
+                    new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y), null,
+                    InputDevice.SOURCE_MOUSE);
 
             mLauncher.getDevice().wait(mStashedTaskbarHintScaleCondition,
                     LauncherInstrumentation.WAIT_TIME_MS);
@@ -251,9 +260,11 @@
                 Point taskbarUnstashArea = new Point(mLauncher.getRealDisplaySize().x / 2,
                         mLauncher.getRealDisplaySize().y - 1);
                 mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
-                        new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null);
+                        new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null,
+                        InputDevice.SOURCE_MOUSE);
 
                 mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
+                return new Taskbar(mLauncher);
             }
         }
     }
@@ -288,4 +299,48 @@
             }
         }
     }
+
+    /**
+     * Emulate the cursor clicking the stashed taskbar to go home.
+     */
+    public Workspace clickStashedTaskbarToGoHome() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "cursor hover entering stashed taskbar")) {
+            long downTime = SystemClock.uptimeMillis();
+            int stashedTaskbarBottomEdge = ResourceUtils.pxFromDp(STASHED_TASKBAR_BOTTOM_EDGE_DP,
+                    mLauncher.getResources().getDisplayMetrics());
+            Point stashedTaskbarHintArea = new Point(mLauncher.getRealDisplaySize().x / 2,
+                    mLauncher.getRealDisplaySize().y - stashedTaskbarBottomEdge - 1);
+            mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_ENTER,
+                    new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y), null,
+                    InputDevice.SOURCE_MOUSE);
+
+            mLauncher.getDevice().wait(mStashedTaskbarHintScaleCondition,
+                    LauncherInstrumentation.WAIT_TIME_MS);
+
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "cursor clicking stashed taskbar to go home")) {
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        null, InputDevice.SOURCE_MOUSE);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER,
+                        InputDevice.SOURCE_MOUSE);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_BUTTON_PRESS,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        null, InputDevice.SOURCE_MOUSE);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_BUTTON_RELEASE,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        null, InputDevice.SOURCE_MOUSE);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER,
+                        InputDevice.SOURCE_MOUSE);
+
+                return mLauncher.getWorkspace();
+            }
+        }
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index c4f5da5..c58ae16 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -20,6 +20,7 @@
 import static android.content.pm.PackageManager.DONT_KILL_APP;
 import static android.content.pm.PackageManager.MATCH_ALL;
 import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
+import static android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT;
 
 import static com.android.launcher3.tapl.Folder.FOLDER_CONTENT_RES_ID;
 import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName;
@@ -98,18 +99,9 @@
     private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 15;
     private static final int GESTURE_STEP_MS = 16;
 
-    static final Pattern EVENT_TOUCH_DOWN = getTouchEventPattern("ACTION_DOWN");
-    static final Pattern EVENT_TOUCH_UP = getTouchEventPattern("ACTION_UP");
-    private static final Pattern EVENT_TOUCH_CANCEL = getTouchEventPattern("ACTION_CANCEL");
     static final Pattern EVENT_PILFER_POINTERS = Pattern.compile("pilferPointers");
     static final Pattern EVENT_START = Pattern.compile("start:");
 
-    static final Pattern EVENT_TOUCH_DOWN_TIS = getTouchEventPatternTIS("ACTION_DOWN");
-    static final Pattern EVENT_TOUCH_UP_TIS = getTouchEventPatternTIS("ACTION_UP");
-    static final Pattern EVENT_TOUCH_CANCEL_TIS = getTouchEventPatternTIS("ACTION_CANCEL");
-    static final Pattern EVENT_HOVER_ENTER_TIS = getTouchEventPatternTIS("ACTION_HOVER_ENTER");
-    static final Pattern EVENT_HOVER_EXIT_TIS = getTouchEventPatternTIS("ACTION_HOVER_EXIT");
-
     private static final Pattern EVENT_KEY_BACK_DOWN =
             getKeyEventPattern("ACTION_DOWN", "KEYCODE_BACK");
     private static final Pattern EVENT_KEY_BACK_UP =
@@ -129,26 +121,19 @@
 
     public enum NavigationModel {ZERO_BUTTON, THREE_BUTTON}
 
-    // Where the gesture happens: outside of Launcher, inside or from inside to outside and
-    // whether the gesture recognition triggers pilfer.
+    // Defines whether the gesture recognition triggers pilfer.
     public enum GestureScope {
-        OUTSIDE_WITHOUT_PILFER, OUTSIDE_WITH_PILFER, INSIDE, INSIDE_TO_OUTSIDE,
-        INSIDE_TO_OUTSIDE_WITHOUT_PILFER,
-        INSIDE_TO_OUTSIDE_WITH_KEYCODE, // For gestures that will trigger a keycode from TIS.
-        OUTSIDE_WITH_KEYCODE,
+        DONT_EXPECT_PILFER,
+        EXPECT_PILFER,
     }
 
-    /**
-     * Represents a point in the code at which a callback can run.
-     */
-    public enum CALLBACK_RUN_POINT {
-        CALLBACK_HOLD_BEFORE_DROP,
-        CALLBACK_HOVER_ENTER,
-        CALLBACK_HOVER_EXIT,
+    public enum TrackpadGestureType {
+        NONE,
+        TWO_FINGER,
+        THREE_FINGER,
+        FOUR_FINGER
     }
 
-    private Consumer<CALLBACK_RUN_POINT> mCallbackAtRunPoint = null;
-
     // Base class for launcher containers.
     abstract static class VisibleContainer {
         protected final LauncherInstrumentation mLauncher;
@@ -176,7 +161,7 @@
         void close();
     }
 
-    private static final String WORKSPACE_RES_ID = "workspace";
+    static final String WORKSPACE_RES_ID = "workspace";
     private static final String APPS_RES_ID = "apps_view";
     private static final String OVERVIEW_RES_ID = "overview_panel";
     private static final String WIDGETS_RES_ID = "primary_widgets_list_view";
@@ -208,20 +193,8 @@
     private boolean mCheckEventsForSuccessfulGestures = false;
     private Runnable mOnLauncherCrashed;
 
-    private static Pattern getTouchEventPattern(String prefix, String action) {
-        // The pattern includes checks that we don't get a multi-touch events or other surprises.
-        return Pattern.compile(
-                prefix + ": MotionEvent.*?action=" + action + ".*?id\\[0\\]=0"
-                        + ".*?toolType\\[0\\]=TOOL_TYPE_FINGER.*?buttonState=0.*?pointerCount=1");
-    }
-
-    private static Pattern getTouchEventPattern(String action) {
-        return getTouchEventPattern("Touch event", action);
-    }
-
-    private static Pattern getTouchEventPatternTIS(String action) {
-        return getTouchEventPattern("TouchInteractionService.onInputEvent", action);
-    }
+    private TrackpadGestureType mTrackpadGestureType = TrackpadGestureType.NONE;
+    private int mPointerCount = 0;
 
     private static Pattern getKeyEventPattern(String action, String keyCode) {
         return Pattern.compile("Key event: KeyEvent.*action=" + action + ".*keyCode=" + keyCode);
@@ -715,6 +688,19 @@
     }
 
     /**
+     * Set the trackpad gesture type of the interaction.
+     * @param trackpadGestureType whether it's not from trackpad, two-finger, three-finger, or
+     *                            four-finger gesture.
+     */
+    public void setTrackpadGestureType(TrackpadGestureType trackpadGestureType) {
+        mTrackpadGestureType = trackpadGestureType;
+    }
+
+    TrackpadGestureType getTrackpadGestureType() {
+        return mTrackpadGestureType;
+    }
+
+    /**
      * Sets expected rotation.
      * TAPL periodically checks that Launcher didn't suddenly change the rotation to unexpected one.
      * Null parameter disables checks. The initial state is "no checks".
@@ -957,8 +943,8 @@
         GestureScope gestureScope = gestureStartFromLauncher
                 // Without the navigation bar layer, the gesture scope on tablets remains inside the
                 // launcher process.
-                ? (isTablet() ? GestureScope.INSIDE : GestureScope.INSIDE_TO_OUTSIDE)
-                : GestureScope.OUTSIDE_WITH_PILFER;
+                ? (isTablet() ? GestureScope.DONT_EXPECT_PILFER : GestureScope.EXPECT_PILFER)
+                : GestureScope.EXPECT_PILFER;
         linearGesture(
                 displaySize.x / 2, displaySize.y - 1,
                 displaySize.x / 2, 0,
@@ -1003,8 +989,11 @@
             // We need waiting for any accessibility event generated after pressing Home because
             // otherwise waitForIdle may return immediately in case when there was a big enough
             // pause in accessibility events prior to pressing Home.
+            boolean isThreeFingerTrackpadGesture =
+                    mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
             final String action;
-            if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
+            if (getNavigationModel() == NavigationModel.ZERO_BUTTON
+                    || isThreeFingerTrackpadGesture) {
                 checkForAnomaly(false, true);
 
                 final Point displaySize = getRealDisplaySize();
@@ -1020,21 +1009,19 @@
                 } else {
                     action = "swiping up to home";
 
+                    int startY = isThreeFingerTrackpadGesture ? displaySize.y * 3 / 4
+                            : displaySize.y - 1;
+                    int endY = isThreeFingerTrackpadGesture ? displaySize.y / 4 : displaySize.y / 2;
                     swipeToState(
-                            displaySize.x / 2, displaySize.y - 1,
-                            displaySize.x / 2, displaySize.y / 2,
+                            displaySize.x / 2, startY,
+                            displaySize.x / 2, endY,
                             ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL,
-                            gestureStartFromLauncher ? GestureScope.INSIDE_TO_OUTSIDE
-                                    : GestureScope.OUTSIDE_WITH_PILFER);
+                            GestureScope.EXPECT_PILFER);
                 }
             } else {
                 log("Hierarchy before clicking home:");
                 dumpViewHierarchy();
                 action = "clicking home button";
-                if (isTablet()) {
-                    expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
-                    expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_UP);
-                }
 
                 runToState(
                         waitForNavigationUiObject("home")::click,
@@ -1059,21 +1046,19 @@
             waitForLauncherInitialized();
             final boolean launcherVisible =
                     isTablet() ? isLauncherContainerVisible() : isLauncherVisible();
-            if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
+            boolean isThreeFingerTrackpadGesture =
+                    mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
+            if (getNavigationModel() == NavigationModel.ZERO_BUTTON
+                    || isThreeFingerTrackpadGesture) {
                 final Point displaySize = getRealDisplaySize();
-                final GestureScope gestureScope =
-                        launcherVisible ? GestureScope.INSIDE_TO_OUTSIDE_WITH_KEYCODE
-                                : GestureScope.OUTSIDE_WITH_KEYCODE;
                 // TODO(b/225505986): change startY and endY back to displaySize.y / 2 once the
                 //  issue is solved.
-                linearGesture(0, displaySize.y / 4, displaySize.x / 2, displaySize.y / 4,
-                        10, false, gestureScope);
+                int startX = isThreeFingerTrackpadGesture ? displaySize.x / 4 : 0;
+                int endX = isThreeFingerTrackpadGesture ? displaySize.x * 3 / 4 : displaySize.x / 2;
+                linearGesture(startX, displaySize.y / 4, endX, displaySize.y / 4,
+                        10, false, GestureScope.DONT_EXPECT_PILFER);
             } else {
                 waitForNavigationUiObject("back").click();
-                if (isTablet()) {
-                    expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
-                    expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_UP);
-                }
             }
             if (launcherVisible) {
                 if (getContext().getApplicationInfo().isOnBackInvokedCallbackEnabled()) {
@@ -1481,15 +1466,17 @@
      * animations because in some scenarios there is a playing animations when the click is
      * attempted.
      */
-    void clickObject(UiObject2 uiObject, GestureScope gestureScope) {
+    void clickObject(UiObject2 uiObject) {
         final long clickTime = SystemClock.uptimeMillis();
         final Point center = uiObject.getVisibleCenter();
-        sendPointer(clickTime, clickTime, MotionEvent.ACTION_DOWN, center, gestureScope);
-        sendPointer(clickTime, clickTime, MotionEvent.ACTION_UP, center, gestureScope);
+        sendPointer(clickTime, clickTime, MotionEvent.ACTION_DOWN, center,
+                GestureScope.DONT_EXPECT_PILFER);
+        sendPointer(clickTime, clickTime, MotionEvent.ACTION_UP, center,
+                GestureScope.DONT_EXPECT_PILFER);
     }
 
     void clickLauncherObject(UiObject2 object) {
-        clickObject(object, GestureScope.INSIDE);
+        clickObject(object);
     }
 
     void scrollToLastVisibleRow(
@@ -1582,7 +1569,8 @@
 
         executeAndWaitForLauncherEvent(
                 () -> linearGesture(
-                        startX, startY, endX, endY, steps, slowDown, GestureScope.INSIDE),
+                        startX, startY, endX, endY, steps, slowDown,
+                        GestureScope.DONT_EXPECT_PILFER),
                 event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
                 () -> "Didn't receive a scroll end message: " + startX + ", " + startY
                         + ", " + endX + ", " + endY,
@@ -1592,18 +1580,43 @@
     // Inject a swipe gesture. Inject exactly 'steps' motion points, incrementing event time by a
     // fixed interval each time.
     public void linearGesture(int startX, int startY, int endX, int endY, int steps,
-            boolean slowDown,
-            GestureScope gestureScope) {
+            boolean slowDown, GestureScope gestureScope) {
         log("linearGesture: " + startX + ", " + startY + " -> " + endX + ", " + endY);
         final long downTime = SystemClock.uptimeMillis();
         final Point start = new Point(startX, startY);
         final Point end = new Point(endX, endY);
         sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
+        if (mTrackpadGestureType != TrackpadGestureType.NONE) {
+            sendPointer(downTime, downTime, getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 1),
+                    start, gestureScope);
+            if (mTrackpadGestureType == TrackpadGestureType.THREE_FINGER
+                    || mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) {
+                sendPointer(downTime, downTime,
+                        getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 2),
+                        start, gestureScope);
+                if (mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) {
+                    sendPointer(downTime, downTime,
+                            getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 3),
+                            start, gestureScope);
+                }
+            }
+        }
         final long endTime = movePointer(
                 start, end, steps, false, downTime, downTime, slowDown, gestureScope);
+        if (mTrackpadGestureType != TrackpadGestureType.NONE) {
+            for (int i = mPointerCount; i >= 2; i--) {
+                sendPointer(downTime, downTime,
+                        getPointerAction(MotionEvent.ACTION_POINTER_UP, i - 1),
+                        start, gestureScope);
+            }
+        }
         sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope);
     }
 
+    private static int getPointerAction(int action, int index) {
+        return action + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
+    }
+
     long movePointer(Point start, Point end, int steps, boolean isDecelerating, long downTime,
             long startTime, boolean slowDown, GestureScope gestureScope) {
         long endTime = movePointer(downTime, startTime, steps * GESTURE_STEP_MS,
@@ -1627,74 +1640,104 @@
         return getContext().getResources();
     }
 
-    private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
-            float x, float y) {
-        MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
-        properties.id = 0;
-        properties.toolType = Configurator.getInstance().getToolType();
+    private static MotionEvent getTrackpadMotionEvent(long downTime, long eventTime,
+            int action, float x, float y, int pointerCount, TrackpadGestureType gestureType) {
+        MotionEvent.PointerProperties[] pointerProperties =
+                new MotionEvent.PointerProperties[pointerCount];
+        MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
+        boolean isMultiFingerGesture = gestureType != TrackpadGestureType.TWO_FINGER;
+        for (int i = 0; i < pointerCount; i++) {
+            pointerProperties[i] = getPointerProperties(i);
+            pointerCoords[i] = getPointerCoords(x, y);
+            if (isMultiFingerGesture) {
+                pointerCoords[i].setAxisValue(AXIS_GESTURE_SWIPE_FINGER_COUNT,
+                        gestureType == TrackpadGestureType.THREE_FINGER ? 3 : 4);
+            }
+        }
+        return MotionEvent.obtain(downTime, eventTime, action, pointerCount, pointerProperties,
+                pointerCoords, 0, 0, 1.0f, 1.0f, 0, 0,
+                InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_CLASS_POINTER, 0, 0,
+                isMultiFingerGesture ? MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE
+                        : MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE);
+    }
 
+    private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
+            float x, float y, int source) {
+        return MotionEvent.obtain(downTime, eventTime, action, 1,
+                new MotionEvent.PointerProperties[]{getPointerProperties(0)},
+                new MotionEvent.PointerCoords[]{getPointerCoords(x, y)},
+                0, 0, 1.0f, 1.0f, 0, 0, source, 0);
+    }
+
+    private static MotionEvent.PointerProperties getPointerProperties(int pointerId) {
+        MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
+        properties.id = pointerId;
+        properties.toolType = Configurator.getInstance().getToolType();
+        return properties;
+    }
+
+    private static MotionEvent.PointerCoords getPointerCoords(float x, float y) {
         MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
         coords.pressure = 1;
         coords.size = 1;
         coords.x = x;
         coords.y = y;
-
-        return MotionEvent.obtain(downTime, eventTime, action, 1,
-                new MotionEvent.PointerProperties[]{properties},
-                new MotionEvent.PointerCoords[]{coords},
-                0, 0, 1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+        return coords;
     }
 
     private boolean hasTIS() {
-        return getTestInfo(TestProtocol.REQUEST_HAS_TIS).getBoolean(TestProtocol.REQUEST_HAS_TIS);
+        return getTestInfo(TestProtocol.REQUEST_HAS_TIS).getBoolean(
+                TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    boolean isGridOnlyOverviewEnabled() {
+        return getTestInfo(TestProtocol.REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW).getBoolean(
+                TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
 
     public void sendPointer(long downTime, long currentTime, int action, Point point,
             GestureScope gestureScope) {
+        sendPointer(downTime, currentTime, action, point, gestureScope,
+                InputDevice.SOURCE_TOUCHSCREEN);
+    }
+
+    public void sendPointer(long downTime, long currentTime, int action, Point point,
+            GestureScope gestureScope, int source) {
         final boolean hasTIS = hasTIS();
-        switch (action) {
+        int pointerCount = mPointerCount;
+
+        boolean isTrackpadGesture = mTrackpadGestureType != TrackpadGestureType.NONE;
+        switch (action & MotionEvent.ACTION_MASK) {
             case MotionEvent.ACTION_DOWN:
-                if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
-                        && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER
-                        && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE) {
-                    expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
-                }
-                if (hasTIS && getNavigationModel() != NavigationModel.THREE_BUTTON) {
-                    expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
+                if (isTrackpadGesture) {
+                    mPointerCount = 1;
+                    pointerCount = mPointerCount;
                 }
                 break;
             case MotionEvent.ACTION_UP:
-                if (hasTIS && gestureScope != GestureScope.INSIDE
-                        && gestureScope != GestureScope.INSIDE_TO_OUTSIDE_WITHOUT_PILFER
-                        && (gestureScope == GestureScope.OUTSIDE_WITH_PILFER
-                        || gestureScope == GestureScope.INSIDE_TO_OUTSIDE)) {
+                if (hasTIS && gestureScope == GestureScope.EXPECT_PILFER) {
                     expectEvent(TestProtocol.SEQUENCE_PILFER, EVENT_PILFER_POINTERS);
                 }
-                if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
-                        && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER
-                        && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE) {
-                    expectEvent(TestProtocol.SEQUENCE_MAIN,
-                            gestureScope == GestureScope.INSIDE
-                                    || gestureScope == GestureScope.OUTSIDE_WITHOUT_PILFER
-                                    ? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL);
-                }
-                if (hasTIS && getNavigationModel() != NavigationModel.THREE_BUTTON) {
-                    expectEvent(TestProtocol.SEQUENCE_TIS,
-                            gestureScope == GestureScope.INSIDE_TO_OUTSIDE_WITH_KEYCODE
-                                    || gestureScope == GestureScope.OUTSIDE_WITH_KEYCODE
-                                    ? EVENT_TOUCH_CANCEL_TIS : EVENT_TOUCH_UP_TIS);
-                }
                 break;
-            case MotionEvent.ACTION_HOVER_ENTER:
-                expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_HOVER_ENTER_TIS);
+            case MotionEvent.ACTION_POINTER_DOWN:
+                mPointerCount++;
+                pointerCount = mPointerCount;
                 break;
-            case MotionEvent.ACTION_HOVER_EXIT:
-                expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_HOVER_EXIT_TIS);
+            case MotionEvent.ACTION_POINTER_UP:
+                // When the gesture is handled outside, it's cancelled within launcher.
+                mPointerCount--;
                 break;
         }
 
-        final MotionEvent event = getMotionEvent(downTime, currentTime, action, point.x, point.y);
+        final MotionEvent event = isTrackpadGesture
+                ? getTrackpadMotionEvent(
+                        downTime, currentTime, action, point.x, point.y, pointerCount,
+                        mTrackpadGestureType)
+                : getMotionEvent(downTime, currentTime, action, point.x, point.y, source);
+        if (action == MotionEvent.ACTION_BUTTON_PRESS
+                || action == MotionEvent.ACTION_BUTTON_RELEASE) {
+            event.setActionButton(MotionEvent.BUTTON_PRIMARY);
+        }
         assertTrue("injectInputEvent failed",
                 mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
         event.recycle();
@@ -1702,8 +1745,7 @@
 
     public long movePointer(long downTime, long startTime, long duration, Point from, Point to,
             GestureScope gestureScope) {
-        return movePointer(
-                downTime, startTime, duration, false, from, to, gestureScope);
+        return movePointer(downTime, startTime, duration, false, from, to, gestureScope);
     }
 
     public long movePointer(long downTime, long startTime, long duration, boolean isDecelerating,
@@ -1763,11 +1805,12 @@
             @NonNull final UiObject2 target, @NonNull String resName, Pattern longClickEvent) {
         final Point targetCenter = target.getVisibleCenter();
         final long downTime = SystemClock.uptimeMillis();
-        sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter, GestureScope.INSIDE);
+        sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter,
+                GestureScope.DONT_EXPECT_PILFER);
         expectEvent(TestProtocol.SEQUENCE_MAIN, longClickEvent);
         final UiObject2 result = waitForLauncherObject(resName);
         sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter,
-                GestureScope.INSIDE);
+                GestureScope.DONT_EXPECT_PILFER);
         return result;
     }
 
@@ -1851,6 +1894,12 @@
         return tasks;
     }
 
+    /** Reinitializes the workspace to its default layout. */
+    public void reinitializeLauncherData() {
+        getTestInfo(TestProtocol.REQUEST_REINITIALIZE_DATA);
+    }
+
+    /** Clears the workspace, leaving it empty. */
     public void clearLauncherData() {
         getTestInfo(TestProtocol.REQUEST_CLEAR_DATA);
     }
@@ -2042,25 +2091,9 @@
             final long downTime = SystemClock.uptimeMillis();
             final Point tapTarget = new Point(x, y);
             sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, tapTarget,
-                    LauncherInstrumentation.GestureScope.INSIDE);
+                    LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
             sendPointer(downTime, downTime, MotionEvent.ACTION_UP, tapTarget,
-                    LauncherInstrumentation.GestureScope.INSIDE);
-        }
-    }
-
-    /**
-     * Sets the consumer to run callbacks at all run-points.
-     */
-    public void setRunPointCallback(Consumer<CALLBACK_RUN_POINT> callback) {
-        mCallbackAtRunPoint = callback;
-    }
-
-    /**
-     * Runs the callback at the specified point if it exists.
-     */
-    void runCallbackIfActive(CALLBACK_RUN_POINT runPoint) {
-        if (mCallbackAtRunPoint != null) {
-            mCallbackAtRunPoint.accept(runPoint);
+                    LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
         }
     }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
index 386deac..bd2c9c1 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
@@ -19,8 +19,6 @@
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.UiObject2;
 
-import com.android.launcher3.testing.shared.TestProtocol;
-
 /**
  * View containing overview actions
  */
@@ -51,13 +49,6 @@
                     "clicked screenshot button")) {
                 UiObject2 closeScreenshot = mLauncher.waitForSystemUiObject(
                         "screenshot_dismiss_image");
-                if (mLauncher.getNavigationModel()
-                        != LauncherInstrumentation.NavigationModel.THREE_BUTTON) {
-                    mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS,
-                            LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS);
-                    mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS,
-                            LauncherInstrumentation.EVENT_TOUCH_UP_TIS);
-                }
                 closeScreenshot.click();
                 try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
                         "dismissed screenshot")) {
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 39b93b4..e4cfc52 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -124,7 +124,7 @@
         final int centerY = taskBounds.centerY();
         mLauncher.executeAndWaitForLauncherEvent(
                 () -> mLauncher.linearGesture(centerX, centerY, centerX, 0, 10, false,
-                        LauncherInstrumentation.GestureScope.INSIDE),
+                        LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER),
                 event -> TestProtocol.DISMISS_ANIMATION_ENDS_MESSAGE.equals(event.getClassName()),
                 () -> "Didn't receive a dismiss animation ends message: " + centerX + ", "
                         + centerY, "swiping to dismiss");
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
index e349620..7c29a6c 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
@@ -50,6 +50,24 @@
         }
     }
 
+    /** Taps the app info item from the overview task menu and returns the LaunchedAppState
+     * representing the App info settings page. */
+    @NonNull
+    public LaunchedAppState tapAppInfoMenuItem() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "before tapping the app info menu item")) {
+            mLauncher.clickLauncherObject(
+                    mLauncher.findObjectInContainer(mMenu, By.text("App info")));
+
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "tapped app info menu item")) {
+                mLauncher.waitUntilSystemLauncherObjectGone("overview_panel");
+                return new LaunchedAppState(mLauncher);
+            }
+        }
+    }
+
     /** Returns true if an item matching the given string is present in the menu. */
     public boolean hasMenuItem(String expectedMenuItemText) {
         UiObject2 menuItem = mLauncher.findObjectInContainer(mMenu, By.text(expectedMenuItemText));
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java
index b2cc92d..e3035bf 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java
@@ -15,13 +15,7 @@
  */
 package com.android.launcher3.tapl;
 
-import static com.android.launcher3.tapl.LauncherInstrumentation.CALLBACK_RUN_POINT.CALLBACK_HOVER_ENTER;
-import static com.android.launcher3.tapl.LauncherInstrumentation.CALLBACK_RUN_POINT.CALLBACK_HOVER_EXIT;
-
-import android.graphics.Point;
 import android.graphics.Rect;
-import android.os.SystemClock;
-import android.view.MotionEvent;
 
 import androidx.test.uiautomator.UiObject2;
 
@@ -42,28 +36,4 @@
     public Rect getVisibleBounds() {
         return mMenuItem.getVisibleBounds();
     }
-
-    /**
-     * Emulate the cursor entering and exiting a hover over this menu item.
-     */
-    public void hoverCursor() {
-        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
-             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                     "cursor hover entering menu item")) {
-            long downTime = SystemClock.uptimeMillis();
-            mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_ENTER,
-                    new Point(mMenuItem.getVisibleCenter().x, mMenuItem.getVisibleCenter().y),
-                    null);
-            mLauncher.runCallbackIfActive(CALLBACK_HOVER_ENTER);
-
-            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
-                    "cursor hover exiting menu item")) {
-                downTime = SystemClock.uptimeMillis();
-                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
-                        new Point(mMenuItem.getVisibleCenter().x, mMenuItem.getVisibleCenter().y),
-                        null);
-                mLauncher.runCallbackIfActive(CALLBACK_HOVER_EXIT);
-            }
-        }
-    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Qsb.java b/tests/tapl/com/android/launcher3/tapl/Qsb.java
index 6bc4f21..7f3f61d 100644
--- a/tests/tapl/com/android/launcher3/tapl/Qsb.java
+++ b/tests/tapl/com/android/launcher3/tapl/Qsb.java
@@ -30,13 +30,21 @@
     private static final String ASSISTANT_APP_PACKAGE = "com.google.android.googlequicksearchbox";
     private static final String ASSISTANT_ICON_RES_ID = "mic_icon";
     protected final LauncherInstrumentation mLauncher;
+    private final UiObject2 mContainer;
+    private final String mQsbResName;
 
-    protected Qsb(LauncherInstrumentation launcher) {
+    protected Qsb(LauncherInstrumentation launcher, UiObject2 container, String qsbResName) {
         mLauncher = launcher;
+        mContainer = container;
+        mQsbResName = qsbResName;
+        waitForQsbObject();
     }
 
     // Waits for the quick search box.
-    protected abstract UiObject2 waitForQsbObject();
+    private UiObject2 waitForQsbObject() {
+        return mLauncher.waitForObjectInContainer(mContainer, mQsbResName);
+    }
+
     /**
      * Launch assistant app by tapping mic icon on qsb.
      */
@@ -79,8 +87,12 @@
             mLauncher.waitForIdle();
             try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
                     "clicked qsb to open search result page")) {
-                return new SearchResultFromQsb(mLauncher);
+                return createSearchResult();
             }
         }
     }
+
+    protected SearchResultFromQsb createSearchResult() {
+        return new SearchResultFromQsb(mLauncher);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
index 80176e9..d02e747 100644
--- a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
+++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
@@ -32,7 +32,7 @@
 
     // This particular ID change should happen with caution
     private static final String SEARCH_CONTAINER_RES_ID = "search_results_list_view";
-    private final LauncherInstrumentation mLauncher;
+    protected final LauncherInstrumentation mLauncher;
 
     SearchResultFromQsb(LauncherInstrumentation launcher) {
         mLauncher = launcher;
@@ -49,27 +49,33 @@
     }
 
     /** Find the app from search results with app name. */
-    public Launchable findAppIcon(String appName) {
+    public AppIcon findAppIcon(String appName) {
         UiObject2 icon = mLauncher.waitForLauncherObject(By.clazz(TextView.class).text(appName));
+        return createAppIcon(icon);
+    }
+
+    protected AppIcon createAppIcon(UiObject2 icon) {
         return new AllAppsAppIcon(mLauncher, icon);
     }
 
     /** Find the web suggestion from search suggestion's title text */
-    public void verifyWebSuggestIsPresent(String text) {
-        ArrayList<UiObject2> goldenGateResults =
+    public SearchWebSuggestion findWebSuggestion(String text) {
+        ArrayList<UiObject2> webSuggestions =
                 new ArrayList<>(mLauncher.waitForObjectsInContainer(
                         mLauncher.waitForSystemLauncherObject(SEARCH_CONTAINER_RES_ID),
                         By.clazz(TextView.class)));
-        boolean found = false;
-        for(UiObject2 uiObject: goldenGateResults) {
+        for (UiObject2 uiObject: webSuggestions) {
             String currentString = uiObject.getText();
             if (currentString.equals(text)) {
-                found = true;
+                return createWebSuggestion(uiObject);
             }
         }
-        if (!found) {
-            throw new IllegalStateException("Web suggestion title: " + text + " not found");
-        }
+        mLauncher.fail("Web suggestion title: " + text + " not found");
+        return null;
+    }
+
+    protected SearchWebSuggestion createWebSuggestion(UiObject2 webSuggestion) {
+        return new SearchWebSuggestion(mLauncher, webSuggestion);
     }
 
     /** Find the total amount of views being displayed and return the size */
diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java
new file mode 100644
index 0000000..6c6ab05
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import androidx.test.uiautomator.UiObject2;
+
+/**
+ * Operations on search result page opened from Taskbar qsb.
+ */
+public class SearchResultFromTaskbarQsb extends SearchResultFromQsb {
+
+    SearchResultFromTaskbarQsb(LauncherInstrumentation launcher) {
+        super(launcher);
+    }
+
+    @Override
+    public TaskbarAppIcon findAppIcon(String appName) {
+        return (TaskbarAppIcon) super.findAppIcon(appName);
+    }
+
+    @Override
+    protected TaskbarAppIcon createAppIcon(UiObject2 icon) {
+        return new TaskbarAppIcon(mLauncher, icon);
+    }
+
+    @Override
+    public TaskbarSearchWebSuggestion findWebSuggestion(String text) {
+        return (TaskbarSearchWebSuggestion) super.findWebSuggestion(text);
+    }
+
+    @Override
+    protected TaskbarSearchWebSuggestion createWebSuggestion(UiObject2 webSuggestion) {
+        return new TaskbarSearchWebSuggestion(mLauncher, webSuggestion);
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/SearchWebSuggestion.java b/tests/tapl/com/android/launcher3/tapl/SearchWebSuggestion.java
new file mode 100644
index 0000000..e4dec98
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/SearchWebSuggestion.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import androidx.test.uiautomator.UiObject2;
+
+import com.android.launcher3.testing.shared.TestProtocol;
+
+import java.util.regex.Pattern;
+
+/**
+ * Operations on a search web suggestion from a qsb.
+ */
+public class SearchWebSuggestion extends Launchable {
+
+    private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onAllAppsItemLongClick");
+
+    SearchWebSuggestion(LauncherInstrumentation launcher, UiObject2 object) {
+        super(launcher, object);
+    }
+
+    @Override
+    protected void expectActivityStartEvents() {
+    }
+
+    @Override
+    protected String launchableType() {
+        return "search web suggestion";
+    }
+
+    @Override
+    protected void waitForLongPressConfirmation() {
+        mLauncher.waitForLauncherObject("popup_container");
+    }
+
+    @Override
+    protected void addExpectedEventsForLongClick() {
+        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, getLongClickEvent());
+    }
+
+    protected Pattern getLongClickEvent() {
+        return LONG_CLICK_EVENT;
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Taskbar.java b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
index 051630e..8671738 100644
--- a/tests/tapl/com/android/launcher3/tapl/Taskbar.java
+++ b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
@@ -76,13 +76,13 @@
                     mLauncher.getRealDisplaySize().x - 1, mLauncher.getRealDisplaySize().y - 1);
 
             mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, stashTarget,
-                    LauncherInstrumentation.GestureScope.INSIDE);
+                    LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
             LauncherInstrumentation.log("hideTaskbar: sent down");
 
             try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("pressed down")) {
                 mLauncher.waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
                 mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, stashTarget,
-                        LauncherInstrumentation.GestureScope.INSIDE);
+                        LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
             }
         } finally {
             mLauncher.getTestInfo(REQUEST_DISABLE_MANUAL_TASKBAR_STASHING);
@@ -147,9 +147,9 @@
                     mLauncher.getRealDisplaySize().y - 1);
 
             mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, tapTarget,
-                    LauncherInstrumentation.GestureScope.INSIDE);
+                    LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
             mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, tapTarget,
-                    LauncherInstrumentation.GestureScope.INSIDE);
+                    LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
         }
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/TaskbarAllAppsQsb.java b/tests/tapl/com/android/launcher3/tapl/TaskbarAllAppsQsb.java
new file mode 100644
index 0000000..7cecd3e
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/TaskbarAllAppsQsb.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import androidx.test.uiautomator.UiObject2;
+
+/**
+ * Operations on Taskbar AllApp screen qsb.
+ */
+public class TaskbarAllAppsQsb extends Qsb {
+
+    TaskbarAllAppsQsb(LauncherInstrumentation launcher, UiObject2 allAppsContainer) {
+        super(launcher, allAppsContainer, "search_container_all_apps");
+    }
+
+    @Override
+    public SearchResultFromTaskbarQsb showSearchResult() {
+        return (SearchResultFromTaskbarQsb) super.showSearchResult();
+    }
+
+    @Override
+    protected SearchResultFromTaskbarQsb createSearchResult() {
+        return new SearchResultFromTaskbarQsb(mLauncher);
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/TaskbarSearchWebSuggestion.java b/tests/tapl/com/android/launcher3/tapl/TaskbarSearchWebSuggestion.java
new file mode 100644
index 0000000..cd8ce42
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/TaskbarSearchWebSuggestion.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import androidx.test.uiautomator.UiObject2;
+
+import java.util.regex.Pattern;
+
+/**
+ * Operations on a search web suggestion from the Taskbar qsb.
+ */
+public class TaskbarSearchWebSuggestion extends SearchWebSuggestion implements
+        SplitscreenDragSource {
+
+    private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onTaskbarItemLongClick");
+
+    TaskbarSearchWebSuggestion(LauncherInstrumentation launcher,
+            UiObject2 object) {
+        super(launcher, object);
+    }
+
+    @Override
+    protected Pattern getLongClickEvent() {
+        return LONG_CLICK_EVENT;
+    }
+
+    /** This method requires public access, however should not be called in tests. */
+    @Override
+    public Launchable getLaunchable() {
+        return this;
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 7bb02cb..6f6f292 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -18,7 +18,6 @@
 
 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SCROLLED;
 
-import static com.android.launcher3.tapl.LauncherInstrumentation.CALLBACK_RUN_POINT.CALLBACK_HOLD_BEFORE_DROP;
 import static com.android.launcher3.testing.shared.TestProtocol.ALL_APPS_STATE_ORDINAL;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 
@@ -105,7 +104,8 @@
                     windowCornerRadius,
                     startY - swipeHeight - mLauncher.getTouchSlop(),
                     12,
-                    ALL_APPS_STATE_ORDINAL, LauncherInstrumentation.GestureScope.INSIDE);
+                    ALL_APPS_STATE_ORDINAL,
+                    LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
                     "swiped to all apps")) {
@@ -387,7 +387,7 @@
                     Until.hasObject(installerAlert), LauncherInstrumentation.WAIT_TIME_MS));
             final UiObject2 ok = device.findObject(By.text("OK"));
             assertNotNull("OK button is not shown", ok);
-            launcher.clickObject(ok, LauncherInstrumentation.GestureScope.OUTSIDE_WITHOUT_PILFER);
+            launcher.clickObject(ok);
             assertTrue("Uninstall alert is not dismissed after clicking OK", device.wait(
                     Until.gone(installerAlert), LauncherInstrumentation.WAIT_TIME_MS));
 
@@ -454,7 +454,7 @@
         launcher.runToState(
                 () -> launcher.sendPointer(
                         downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest,
-                        LauncherInstrumentation.GestureScope.INSIDE),
+                        LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER),
                 NORMAL_STATE_ORDINAL,
                 "sending UP event");
         if (expectedEvents != null) {
@@ -543,7 +543,7 @@
                 executeAndWaitForPageScroll(launcher,
                         () -> launcher.movePointer(finalDragStart, screenEdge, DEFAULT_DRAG_STEPS,
                                 true, downTime, downTime, true,
-                                LauncherInstrumentation.GestureScope.INSIDE));
+                                LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER));
                 targetDest.x += displayX * (targetDest.x > 0 ? -1 : 1);
                 dragStart = screenEdge;
             }
@@ -552,8 +552,7 @@
             // we just have to put move the icon to the destination and drop it
             launcher.movePointer(dragStart, targetDest, DEFAULT_DRAG_STEPS, isDecelerating,
                     downTime, SystemClock.uptimeMillis(), false,
-                    LauncherInstrumentation.GestureScope.INSIDE);
-            launcher.runCallbackIfActive(CALLBACK_HOLD_BEFORE_DROP);
+                    LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
             dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents);
         }
     }
@@ -586,8 +585,7 @@
             // we just have to put move the icon to the destination and drop it
             launcher.movePointer(dragStart, targetDest, DEFAULT_DRAG_STEPS, isDecelerating,
                     downTime, SystemClock.uptimeMillis(), false,
-                    LauncherInstrumentation.GestureScope.INSIDE);
-            launcher.runCallbackIfActive(CALLBACK_HOLD_BEFORE_DROP);
+                    LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
             dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents);
         }
     }
@@ -624,7 +622,7 @@
             executeAndWaitForPageScroll(launcher,
                     () -> launcher.movePointer(finalDragStart, screenEdge, DEFAULT_DRAG_STEPS,
                             true, downTime, downTime, true,
-                            LauncherInstrumentation.GestureScope.INSIDE));
+                            LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER));
             currentPage = Workspace.geCurrentPage(launcher);
             currentPosition = screenEdge;
         }
@@ -653,7 +651,7 @@
 
         launcher.movePointer(dragStart, targetDest, DEFAULT_DRAG_STEPS, true,
                 downTime, SystemClock.uptimeMillis(), false,
-                LauncherInstrumentation.GestureScope.INSIDE);
+                LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
         dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents);
     }