Merge "Fix invoking transient taskbar in desktop windowing prototypes" into tm-qpr-dev
diff --git a/Android.bp b/Android.bp
index 0bbb3d2..e730c9d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -80,10 +80,10 @@
         "androidx.preference_preference",
         "SystemUISharedLib",
         "SystemUIAnimationLib",
+        "launcher-testing-shared",
     ],
     srcs: [
         "tests/tapl/**/*.java",
-        "src/com/android/launcher3/testing/shared/**/*.java",
     ],
     resource_dirs: [ ],
     manifest: "tests/tapl/AndroidManifest.xml",
@@ -169,7 +169,10 @@
 android_library {
     name: "Launcher3CommonDepsLib",
     srcs: ["src_build_config/**/*.java"],
-    static_libs: ["Launcher3ResLib"],
+    static_libs: [
+        "Launcher3ResLib",
+        "launcher-testing-shared",
+    ],
     sdk_version: "current",
     min_sdk_version: min_launcher3_sdk_version,
     manifest: "AndroidManifest-common.xml",
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 951be4e..0c7b48fe 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -40,6 +40,7 @@
     <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.VIBRATE"/>
     <!-- for rotating surface by arbitrary degree -->
     <uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" />
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
diff --git a/build.gradle b/build.gradle
index 68ed73d..090bafe 100644
--- a/build.gradle
+++ b/build.gradle
@@ -147,6 +147,7 @@
     implementation "androidx.recyclerview:recyclerview:${ANDROID_X_VERSION}"
     implementation "androidx.preference:preference:${ANDROID_X_VERSION}"
     implementation project(':IconLoader')
+    implementation project(':UiTestsLibLauncher')
     withQuickstepImplementation project(':SharedLibWrapper')
 
     // Recents lib dependency
diff --git a/quickstep/res/drawable/hotseat_icon.xml b/quickstep/res/drawable/hotseat_icon.xml
new file mode 100644
index 0000000..b849fe9
--- /dev/null
+++ b/quickstep/res/drawable/hotseat_icon.xml
@@ -0,0 +1,21 @@
+<?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"
+    android:shape="rectangle">
+    <solid android:color="@color/mock_app_icon" />
+    <corners android:radius="@dimen/gesture_tutorial_hotseat_icon_corner_radius" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/hotseat_icon_home.xml b/quickstep/res/drawable/hotseat_icon_home.xml
new file mode 100644
index 0000000..d59dd4a
--- /dev/null
+++ b/quickstep/res/drawable/hotseat_icon_home.xml
@@ -0,0 +1,21 @@
+<?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"
+    android:shape="rectangle">
+    <solid android:color="@color/gesture_home_tutorial_background" />
+    <corners android:radius="@dimen/gesture_tutorial_hotseat_icon_corner_radius" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/hotseat_search_bar.xml b/quickstep/res/drawable/hotseat_search_bar.xml
new file mode 100644
index 0000000..ea332e9
--- /dev/null
+++ b/quickstep/res/drawable/hotseat_search_bar.xml
@@ -0,0 +1,21 @@
+<?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"
+    android:shape="rectangle">
+    <solid android:color="@color/mock_search_bar" />
+    <corners android:radius="@dimen/gesture_tutorial_hotseat_search_corner_radius" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/top_task_view.xml b/quickstep/res/drawable/top_task_view.xml
new file mode 100644
index 0000000..d2176c3
--- /dev/null
+++ b/quickstep/res/drawable/top_task_view.xml
@@ -0,0 +1,21 @@
+<?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"
+    android:shape="rectangle">
+    <solid android:color="@color/gesture_tutorial_fake_previous_task_view_color" />
+    <corners android:radius="@dimen/gesture_tutorial_small_task_view_corner_radius" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/layout-land/gesture_tutorial_mock_hotseat.xml b/quickstep/res/layout-land/gesture_tutorial_mock_hotseat.xml
index 1e2e014..c7e176a 100644
--- a/quickstep/res/layout-land/gesture_tutorial_mock_hotseat.xml
+++ b/quickstep/res/layout-land/gesture_tutorial_mock_hotseat.xml
@@ -24,54 +24,50 @@
     android:paddingStart="56dp"
     android:paddingEnd="56dp">
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_1"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintVertical_chainStyle="spread_inside"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toTopOf="@id/hotseat_icon_2"
         app:layout_constraintStart_toStartOf="parent"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_2"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toBottomOf="@id/hotseat_icon_1"
         app:layout_constraintBottom_toTopOf="@id/hotseat_icon_3"
         app:layout_constraintStart_toStartOf="parent"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_3"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toBottomOf="@id/hotseat_icon_2"
         app:layout_constraintBottom_toTopOf="@id/hotseat_icon_4"
         app:layout_constraintStart_toStartOf="parent"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_4"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toBottomOf="@id/hotseat_icon_3"
         app:layout_constraintBottom_toBottomOf="parent"
diff --git a/quickstep/res/layout-land/gesture_tutorial_tablet_mock_hotseat.xml b/quickstep/res/layout-land/gesture_tutorial_tablet_mock_hotseat.xml
index f04fbb6..28d32a4 100644
--- a/quickstep/res/layout-land/gesture_tutorial_tablet_mock_hotseat.xml
+++ b/quickstep/res/layout-land/gesture_tutorial_tablet_mock_hotseat.xml
@@ -22,84 +22,77 @@
     android:paddingStart="@dimen/gesture_tutorial_hotseat_padding_start_end"
     android:paddingEnd="@dimen/gesture_tutorial_hotseat_padding_start_end">
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_search_bar"
         android:layout_width="200dp"
         android:layout_height="@dimen/gesture_tutorial_hotseat_search_height"
+        android:background="@drawable/hotseat_search_bar"
+        android:clipToOutline="true"
 
-        app:layout_constraintHorizontal_chainStyle="spread_inside"
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_search_corner_radius"
-        app:cardBackgroundColor="@color/mock_search_bar"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_1"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_1"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintStart_toEndOf="@id/hotseat_search_bar"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_2"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_2"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintStart_toEndOf="@id/hotseat_icon_1"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_3"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_3"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintStart_toEndOf="@id/hotseat_icon_2"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_4"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_4"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintStart_toEndOf="@id/hotseat_icon_3"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_5"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_5"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
diff --git a/quickstep/res/layout/gesture_tutorial_fragment.xml b/quickstep/res/layout/gesture_tutorial_fragment.xml
index 8eeef67..3bd0df0 100644
--- a/quickstep/res/layout/gesture_tutorial_fragment.xml
+++ b/quickstep/res/layout/gesture_tutorial_fragment.xml
@@ -57,33 +57,34 @@
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintEnd_toEndOf="parent"/>
 
-        <androidx.cardview.widget.CardView
+
+        <View
             android:id="@+id/top_task_view"
             android:layout_width="match_parent"
             android:layout_height="0dp"
-            android:visibility="invisible"
             android:layout_marginBottom="@dimen/gesture_tutorial_multi_row_task_view_spacing"
+            android:background="@drawable/top_task_view"
+            android:clipToOutline="true"
+            android:visibility="invisible"
 
-            app:cardElevation="0dp"
-            app:cardCornerRadius="@dimen/gesture_tutorial_small_task_view_corner_radius"
             app:layout_constraintVertical_chainStyle="spread"
-            app:layout_constraintTop_toTopOf="@id/full_task_view"
             app:layout_constraintBottom_toTopOf="@id/bottom_task_view"
+            app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"/>
+            app:layout_constraintTop_toTopOf="@id/full_task_view" />
 
-        <androidx.cardview.widget.CardView
+        <View
             android:id="@+id/bottom_task_view"
             android:layout_width="match_parent"
             android:layout_height="0dp"
+            android:background="@drawable/top_task_view"
+            android:clipToOutline="true"
             android:visibility="invisible"
 
-            app:cardElevation="0dp"
-            app:cardCornerRadius="@dimen/gesture_tutorial_small_task_view_corner_radius"
-            app:layout_constraintTop_toBottomOf="@id/top_task_view"
             app:layout_constraintBottom_toBottomOf="@id/full_task_view"
+            app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"/>
+            app:layout_constraintTop_toBottomOf="@id/top_task_view" />
 
     </com.android.quickstep.interaction.AnimatedTaskView>
 
diff --git a/quickstep/res/layout/gesture_tutorial_mock_hotseat.xml b/quickstep/res/layout/gesture_tutorial_mock_hotseat.xml
index 8513dcf..8ee0339 100644
--- a/quickstep/res/layout/gesture_tutorial_mock_hotseat.xml
+++ b/quickstep/res/layout/gesture_tutorial_mock_hotseat.xml
@@ -8,67 +8,62 @@
     android:paddingStart="26dp"
     android:paddingEnd="26dp">
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_1"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintHorizontal_chainStyle="spread_inside"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_2"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_2"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintStart_toEndOf="@id/hotseat_icon_1"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_3"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_3"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintStart_toEndOf="@id/hotseat_icon_2"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_4"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_4"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintStart_toEndOf="@id/hotseat_icon_3"
         app:layout_constraintEnd_toEndOf="parent"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:layout_width="0dp"
         android:layout_height="@dimen/gesture_tutorial_hotseat_search_height"
         android:layout_marginTop="@dimen/gesture_tutorial_hotseat_icon_search_margin"
+        android:background="@drawable/hotseat_search_bar"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_search_corner_radius"
-        app:cardBackgroundColor="@color/mock_search_bar"
         app:layout_constraintTop_toBottomOf="@id/hotseat_icon_1"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent"/>
diff --git a/quickstep/res/layout/gesture_tutorial_tablet_mock_hotseat.xml b/quickstep/res/layout/gesture_tutorial_tablet_mock_hotseat.xml
index 363f14e..63c51e8 100644
--- a/quickstep/res/layout/gesture_tutorial_tablet_mock_hotseat.xml
+++ b/quickstep/res/layout/gesture_tutorial_tablet_mock_hotseat.xml
@@ -22,84 +22,78 @@
     android:paddingStart="@dimen/gesture_tutorial_hotseat_padding_start_end"
     android:paddingEnd="@dimen/gesture_tutorial_hotseat_padding_start_end">
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_search_bar"
         android:layout_width="0dp"
         android:layout_height="@dimen/gesture_tutorial_hotseat_search_height"
+        android:background="@drawable/hotseat_search_bar"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_search_corner_radius"
-        app:cardBackgroundColor="@color/mock_search_bar"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_1"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_marginTop="@dimen/gesture_tutorial_hotseat_icon_search_margin"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintHorizontal_chainStyle="spread_inside"
         app:layout_constraintTop_toBottomOf="@id/hotseat_search_bar"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_2"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_2"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_marginTop="@dimen/gesture_tutorial_hotseat_icon_search_margin"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toBottomOf="@id/hotseat_search_bar"
         app:layout_constraintStart_toEndOf="@id/hotseat_icon_1"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_3"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_3"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_marginTop="@dimen/gesture_tutorial_hotseat_icon_search_margin"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toBottomOf="@id/hotseat_search_bar"
         app:layout_constraintStart_toEndOf="@id/hotseat_icon_2"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_4"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_4"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_marginTop="@dimen/gesture_tutorial_hotseat_icon_search_margin"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toBottomOf="@id/hotseat_search_bar"
         app:layout_constraintStart_toEndOf="@id/hotseat_icon_3"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_5"/>
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_5"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_marginTop="@dimen/gesture_tutorial_hotseat_icon_search_margin"
+        android:background="@drawable/hotseat_icon"
+        android:clipToOutline="true"
 
-        app:cardElevation="0dp"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardBackgroundColor="@color/mock_app_icon"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintTop_toBottomOf="@id/hotseat_search_bar"
         app:layout_constraintStart_toEndOf="@id/hotseat_icon_4"
diff --git a/quickstep/res/layout/redesigned_gesture_tutorial_fragment.xml b/quickstep/res/layout/redesigned_gesture_tutorial_fragment.xml
index 13b7238..13482ac 100644
--- a/quickstep/res/layout/redesigned_gesture_tutorial_fragment.xml
+++ b/quickstep/res/layout/redesigned_gesture_tutorial_fragment.xml
@@ -57,29 +57,29 @@
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toTopOf="parent" />
 
-        <androidx.cardview.widget.CardView
+
+        <View
             android:id="@+id/top_task_view"
             android:layout_width="match_parent"
             android:layout_height="0dp"
             android:layout_marginBottom="@dimen/gesture_tutorial_multi_row_task_view_spacing"
+            android:background="@drawable/top_task_view"
+            android:clipToOutline="true"
             android:visibility="invisible"
 
-            app:cardCornerRadius="@dimen/gesture_tutorial_small_task_view_corner_radius"
-            app:cardElevation="0dp"
             app:layout_constraintBottom_toTopOf="@id/bottom_task_view"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="@id/full_task_view"
-            app:layout_constraintVertical_chainStyle="spread" />
+            app:layout_constraintTop_toTopOf="@id/full_task_view" />
 
-        <androidx.cardview.widget.CardView
+        <View
             android:id="@+id/bottom_task_view"
             android:layout_width="match_parent"
             android:layout_height="0dp"
+            android:background="@drawable/top_task_view"
+            android:clipToOutline="true"
             android:visibility="invisible"
 
-            app:cardCornerRadius="@dimen/gesture_tutorial_small_task_view_corner_radius"
-            app:cardElevation="0dp"
             app:layout_constraintBottom_toBottomOf="@id/full_task_view"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
@@ -99,13 +99,13 @@
         android:background="@drawable/gesture_tutorial_ripple" />
 
     <include
-        layout="@layout/gesture_tutorial_tablet_mock_taskbar"
         android:id="@+id/gesture_tutorial_fake_taskbar_view"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/gesture_tutorial_mock_taskbar_height"
+        layout="@layout/gesture_tutorial_tablet_mock_taskbar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
         android:layout_alignParentBottom="true"
-        android:layout_alignParentStart="true"
-        android:layout_alignParentEnd="true" />
+        android:layout_centerHorizontal="true"
+        android:layout_marginBottom="@dimen/gesture_tutorial_taskbar_margin_bottom" />
 
     <ImageView
         android:id="@+id/gesture_tutorial_edge_gesture_video"
diff --git a/quickstep/res/layout/redesigned_gesture_tutorial_mock_hotseat.xml b/quickstep/res/layout/redesigned_gesture_tutorial_mock_hotseat.xml
index 6d9ffae..b1c8b31 100644
--- a/quickstep/res/layout/redesigned_gesture_tutorial_mock_hotseat.xml
+++ b/quickstep/res/layout/redesigned_gesture_tutorial_mock_hotseat.xml
@@ -21,70 +21,64 @@
     android:layout_height="wrap_content"
     android:paddingBottom="70dp"
     android:paddingStart="26dp"
-    android:paddingEnd="26dp"
-    android:background="@color/gesture_home_tutorial_swipe_up_rect">
+    android:paddingEnd="26dp">
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_1"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon_home"
+        android:clipToOutline="true"
 
-        app:cardBackgroundColor="@color/gesture_home_tutorial_background"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardElevation="0dp"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_2"
         app:layout_constraintHorizontal_chainStyle="spread_inside"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_2"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon_home"
+        android:clipToOutline="true"
 
-        app:cardBackgroundColor="@color/gesture_home_tutorial_background"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardElevation="0dp"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_3"
         app:layout_constraintStart_toEndOf="@id/hotseat_icon_1"
         app:layout_constraintTop_toTopOf="parent" />
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_3"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon_home"
+        android:clipToOutline="true"
 
-        app:cardBackgroundColor="@color/gesture_home_tutorial_background"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardElevation="0dp"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintEnd_toStartOf="@id/hotseat_icon_4"
         app:layout_constraintStart_toEndOf="@id/hotseat_icon_2"
         app:layout_constraintTop_toTopOf="parent" />
 
-    <androidx.cardview.widget.CardView
+    <View
         android:id="@+id/hotseat_icon_4"
         android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
         android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
+        android:background="@drawable/hotseat_icon_home"
+        android:clipToOutline="true"
 
-        app:cardBackgroundColor="@color/gesture_home_tutorial_background"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
-        app:cardElevation="0dp"
         app:layout_constraintDimensionRatio="1:1"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toEndOf="@id/hotseat_icon_3"
         app:layout_constraintTop_toTopOf="parent" />
 
-    <androidx.cardview.widget.CardView
+    <View
         android:layout_width="0dp"
         android:layout_height="@dimen/gesture_tutorial_hotseat_search_height"
         android:layout_marginTop="@dimen/gesture_tutorial_hotseat_icon_search_margin"
+        android:background="@drawable/hotseat_icon_home"
+        android:clipToOutline="true"
 
-        app:cardBackgroundColor="@color/gesture_home_tutorial_background"
-        app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_search_corner_radius"
-        app:cardElevation="0dp"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@id/hotseat_icon_1" />
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 0d8391b..3846a9c 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -297,15 +297,11 @@
     <!-- An additional touch slop to prevent x-axis movement during the swipe up to show taskbar -->
     <dimen name="transient_taskbar_clamped_offset_bound">16dp</dimen>
     <!-- Taskbar swipe up thresholds -->
-    <dimen name="taskbar_nav_threshold">40dp</dimen>
-    <dimen name="taskbar_app_window_threshold">150dp</dimen>
-    <dimen name="taskbar_home_overview_threshold">225dp</dimen>
+    <dimen name="taskbar_nav_threshold">30dp</dimen>
+    <dimen name="taskbar_app_window_threshold">100dp</dimen>
+    <dimen name="taskbar_home_overview_threshold">180dp</dimen>
     <dimen name="taskbar_catch_up_threshold">300dp</dimen>
 
-    <dimen name="taskbar_nav_threshold_v2">30dp</dimen>
-    <dimen name="taskbar_app_window_threshold_v2">100dp</dimen>
-    <dimen name="taskbar_home_overview_threshold_v2">180dp</dimen>
-
     <!--  Taskbar 3 button spacing  -->
     <dimen name="taskbar_button_space_inbetween">24dp</dimen>
     <dimen name="taskbar_button_space_inbetween_phone">40dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 9cec881..728c91f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -20,6 +20,7 @@
 import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
 
+import static com.android.launcher3.LauncherAnimUtils.ROTATION_DRAWABLE_PERCENT;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
 import static com.android.launcher3.taskbar.LauncherTaskbarUIController.SYSUI_SURFACE_PROGRESS_INDEX;
@@ -60,6 +61,7 @@
 import android.graphics.Region.Op;
 import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.PaintDrawable;
+import android.graphics.drawable.RotateDrawable;
 import android.inputmethodservice.InputMethodService;
 import android.os.Handler;
 import android.util.Property;
@@ -172,10 +174,10 @@
     // Initialized in init.
     private TaskbarControllers mControllers;
     private boolean mIsImeRenderingNavButtons;
-    private View mA11yButton;
+    private ImageView mA11yButton;
     private int mSysuiStateFlags;
-    private View mBackButton;
-    private View mHomeButton;
+    private ImageView mBackButton;
+    private ImageView mHomeButton;
     private MultiValueAlpha mBackButtonAlpha;
     private MultiValueAlpha mHomeButtonAlpha;
     private FloatingRotationButton mFloatingRotationButton;
@@ -186,7 +188,7 @@
     private final ViewTreeObserver.OnComputeInternalInsetsListener mSeparateWindowInsetsComputer =
             this::onComputeInsetsForSeparateWindow;
     private final RecentsHitboxExtender mHitboxExtender = new RecentsHitboxExtender();
-    private View mRecentsButton;
+    private ImageView mRecentsButton;
 
     public NavbarButtonsViewController(TaskbarActivityContext context, FrameLayout navButtonsView) {
         mContext = context;
@@ -353,13 +355,13 @@
                     return (flags & FLAG_DISABLE_BACK) == 0
                             && ((flags & FLAG_KEYGUARD_VISIBLE) == 0 || showingOnKeyguard);
                 }));
-        boolean isRtl = Utilities.isRtl(mContext.getResources());
         mPropertyHolders.add(new StatePropertyHolder(mBackButton,
-                flags -> (flags & FLAG_IME_VISIBLE) != 0 && !mContext.isNavBarKidsModeActive(),
-                View.ROTATION, isRtl ? 90 : -90, 0));
+                flags -> (flags & FLAG_IME_VISIBLE) != 0,
+                ROTATION_DRAWABLE_PERCENT, 1f, 0f));
         // Translate back button to be at end/start of other buttons for keyguard
         int navButtonSize = mContext.getResources().getDimensionPixelSize(
                 R.dimen.taskbar_nav_buttons_size);
+        boolean isRtl = Utilities.isRtl(mContext.getResources());
         mPropertyHolders.add(new StatePropertyHolder(
                 mBackButton, flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0
                         || (flags & FLAG_KEYGUARD_VISIBLE) != 0,
@@ -734,13 +736,18 @@
             int paddingBottom = paddingTop;
 
             // Update icons
-            ((ImageView) mBackButton).setImageDrawable(
-                    mBackButton.getContext().getDrawable(R.drawable.ic_sysbar_back_kids));
-            ((ImageView) mBackButton).setScaleType(ImageView.ScaleType.FIT_CENTER);
+            final RotateDrawable rotateDrawable = new RotateDrawable();
+            rotateDrawable.setDrawable(mContext.getDrawable(R.drawable.ic_sysbar_back_kids));
+            rotateDrawable.setFromDegrees(0f);
+            rotateDrawable.setToDegrees(-90f);
+            mBackButton.setImageDrawable(rotateDrawable);
+            mBackButton.setScaleType(ImageView.ScaleType.FIT_CENTER);
             mBackButton.setPadding(paddingleft, paddingTop, paddingRight, paddingBottom);
-            ((ImageView) mHomeButton).setImageDrawable(
+            mBackButton.setScaleX(Utilities.isRtl(mContext.getResources()) ? -1f : 1f);
+
+            mHomeButton.setImageDrawable(
                     mHomeButton.getContext().getDrawable(R.drawable.ic_sysbar_home_kids));
-            ((ImageView) mHomeButton).setScaleType(ImageView.ScaleType.FIT_CENTER);
+            mHomeButton.setScaleType(ImageView.ScaleType.FIT_CENTER);
             mHomeButton.setPadding(paddingleft, paddingTop, paddingRight, paddingBottom);
 
             // Home button layout
@@ -781,6 +788,12 @@
 
             mHomeButton.setOnLongClickListener(null);
         } else if (mContext.isThreeButtonNav()) {
+            final RotateDrawable rotateDrawable = new RotateDrawable();
+            rotateDrawable.setDrawable(mContext.getDrawable(R.drawable.ic_sysbar_back));
+            rotateDrawable.setFromDegrees(0f);
+            rotateDrawable.setToDegrees(Utilities.isRtl(mContext.getResources()) ? 90f : -90f);
+            mBackButton.setImageDrawable(rotateDrawable);
+
             // Setup normal 3 button
             // Add spacing after the end of the last nav button
             FrameLayout.LayoutParams navButtonParams =
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
index b5afda3..df95dc1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -39,6 +39,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.taskbar.LauncherTaskbarUIController;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -62,6 +63,7 @@
     private static final long TRANSLATION_ANIM_MIN_DURATION_MS = 80;
     private static final float TRANSLATION_ANIM_VELOCITY_DP_PER_MS = 0.8f;
 
+    private final VibratorWrapper mVibratorWrapper;
     private final RecentsView mRecentsView;
     private final MotionPauseDetector mMotionPauseDetector;
     private final float mMotionPauseMinDisplacement;
@@ -82,6 +84,7 @@
         mRecentsView = l.getOverviewPanel();
         mMotionPauseDetector = new MotionPauseDetector(l);
         mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
+        mVibratorWrapper = VibratorWrapper.INSTANCE.get(l.getApplicationContext());
     }
 
     @Override
@@ -188,6 +191,11 @@
             // need to manually set the duration to a reasonable value.
             animator.setDuration(HINT_STATE.getTransitionDuration(mLauncher, true /* isToState */));
         }
+        if (FeatureFlags.ENABLE_HAPTICS_ALL_APPS.get() &&
+                ((mFromState == NORMAL && mToState == ALL_APPS)
+                        || (mFromState == ALL_APPS && mToState == NORMAL)) && isFling) {
+            mVibratorWrapper.vibrateForDragBump();
+        }
     }
 
     private void onMotionPauseDetected() {
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 2485ff8..4f10dde 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -27,7 +27,6 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_REVISED_THRESHOLDS;
 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;
@@ -367,17 +366,12 @@
         TaskbarUIController controller = mActivityInterface.getTaskbarController();
         mTaskbarAlreadyOpen = controller != null && !controller.isTaskbarStashed();
         mIsTaskbarAllAppsOpen = controller != null && controller.isTaskbarAllAppsOpen();
-        mTaskbarAppWindowThreshold = res
-                .getDimensionPixelSize(ENABLE_TASKBAR_REVISED_THRESHOLDS.get()
-                        ? R.dimen.taskbar_app_window_threshold_v2
-                        : R.dimen.taskbar_app_window_threshold);
+        mTaskbarAppWindowThreshold =
+                res.getDimensionPixelSize(R.dimen.taskbar_app_window_threshold);
         boolean swipeWillNotShowTaskbar = mTaskbarAlreadyOpen;
         mTaskbarHomeOverviewThreshold = swipeWillNotShowTaskbar
                 ? 0
-                : res.getDimensionPixelSize(
-                        ENABLE_TASKBAR_REVISED_THRESHOLDS.get()
-                                ? R.dimen.taskbar_home_overview_threshold_v2
-                                : R.dimen.taskbar_home_overview_threshold);
+                : res.getDimensionPixelSize(R.dimen.taskbar_home_overview_threshold);
         mTaskbarCatchUpThreshold = res.getDimensionPixelSize(R.dimen.taskbar_catch_up_threshold);
     }
 
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 89e8f51..7f2886c 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -346,7 +346,6 @@
             case RECENTS:
                 return OVERVIEW;
             case NEW_TASK:
-                return QUICK_SWITCH_FROM_HOME;
             case LAST_TASK:
                 return BACKGROUND_APP;
             case HOME:
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 2200b35..139e923 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -628,14 +628,20 @@
         }
     }
 
-    public void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, Bundle options1,
-            PendingIntent pendingIntent2, Bundle options2,
-            @SplitConfigurationOptions.StagePosition int sidePosition, float splitRatio,
-            RemoteAnimationAdapter adapter, InstanceId instanceId) {
+    /**
+     * Starts a pair of intents or shortcuts in split-screen using legacy transition. Passing a
+     * non-null shortcut info means to start the app as a shortcut.
+     */
+    public void startIntentsWithLegacyTransition(PendingIntent pendingIntent1,
+            @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1,
+            PendingIntent pendingIntent2, @Nullable ShortcutInfo shortcutInfo2,
+            @Nullable Bundle options2, @SplitConfigurationOptions.StagePosition int sidePosition,
+            float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) {
         if (mSystemUiProxy != null) {
             try {
-                mSplitScreen.startIntentsWithLegacyTransition(pendingIntent1, options1,
-                        pendingIntent2, options2, sidePosition, splitRatio, adapter, instanceId);
+                mSplitScreen.startIntentsWithLegacyTransition(pendingIntent1, shortcutInfo1,
+                        options1, pendingIntent2, shortcutInfo2, options2, sidePosition, splitRatio,
+                        adapter, instanceId);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startIntentsWithLegacyTransition");
             }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
index 3afd0a3..1630d0f 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
@@ -18,7 +18,6 @@
 import static android.view.MotionEvent.INVALID_POINTER_ID;
 
 import static com.android.launcher3.Utilities.squaredHypot;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_REVISED_THRESHOLDS;
 import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TOUCHING;
 
 import android.content.Context;
@@ -74,9 +73,7 @@
 
         Resources res = context.getResources();
         mUnstashArea = res.getDimensionPixelSize(R.dimen.taskbar_unstash_input_area);
-        int taskbarNavThreshold = res.getDimensionPixelSize(ENABLE_TASKBAR_REVISED_THRESHOLDS.get()
-                ? R.dimen.taskbar_nav_threshold_v2
-                : R.dimen.taskbar_nav_threshold);
+        int taskbarNavThreshold = res.getDimensionPixelSize(R.dimen.taskbar_nav_threshold);
         int screenHeight = taskbarActivityContext.getDeviceProfile().heightPx;
         mTaskbarNavThresholdY = screenHeight - taskbarNavThreshold;
         mIsTaskbarAllAppsOpen =
diff --git a/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java b/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java
index 53ad138..3ccd683 100644
--- a/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java
+++ b/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java
@@ -30,7 +30,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.cardview.widget.CardView;
 import androidx.constraintlayout.widget.ConstraintLayout;
 
 import com.android.launcher3.R;
@@ -46,8 +45,8 @@
 public class AnimatedTaskView extends ConstraintLayout {
 
     private View mFullTaskView;
-    private CardView mTopTaskView;
-    private CardView mBottomTaskView;
+    private View mTopTaskView;
+    private View mBottomTaskView;
 
     private ViewOutlineProvider mTaskViewOutlineProvider = null;
     private final Rect mTaskViewAnimatedRect = new Rect();
@@ -185,8 +184,6 @@
 
     void setFakeTaskViewFillColor(@ColorInt int colorResId) {
         mFullTaskView.setBackgroundColor(colorResId);
-        mTopTaskView.setCardBackgroundColor(colorResId);
-        mBottomTaskView.setCardBackgroundColor(colorResId);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
index c89d4b6..bce639b 100644
--- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
@@ -61,10 +61,10 @@
 
     @Override
     protected int getMockAppTaskLayoutResId() {
-        return mTutorialFragment.isLargeScreen()
-                ? R.layout.gesture_tutorial_tablet_mock_webpage
-                : ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
-                    ? R.layout.swipe_up_gesture_tutorial_shape
+        return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
+                ? R.layout.swipe_up_gesture_tutorial_shape
+                : mTutorialFragment.isLargeScreen()
+                    ? R.layout.gesture_tutorial_tablet_mock_webpage
                     : R.layout.gesture_tutorial_mock_webpage;
     }
 
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index ccdb266..6fcb840 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -154,7 +154,6 @@
 
             mFeedbackTitleView.setText(getIntroductionTitle());
             mFeedbackSubtitleView.setText(getIntroductionSubtitle());
-            mSkipButton.setVisibility(GONE);
         }
 
         mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent(
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index e5d54d7..1b2bfc9 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -261,8 +261,9 @@
                         getOppositeStagePosition(stagePosition), splitRatio, adapter,
                         shellInstanceId);
             } else {
-                mSystemUiProxy.startIntentsWithLegacyTransition(getPendingIntent(intent1),
-                        options1.toBundle(), getPendingIntent(intent2), null /* options2 */,
+                mSystemUiProxy.startIntentsWithLegacyTransition(
+                        getPendingIntent(intent1), getShortcutInfo(intent1), options1.toBundle(),
+                        getPendingIntent(intent2), getShortcutInfo(intent2), null /* options2 */,
                         stagePosition, splitRatio, adapter, shellInstanceId);
             }
         }
@@ -271,15 +272,13 @@
     private void launchIntentOrShortcut(Intent intent, ActivityOptions options1, int taskId,
             @StagePosition int stagePosition, float splitRatio, RemoteTransition remoteTransition,
             @Nullable InstanceId shellInstanceId) {
-        PendingIntent pendingIntent = getPendingIntent(intent);
-        final ShortcutInfo shortcutInfo = getShortcutInfo(intent,
-                pendingIntent.getCreatorUserHandle());
+        final ShortcutInfo shortcutInfo = getShortcutInfo(intent);
         if (shortcutInfo != null) {
             mSystemUiProxy.startShortcutAndTask(shortcutInfo,
                     options1.toBundle(), taskId, null /* options2 */, stagePosition,
                     splitRatio, remoteTransition, shellInstanceId);
         } else {
-            mSystemUiProxy.startIntentAndTask(pendingIntent, options1.toBundle(), taskId,
+            mSystemUiProxy.startIntentAndTask(getPendingIntent(intent), options1.toBundle(), taskId,
                     null /* options2 */, stagePosition, splitRatio, remoteTransition,
                     shellInstanceId);
         }
@@ -288,15 +287,13 @@
     private void launchIntentOrShortcutLegacy(Intent intent, ActivityOptions options1, int taskId,
             @StagePosition int stagePosition, float splitRatio, RemoteAnimationAdapter adapter,
             @Nullable InstanceId shellInstanceId) {
-        PendingIntent pendingIntent = getPendingIntent(intent);
-        final ShortcutInfo shortcutInfo = getShortcutInfo(intent,
-                pendingIntent.getCreatorUserHandle());
+        final ShortcutInfo shortcutInfo = getShortcutInfo(intent);
         if (shortcutInfo != null) {
             mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(shortcutInfo,
                     options1.toBundle(), taskId, null /* options2 */, stagePosition,
                     splitRatio, adapter, shellInstanceId);
         } else {
-            mSystemUiProxy.startIntentAndTaskWithLegacyTransition(pendingIntent,
+            mSystemUiProxy.startIntentAndTaskWithLegacyTransition(getPendingIntent(intent),
                     options1.toBundle(), taskId, null /* options2 */, stagePosition, splitRatio,
                     adapter, shellInstanceId);
         }
@@ -322,7 +319,7 @@
     }
 
     @Nullable
-    private ShortcutInfo getShortcutInfo(Intent intent, UserHandle userHandle) {
+    private ShortcutInfo getShortcutInfo(Intent intent) {
         if (intent == null || intent.getPackage() == null) {
             return null;
         }
@@ -334,7 +331,7 @@
 
         try {
             final Context context = mContext.createPackageContextAsUser(
-                    intent.getPackage(), 0 /* flags */, userHandle);
+                    intent.getPackage(), 0 /* flags */, mUser);
             return new ShortcutInfo.Builder(context, shortcutId).build();
         } catch (PackageManager.NameNotFoundException e) {
             Log.w(TAG, "Failed to create a ShortcutInfo for " + intent.getPackage());
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 3f7d677..5cf79ea 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -355,6 +355,12 @@
         mSnapshotView2.setSplashAlpha(mTaskThumbnailSplashAlpha);
     }
 
+    @Override
+    protected void refreshTaskThumbnailSplash() {
+        super.refreshTaskThumbnailSplash();
+        mSnapshotView2.refreshSplashView();
+    }
+
     /**
      *     Sets visibility for thumbnails and associated elements (DWB banners).
      *     IconView is unaffected.
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 517e86a..ff26129 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -94,6 +94,7 @@
 
     @Override
     public void onTaskIconChanged(int taskId) {
+        super.onTaskIconChanged(taskId);
         // If Launcher needs to return to split select state, do it now, after the icon has updated.
         if (mActivity.hasPendingSplitSelectInfo()) {
             PendingSplitSelectInfo recoveryData = mActivity.getPendingSplitSelectInfo();
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 2505d91..5b40849 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -956,6 +956,14 @@
         }
     }
 
+    @Override
+    public void onTaskIconChanged(int taskId) {
+        TaskView taskView = getTaskViewByTaskId(taskId);
+        if (taskView != null) {
+            taskView.refreshTaskThumbnailSplash();
+        }
+    }
+
     /**
      * Update the thumbnail of the task.
      * @param refreshNow Refresh immediately if it's true.
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index c71a74e..432eadc 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -342,10 +342,11 @@
 
         // Draw splash above thumbnail to hide inconsistencies in rotation and aspect ratios.
         if (shouldShowSplashView()) {
+            // Always draw background for hiding inconsistencies, even if splash view is not yet
+            // loaded (which can happen as task icons are loaded asynchronously in the background)
+            canvas.drawRoundRect(x, y, width + 1, height + 1, cornerRadius,
+                    cornerRadius, mSplashBackgroundPaint);
             if (mSplashView != null) {
-                canvas.drawRoundRect(x, y, width + 1, height + 1, cornerRadius,
-                        cornerRadius, mSplashBackgroundPaint);
-
                 mSplashView.layout((int) x, (int) (y + 1), (int) width, (int) height - 1);
                 mSplashView.draw(canvas);
             }
@@ -375,6 +376,13 @@
                 || isThumbnailRotationDifferentFromTask();
     }
 
+    protected void refreshSplashView() {
+        if (mTask != null) {
+            updateSplashView(mTask.icon);
+            invalidate();
+        }
+    }
+
     private void updateSplashView(Drawable icon) {
         if (icon == null || icon.getConstantState() == null) {
             mSplashViewDrawable = null;
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 19ffa27..829e72c 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -1215,6 +1215,10 @@
         mSnapshotView.setSplashAlpha(mTaskThumbnailSplashAlpha);
     }
 
+    protected void refreshTaskThumbnailSplash() {
+        mSnapshotView.refreshSplashView();
+    }
+
     private void setSplitSelectTranslationX(float x) {
         mSplitSelectTranslationX = x;
         applyTranslationX();
diff --git a/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
index 9afd893..bc1b87d 100644
--- a/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
@@ -19,7 +19,7 @@
 import android.graphics.RectF
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.launcher3.DeviceProfileBaseTest
+import com.android.launcher3.FakeInvariantDeviceProfileTest
 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
 import com.android.quickstep.views.TaskView.FullscreenDrawParams
@@ -36,7 +36,7 @@
 /** Test for FullscreenDrawParams class. */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class FullscreenDrawParamsTest : DeviceProfileBaseTest() {
+class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() {
 
     private val TASK_SCALE = 0.7f
     private var mThumbnailData: ThumbnailData = mock(ThumbnailData::class.java)
diff --git a/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt b/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt
index adbca32..a347156 100644
--- a/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt
@@ -18,7 +18,7 @@
 import android.graphics.Rect
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.launcher3.DeviceProfileBaseTest
+import com.android.launcher3.FakeInvariantDeviceProfileTest
 import com.android.launcher3.util.WindowBounds
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -26,7 +26,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class HotseatWidthCalculationTest : DeviceProfileBaseTest() {
+class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() {
 
     /**
      * This is a case when after setting the hotseat, the space needs to be recalculated but it
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 9f34775..bc5fa19 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -191,7 +191,7 @@
         mLauncher.goHome().switchToOverview().getCurrentTask()
                 .tapMenu()
                 .tapSplitMenuItem()
-                .getTestActivityTask(2)
+                .getCurrentTask()
                 .open();
     }
 
diff --git a/res/layout/workspace_screen_foldable.xml b/res/layout/workspace_screen_foldable.xml
new file mode 100644
index 0000000..1e01250
--- /dev/null
+++ b/res/layout/workspace_screen_foldable.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+
+<com.android.launcher3.MultipageCellLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:hapticFeedbackEnabled="false"
+    launcher:containerType="workspace" />
\ No newline at end of file
diff --git a/res/values-sw600dp-land/dimens.xml b/res/values-sw600dp-land/dimens.xml
index 63970cd..92f6881 100644
--- a/res/values-sw600dp-land/dimens.xml
+++ b/res/values-sw600dp-land/dimens.xml
@@ -30,6 +30,4 @@
     <dimen name="dynamic_grid_edge_margin">11.33dp</dimen>
     <dimen name="cell_layout_padding">11.33dp</dimen>
 
-<!-- AllApps -->
-    <dimen name="all_apps_bottom_sheet_horizontal_padding">52dp</dimen>
 </resources>
diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml
index d69e777..0af0603 100644
--- a/res/values-sw600dp/dimens.xml
+++ b/res/values-sw600dp/dimens.xml
@@ -26,7 +26,6 @@
 
 <!-- AllApps -->
     <dimen name="all_apps_search_bar_content_overlap">0dp</dimen>
-    <dimen name="all_apps_bottom_sheet_horizontal_padding">48dp</dimen>
 
 <!-- Fast scroll -->
     <dimen name="fastscroll_popup_width">75dp</dimen>
diff --git a/res/values-sw720dp-land/dimens.xml b/res/values-sw720dp-land/dimens.xml
index 235631d..6362960 100644
--- a/res/values-sw720dp-land/dimens.xml
+++ b/res/values-sw720dp-land/dimens.xml
@@ -22,9 +22,6 @@
     <dimen name="dynamic_grid_edge_margin">21.93dp</dimen>
     <dimen name="cell_layout_padding">29.33dp</dimen>
 
-<!-- AllApps -->
-    <dimen name="all_apps_bottom_sheet_horizontal_padding">32dp</dimen>
-
 <!-- Dragging-->
     <dimen name="drop_target_top_margin">0dp</dimen>
     <dimen name="drop_target_bottom_margin">32dp</dimen>
diff --git a/res/values-sw720dp/dimens.xml b/res/values-sw720dp/dimens.xml
index 09b2d6f..d7a5881 100644
--- a/res/values-sw720dp/dimens.xml
+++ b/res/values-sw720dp/dimens.xml
@@ -18,9 +18,6 @@
 <!-- PagedView  -->
     <dimen name="min_page_snap_velocity">3400dp</dimen>
 
-<!-- AllApps -->
-    <dimen name="all_apps_bottom_sheet_horizontal_padding">28dp</dimen>
-
 <!-- Dynamic grid -->
     <dimen name="dynamic_grid_edge_margin">27.59dp</dimen>
     <dimen name="cell_layout_padding">36dp</dimen>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index e6f285c..c3bd90e 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -151,6 +151,9 @@
         <!-- Support attributes in FolderStyle -->
         <attr name="folderStyle" format="reference" />
 
+        <!-- Support attributes in AllAppsStyle. Defaults to AllAppsStyleDefault -->
+        <attr name="allAppsStyle" format="reference" />
+
         <!-- numAllAppsColumns defaults to numColumns, if not specified -->
         <attr name="numAllAppsColumns" format="integer" />
         <!-- Number of columns to use when extending the all-apps size,
@@ -221,6 +224,21 @@
         <attr name="alignOnIcon" format="boolean" />
     </declare-styleable>
 
+    <!--  Responsive grids attributes  -->
+    <declare-styleable name="WorkspaceSpec">
+        <attr name="specType" format="integer">
+            <enum name="height" value="0" />
+            <enum name="width" value="1" />
+        </attr>
+        <attr name="maxAvailableSize" format="dimension" />
+    </declare-styleable>
+
+    <declare-styleable name="SpecSize">
+        <attr name="fixedSize" format="dimension" />
+        <attr name="ofAvailableSpace" format="float" />
+        <attr name="ofRemainderSpace" format="float" />
+    </declare-styleable>
+
     <declare-styleable name="ProfileDisplayOption">
         <attr name="name" />
         <attr name="minWidthDps" format="float" />
@@ -418,6 +436,10 @@
         <attr name="iconDrawablePadding" format="dimension" />
     </declare-styleable>
 
+    <declare-styleable name="AllAppsStyle">
+        <attr name="horizontalPadding" format="dimension" />
+    </declare-styleable>
+
     <declare-styleable name="ShadowDrawable">
         <attr name="android:src" />
         <attr name="android:shadowColor" />
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 321aef5..a9638f9 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -131,7 +131,6 @@
     <dimen name="all_apps_content_fade_in_offset">150dp</dimen>
     <dimen name="all_apps_tip_bottom_margin">8dp</dimen>
     <dimen name="all_apps_height_extra">6dp</dimen>
-    <dimen name="all_apps_bottom_sheet_horizontal_padding">0dp</dimen>
     <dimen name="all_apps_paged_view_top_padding">40dp</dimen>
 
     <dimen name="all_apps_icon_drawable_padding">8dp</dimen>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 6b57532..a0b5570 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -304,4 +304,8 @@
         <item name="iconDrawablePadding">7dp</item>
     </style>
 
+    <style name="AllAppsStyleDefault">
+        <item name="horizontalPadding">16dp</item>
+    </style>
+
 </resources>
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index d388ebc..b96e4df 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -99,9 +99,9 @@
     private Point mBorderSpace;
 
     @ViewDebug.ExportedProperty(category = "launcher")
-    private int mCountX;
+    protected int mCountX;
     @ViewDebug.ExportedProperty(category = "launcher")
-    private int mCountY;
+    protected int mCountY;
 
     private boolean mDropPending = false;
 
@@ -111,8 +111,8 @@
     @Thunk final int[] mTempLocation = new int[2];
     final PointF mTmpPointF = new PointF();
 
-    private GridOccupancy mOccupied;
-    private GridOccupancy mTmpOccupied;
+    protected GridOccupancy mOccupied;
+    protected GridOccupancy mTmpOccupied;
 
     private OnTouchListener mInterceptTouchListener;
 
@@ -121,7 +121,7 @@
 
     private static final int[] BACKGROUND_STATE_ACTIVE = new int[] { android.R.attr.state_active };
     private static final int[] BACKGROUND_STATE_DEFAULT = EMPTY_STATE_SET;
-    private final Drawable mBackground;
+    protected final Drawable mBackground;
 
     // These values allow a fixed measurement to be set on the CellLayout.
     private int mFixedWidth = -1;
@@ -154,7 +154,7 @@
     private int mGridVisualizationRoundingRadius;
     private float mGridAlpha = 0f;
     private int mGridColor = 0;
-    private float mSpringLoadedProgress = 0f;
+    protected float mSpringLoadedProgress = 0f;
     private float mScrollProgress = 0f;
 
     // When a drag operation is in progress, holds the nearest cell to the touch point
@@ -164,7 +164,7 @@
     private boolean mDragging = false;
 
     private final TimeInterpolator mEaseOutInterpolator;
-    private final ShortcutAndWidgetContainer mShortcutsAndWidgets;
+    protected final ShortcutAndWidgetContainer mShortcutsAndWidgets;
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({WORKSPACE, HOTSEAT, FOLDER})
@@ -565,7 +565,7 @@
         return mSpringLoadedProgress;
     }
 
-    private void updateBgAlpha() {
+    protected void updateBgAlpha() {
         mBackground.setAlpha((int) (mSpringLoadedProgress * 255));
     }
 
@@ -1662,7 +1662,7 @@
      * @param spanY vertical cell span
      * @return the configuration that represents the found reorder
      */
-    private ItemConfiguration closestEmptySpaceReorder(int pixelX, int pixelY, int minSpanX,
+    ItemConfiguration closestEmptySpaceReorder(int pixelX, int pixelY, int minSpanX,
             int minSpanY, int spanX, int spanY) {
         ItemConfiguration solution = new ItemConfiguration();
         int[] result = new int[2];
@@ -2402,8 +2402,15 @@
         return true;
     }
 
-    private ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY,
-            int spanX, int spanY, int[] direction, View dragView, boolean decX,
+    protected ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX,
+            int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX,
+            ItemConfiguration solution) {
+        return findReorderSolutionRecursive(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY,
+                direction, dragView, decX, solution);
+    }
+
+    protected ItemConfiguration findReorderSolutionRecursive(int pixelX, int pixelY, int minSpanX,
+            int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX,
             ItemConfiguration solution) {
         // Copy the current state into the solution. This solution will be manipulated as necessary.
         copyCurrentStateToSolution(solution, false);
@@ -2426,11 +2433,11 @@
             // We try shrinking the widget down to size in an alternating pattern, shrink 1 in
             // x, then 1 in y etc.
             if (spanX > minSpanX && (minSpanY == spanY || decX)) {
-                return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX - 1, spanY,
-                        direction, dragView, false, solution);
+                return findReorderSolutionRecursive(pixelX, pixelY, minSpanX, minSpanY, spanX - 1,
+                        spanY, direction, dragView, false, solution);
             } else if (spanY > minSpanY) {
-                return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY - 1,
-                        direction, dragView, true, solution);
+                return findReorderSolutionRecursive(pixelX, pixelY, minSpanX, minSpanY, spanX,
+                        spanY - 1, direction, dragView, true, solution);
             }
             solution.isSolution = false;
         } else {
@@ -2443,7 +2450,7 @@
         return solution;
     }
 
-    private void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) {
+    protected void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) {
         int childCount = mShortcutsAndWidgets.getChildCount();
         for (int i = 0; i < childCount; i++) {
             View child = mShortcutsAndWidgets.getChildAt(i);
@@ -2624,7 +2631,7 @@
         return mItemPlacementDirty;
     }
 
-    private static class ItemConfiguration extends CellAndSpan {
+    static class ItemConfiguration extends CellAndSpan {
         final ArrayMap<View, CellAndSpan> map = new ArrayMap<>();
         private final ArrayMap<View, CellAndSpan> savedMap = new ArrayMap<>();
         final ArrayList<View> sortedViews = new ArrayList<>();
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 0070f95..086cf05 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -404,6 +404,7 @@
         allAppsBorderSpacePx = new Point(
                 pxFromDp(inv.allAppsBorderSpaces[mTypeIndex].x, mMetrics),
                 pxFromDp(inv.allAppsBorderSpaces[mTypeIndex].y, mMetrics));
+        setupAllAppsStyle(context);
 
         workspacePageIndicatorHeight = res.getDimensionPixelSize(
                 R.dimen.workspace_page_indicator_height);
@@ -810,9 +811,6 @@
         int cellLayoutHorizontalPadding =
                 (cellLayoutPaddingPx.left + cellLayoutPaddingPx.right) / 2;
         if (isTablet) {
-            allAppsLeftRightPadding =
-                    res.getDimensionPixelSize(R.dimen.all_apps_bottom_sheet_horizontal_padding);
-
             int usedWidth = (allAppsCellWidthPx * numShownAllAppsColumns)
                     + (allAppsBorderSpacePx.x * (numShownAllAppsColumns - 1))
                     + allAppsLeftRightPadding * 2;
@@ -823,6 +821,20 @@
         }
     }
 
+    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.
      */
@@ -1806,7 +1818,7 @@
          * Set the viewScaleProvider for the builder
          *
          * @param viewScaleProvider The viewScaleProvider to be set for the
-         *                                DeviceProfile
+         *                          DeviceProfile
          * @return This builder
          */
         @NonNull
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index d97eac9..604c1b8 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -175,6 +175,7 @@
      */
     public int numAllAppsColumns;
     public int numDatabaseAllAppsColumns;
+    public @StyleRes int allAppsStyle;
 
     /**
      * Do not query directly. see {@link DeviceProfile#isScalableGrid}.
@@ -384,6 +385,8 @@
         hotseatBarBottomSpace = displayOption.hotseatBarBottomSpace;
         hotseatQsbSpace = displayOption.hotseatQsbSpace;
 
+        allAppsStyle = closestProfile.allAppsStyle;
+
         numAllAppsColumns = closestProfile.numAllAppsColumns;
         numDatabaseAllAppsColumns = deviceType == TYPE_MULTI_DISPLAY
                 ? closestProfile.numDatabaseAllAppsColumns : closestProfile.numAllAppsColumns;
@@ -789,6 +792,7 @@
         private final @StyleRes int folderStyle;
         private final @StyleRes int cellStyle;
 
+        private final @StyleRes int allAppsStyle;
         private final int numAllAppsColumns;
         private final int numDatabaseAllAppsColumns;
         private final int numHotseatIcons;
@@ -824,6 +828,8 @@
             demoModeLayoutId = a.getResourceId(
                     R.styleable.GridDisplayOption_demoModeLayoutId, defaultLayoutId);
 
+            allAppsStyle = a.getResourceId(R.styleable.GridDisplayOption_allAppsStyle,
+                    R.style.AllAppsStyleDefault);
             numAllAppsColumns = a.getInt(
                     R.styleable.GridDisplayOption_numAllAppsColumns, numColumns);
             numDatabaseAllAppsColumns = a.getInt(
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 2f1f59c..41632f7 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -44,6 +44,7 @@
 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.SHOW_DOT_PAGINATION;
 import static com.android.launcher3.logging.StatsLogManager.EventEnum;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
@@ -138,6 +139,7 @@
 import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.celllayout.CellPosMapper.CellPos;
+import com.android.launcher3.celllayout.CellPosMapper.TwoPanelCellPosMapper;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dot.DotInfo;
@@ -730,7 +732,11 @@
         }
 
         onDeviceProfileInitiated();
-        mCellPosMapper = CellPosMapper.DEFAULT;
+        if (FOLDABLE_SINGLE_PAGE.get() && mDeviceProfile.isTwoPanels) {
+            mCellPosMapper = new TwoPanelCellPosMapper(mDeviceProfile.inv.numColumns);
+        } else {
+            mCellPosMapper = CellPosMapper.DEFAULT;
+        }
         mModelWriter = mModel.getWriter(getDeviceProfile().isVerticalBarLayout(), true,
                 mCellPosMapper, this);
         return true;
@@ -1342,6 +1348,7 @@
 
         // Setup Apps
         mAppsView = findViewById(R.id.apps_view);
+        mAppsView.setAllAppsTransitionController(mAllAppsController);
 
         // Setup Scrim
         mScrimView = findViewById(R.id.scrim_view);
diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java
index 4e80d41..c20f323 100644
--- a/src/com/android/launcher3/LauncherAnimUtils.java
+++ b/src/com/android/launcher3/LauncherAnimUtils.java
@@ -26,6 +26,7 @@
 import android.util.IntProperty;
 import android.view.View;
 import android.view.ViewGroup.LayoutParams;
+import android.widget.ImageView;
 import android.widget.TextView;
 
 import com.android.launcher3.util.MultiScalePropertyFactory;
@@ -200,6 +201,23 @@
                 }
             };
 
+    public static final FloatProperty<ImageView> ROTATION_DRAWABLE_PERCENT =
+            new FloatProperty<ImageView>("drawableRotationPercent") {
+                // RotateDrawable linearly interpolates the rotation degrees between fromDegrees
+                // and toDegrees using the drawable level as a percent of its MAX_LEVEL.
+                private static final int MAX_LEVEL = 10000;
+
+                @Override
+                public void setValue(ImageView view, float percent) {
+                    view.setImageLevel((int) (percent * MAX_LEVEL));
+                }
+
+                @Override
+                public Float get(ImageView view) {
+                    return view.getDrawable().getLevel() / (float) MAX_LEVEL;
+                }
+            };
+
     /**
      * Utility method to create an {@link AnimatorListener} which executes a callback on animation
      * cancel.
diff --git a/src/com/android/launcher3/MultipageCellLayout.java b/src/com/android/launcher3/MultipageCellLayout.java
new file mode 100644
index 0000000..d671c7d
--- /dev/null
+++ b/src/com/android/launcher3/MultipageCellLayout.java
@@ -0,0 +1,168 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.launcher3.celllayout.CellLayoutLayoutParams;
+import com.android.launcher3.util.CellAndSpan;
+import com.android.launcher3.util.GridOccupancy;
+
+import java.util.function.Supplier;
+
+/**
+ * CellLayout that simulates a split in the middle for use in foldable devices.
+ */
+public class MultipageCellLayout extends CellLayout {
+
+    private final Drawable mLeftBackground;
+    private final Drawable mRightBackground;
+
+    private View mSeam;
+
+    public MultipageCellLayout(Context context) {
+        this(context, null);
+    }
+
+    public MultipageCellLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public MultipageCellLayout(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        mLeftBackground = getContext().getDrawable(R.drawable.bg_celllayout);
+        mLeftBackground.setCallback(this);
+        mLeftBackground.setAlpha(0);
+
+        mRightBackground = getContext().getDrawable(R.drawable.bg_celllayout);
+        mRightBackground.setCallback(this);
+        mRightBackground.setAlpha(0);
+
+        DeviceProfile deviceProfile = mActivity.getDeviceProfile();
+
+        mCountX = deviceProfile.inv.numColumns * 2;
+        mCountY = deviceProfile.inv.numRows;
+        mSeam = new View(getContext());
+        setGridSize(mCountX, mCountY);
+    }
+
+    @Override
+    ItemConfiguration closestEmptySpaceReorder(int pixelX, int pixelY, int minSpanX, int minSpanY,
+            int spanX, int spanY) {
+        return simulateSeam(
+                () -> super.closestEmptySpaceReorder(pixelX, pixelY, minSpanX, minSpanY, spanX,
+                        spanY));
+    }
+
+    @Override
+    protected ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX,
+            int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX,
+            ItemConfiguration solution) {
+        return simulateSeam(
+                () -> super.findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY,
+                        direction, dragView, decX, solution));
+    }
+
+    @Override
+    public ItemConfiguration dropInPlaceSolution(int pixelX, int pixelY, int spanX, int spanY,
+            View dragView) {
+        return simulateSeam(
+                () -> super.dropInPlaceSolution(pixelX, pixelY, spanX, spanY, dragView));
+    }
+
+    protected ItemConfiguration simulateSeam(Supplier<ItemConfiguration> f) {
+        CellLayoutLayoutParams lp = new CellLayoutLayoutParams(mCountX / 2, 0, 1, mCountY);
+        lp.canReorder = false;
+        mCountX++;
+        mShortcutsAndWidgets.addViewInLayout(mSeam, lp);
+        GridOccupancy auxGrid = mOccupied;
+        mOccupied = createGridOccupancy();
+        mTmpOccupied = new GridOccupancy(mCountX, mCountY);
+
+        ItemConfiguration res = removeSeamFromSolution(f.get());
+
+        mCountX--;
+        mShortcutsAndWidgets.removeViewInLayout(mSeam);
+        mOccupied = auxGrid;
+        mTmpOccupied = new GridOccupancy(mCountX, mCountY);
+        return res;
+    }
+
+    private ItemConfiguration removeSeamFromSolution(ItemConfiguration solution) {
+        solution.map.forEach((view, cell) -> cell.cellX = cell.cellX > mCountX / 2
+                ? cell.cellX - 1 : cell.cellX);
+        solution.cellX = solution.cellX > mCountX / 2 ? solution.cellX - 1 : solution.cellX;
+        return solution;
+    }
+
+    GridOccupancy createGridOccupancy() {
+        GridOccupancy grid = new GridOccupancy(mCountX, mCountY);
+        for (int i = 0; i < mShortcutsAndWidgets.getChildCount(); i++) {
+            View view = mShortcutsAndWidgets.getChildAt(i);
+            CellLayoutLayoutParams lp = (CellLayoutLayoutParams) view.getLayoutParams();
+            int seamOffset = lp.getCellX() >= mCountX / 2 && lp.canReorder ? 1 : 0;
+            grid.markCells(lp.getCellX() + seamOffset, lp.getCellY(), lp.cellHSpan, lp.cellVSpan,
+                    true);
+        }
+        return grid;
+    }
+
+    @Override
+    protected void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) {
+        int childCount = mShortcutsAndWidgets.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = mShortcutsAndWidgets.getChildAt(i);
+            CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
+            int seamOffset = lp.getCellX() >= mCountX / 2 && lp.canReorder ? 1 : 0;
+            CellAndSpan c = new CellAndSpan(lp.getCellX() + seamOffset, lp.getCellY(), lp.cellHSpan,
+                    lp.cellVSpan);
+            solution.add(child, c);
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (mLeftBackground.getAlpha() > 0) {
+            mLeftBackground.setState(mBackground.getState());
+            mLeftBackground.draw(canvas);
+        }
+        if (mRightBackground.getAlpha() > 0) {
+            mRightBackground.setState(mBackground.getState());
+            mRightBackground.draw(canvas);
+        }
+
+        super.onDraw(canvas);
+    }
+
+    @Override
+    protected void updateBgAlpha() {
+        mLeftBackground.setAlpha((int) (mSpringLoadedProgress * 255));
+        mRightBackground.setAlpha((int) (mSpringLoadedProgress * 255));
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        Rect rect = mBackground.getBounds();
+        mLeftBackground.setBounds(rect.left, rect.top, rect.right / 2 - 20, rect.bottom);
+        mRightBackground.setBounds(rect.right / 2 + 20, rect.top, rect.right, rect.bottom);
+    }
+}
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index 5f39f7e..55b745b 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -107,6 +107,18 @@
         }
     }
 
+    /**
+     * Adds view to Layout a new position and it does not trigger a layout request.
+     * For more information check documentation for
+     * {@code ViewGroup#addViewInLayout(View, int, LayoutParams, boolean)}
+     *
+     * @param child view to be added
+     * @return true if the child was added, false otherwise
+     */
+    public boolean addViewInLayout(View child, LayoutParams layoutParams) {
+        return super.addViewInLayout(child, -1, layoutParams, true);
+    }
+
     public void setupLp(View child) {
         CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
         if (child instanceof NavigableAppWidgetHostView) {
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index df829f1..cfb8ca4 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -135,6 +135,7 @@
  * The workspace is a wide area with a wallpaper and a finite number of pages.
  * Each page contains a number of icons, folders or widgets the user can
  * interact with. A workspace is meant to be used with a fixed width only.
+ *
  * @param <T> Class that extends View and PageIndicator
  */
 public class Workspace<T extends View & PageIndicator> extends PagedView<T>
@@ -142,12 +143,16 @@
         DragController.DragListener, Insettable, StateHandler<LauncherState>,
         WorkspaceLayoutManager, LauncherBindableItemsContainer, LauncherOverlayCallbacks {
 
-    /** The value that {@link #mTransitionProgress} must be greater than for
-     * {@link #transitionStateShouldAllowDrop()} to return true. */
+    /**
+     * The value that {@link #mTransitionProgress} must be greater than for
+     * {@link #transitionStateShouldAllowDrop()} to return true.
+     */
     private static final float ALLOW_DROP_TRANSITION_PROGRESS = 0.25f;
 
-    /** The value that {@link #mTransitionProgress} must be greater than for
-     * {@link #isFinishedSwitchingState()} ()} to return true. */
+    /**
+     * The value that {@link #mTransitionProgress} must be greater than for
+     * {@link #isFinishedSwitchingState()} ()} to return true.
+     */
     private static final float FINISHED_SWITCHING_STATE_TRANSITION_PROGRESS = 0.5f;
 
     private static final float SIGNIFICANT_MOVE_SCREEN_WIDTH_PERCENTAGE = 0.15f;
@@ -161,16 +166,20 @@
     private final int mAllAppsIconSize;
 
     private LayoutTransition mLayoutTransition;
-    @Thunk final WallpaperManager mWallpaperManager;
+    @Thunk
+    final WallpaperManager mWallpaperManager;
 
     protected ShortcutAndWidgetContainer mDragSourceInternal;
 
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-    @Thunk public final IntSparseArrayMap<CellLayout> mWorkspaceScreens = new IntSparseArrayMap<>();
+    @Thunk
+    public final IntSparseArrayMap<CellLayout> mWorkspaceScreens = new IntSparseArrayMap<>();
 
-    @Thunk final IntArray mScreenOrder = new IntArray();
+    @Thunk
+    final IntArray mScreenOrder = new IntArray();
 
-    @Thunk boolean mDeferRemoveExtraEmptyScreen = false;
+    @Thunk
+    boolean mDeferRemoveExtraEmptyScreen = false;
 
     /**
      * CellInfo for the cell that is currently being dragged
@@ -180,14 +189,16 @@
     /**
      * Target drop area calculated during last acceptDrop call.
      */
-    @Thunk int[] mTargetCell = new int[2];
+    @Thunk
+    int[] mTargetCell = new int[2];
     private int mDragOverX = -1;
     private int mDragOverY = -1;
 
     /**
      * The CellLayout that is currently being dragged over
      */
-    @Thunk CellLayout mDragTargetLayout = null;
+    @Thunk
+    CellLayout mDragTargetLayout = null;
     /**
      * The CellLayout that we will show as highlighted
      */
@@ -198,13 +209,16 @@
      */
     private CellLayout mDropToLayout = null;
 
-    @Thunk final Launcher mLauncher;
-    @Thunk DragController mDragController;
+    @Thunk
+    final Launcher mLauncher;
+    @Thunk
+    DragController mDragController;
 
     protected final int[] mTempXY = new int[2];
     private final float[] mTempFXY = new float[2];
     private final Rect mTempRect = new Rect();
-    @Thunk float[] mDragViewVisualCenter = new float[2];
+    @Thunk
+    float[] mDragViewVisualCenter = new float[2];
 
     private SpringLoadedDragController mSpringLoadedDragController;
 
@@ -249,8 +263,10 @@
     private static final int DRAG_MODE_ADD_TO_FOLDER = 2;
     private static final int DRAG_MODE_REORDER = 3;
     protected int mDragMode = DRAG_MODE_NONE;
-    @Thunk int mLastReorderX = -1;
-    @Thunk int mLastReorderY = -1;
+    @Thunk
+    int mLastReorderX = -1;
+    @Thunk
+    int mLastReorderY = -1;
 
     private SparseArray<Parcelable> mSavedStates;
     private final IntArray mRestoredPages = new IntArray();
@@ -275,7 +291,7 @@
      * Used to inflate the Workspace from XML.
      *
      * @param context The application's context.
-     * @param attrs The attributes set containing the Workspace's customization values.
+     * @param attrs   The attributes set containing the Workspace's customization values.
      */
     public Workspace(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
@@ -284,8 +300,8 @@
     /**
      * Used to inflate the Workspace from XML.
      *
-     * @param context The application's context.
-     * @param attrs The attributes set containing the Workspace's customization values.
+     * @param context  The application's context.
+     * @param attrs    The attributes set containing the Workspace's customization values.
      * @param defStyle Unused.
      */
     public Workspace(Context context, AttributeSet attrs, int defStyle) {
@@ -421,7 +437,9 @@
         return mWallpaperOffset.wallpaperOffsetForScroll(pageScroll);
     }
 
-    /** Returns the number of pages used for the wallpaper parallax. */
+    /**
+     * Returns the number of pages used for the wallpaper parallax.
+     */
     public int getNumPagesForWallpaperParallax() {
         return mWallpaperOffset.getNumPagesForWallpaperParallax();
     }
@@ -483,7 +501,7 @@
     }
 
     private boolean isTwoPanelEnabled() {
-        return mLauncher.mDeviceProfile.isTwoPanels;
+        return !FOLDABLE_SINGLE_PAGE.get() && mLauncher.mDeviceProfile.isTwoPanels;
     }
 
     @Override
@@ -553,6 +571,7 @@
     void enableLayoutTransitions() {
         setLayoutTransition(mLayoutTransition);
     }
+
     void disableLayoutTransitions() {
         setLayoutTransition(null);
     }
@@ -644,8 +663,15 @@
 
         // Inflate the cell layout, but do not add it automatically so that we can get the newly
         // created CellLayout.
-        CellLayout newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate(
+        DeviceProfile dp = mLauncher.getDeviceProfile();
+        CellLayout newScreen;
+        if (FOLDABLE_SINGLE_PAGE.get() && dp.isTwoPanels) {
+            newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate(
+                    R.layout.workspace_screen_foldable, this, false /* attachToRoot */);
+        } else {
+            newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate(
                     R.layout.workspace_screen, this, false /* attachToRoot */);
+        }
 
         mWorkspaceScreens.put(screenId, newScreen);
         mScreenOrder.add(insertIndex, screenId);
@@ -783,17 +809,17 @@
      * Empty page(s) from the end of mWorkspaceScreens will always be removed. The pages with
      * ID = Workspace.EXTRA_EMPTY_SCREEN_IDS will be removed if there are other non-empty pages.
      * If there are no more non-empty pages left, extra empty page(s) will either stay or get added.
-     *
+     * <p>
      * If stripEmptyScreens is true, all empty pages (not just the ones on the end) will be removed
      * from the Workspace, and if there are no more pages left then extra empty page(s) will be
      * added.
-     *
+     * <p>
      * The number of extra empty pages is equal to what getPanelCount() returns.
-     *
+     * <p>
      * After the method returns the possible pages are:
      * stripEmptyScreens = true : [non-empty pages, extra empty page(s) alone]
      * stripEmptyScreens = false : [non-empty pages, empty pages (not in the end),
-     *                             extra empty page(s) alone]
+     * extra empty page(s) alone]
      */
     public void removeExtraEmptyScreenDelayed(
             int delay, boolean stripEmptyScreens, Runnable onComplete) {
@@ -844,11 +870,11 @@
     }
 
     /**
-     *  Commits the extra empty pages then returns the screen ids of those new screens.
-     *  Usually there's only one extra empty screen, but when two panel home is enabled we commit
-     *  two extra screens.
-     *
-     *  Returns an empty IntSet in case we cannot commit any new screens.
+     * Commits the extra empty pages then returns the screen ids of those new screens.
+     * Usually there's only one extra empty screen, but when two panel home is enabled we commit
+     * two extra screens.
+     * <p>
+     * Returns an empty IntSet in case we cannot commit any new screens.
      */
     public IntSet commitExtraEmptyScreens() {
         if (mLauncher.isWorkspaceLoading()) {
@@ -869,7 +895,7 @@
         mScreenOrder.removeValue(emptyScreenId);
 
         int newScreenId = LauncherSettings.Settings.call(getContext().getContentResolver(),
-                LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
+                        LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
                 .getInt(LauncherSettings.Settings.EXTRA_VALUE);
         // Launcher database isn't aware of empty pages that are already bound, so we need to
         // skip those IDs manually.
@@ -1058,8 +1084,10 @@
         return mIsSwitchingState;
     }
 
-    /** This differs from isSwitchingState in that we take into account how far the transition
-     *  has completed. */
+    /**
+     * This differs from isSwitchingState in that we take into account how far the transition
+     * has completed.
+     */
     public boolean isFinishedSwitchingState() {
         return !mIsSwitchingState
                 || (mTransitionProgress > FINISHED_SWITCHING_STATE_TRANSITION_PROGRESS);
@@ -1387,7 +1415,9 @@
         return mLauncher.isInState(SPRING_LOADED) || !workspaceInModalState();
     }
 
-    /** Returns whether a drag should be allowed to be started from the current workspace state. */
+    /**
+     * Returns whether a drag should be allowed to be started from the current workspace state.
+     */
     public boolean workspaceIconsCanBeDragged() {
         return mLauncher.getStateManager().getState().hasFlag(FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED);
     }
@@ -1419,7 +1449,7 @@
                 // In overview mode, make sure that the two side pages are visible.
                 leftScreen = Utilities.boundToRange(getCurrentPage() - 1, 0, rightScreen);
                 rightScreen = Utilities.boundToRange(getCurrentPage() + 1,
-                    leftScreen, getPageCount() - 1);
+                        leftScreen, getPageCount() - 1);
             }
 
             if (leftScreen == rightScreen) {
@@ -1621,7 +1651,7 @@
         if (draggableView != null) {
             draggableView.getSourceVisualDragBounds(dragRect);
             dragLayerY += dragRect.top;
-            dragVisualizeOffset = new Point(- halfPadding, halfPadding);
+            dragVisualizeOffset = new Point(-halfPadding, halfPadding);
         }
 
 
@@ -1752,7 +1782,7 @@
     }
 
     boolean willCreateUserFolder(ItemInfo info, CellLayout target, int[] targetCell,
-            float distance, boolean considerTimeout) {
+                                 float distance, boolean considerTimeout) {
         if (distance > target.getFolderCreationRadius(targetCell)) return false;
         View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
         return willCreateUserFolder(info, dropOverView, considerTimeout);
@@ -1788,12 +1818,13 @@
     }
 
     boolean willAddToExistingUserFolder(ItemInfo dragInfo, CellLayout target, int[] targetCell,
-            float distance) {
+                                        float distance) {
         if (distance > target.getFolderCreationRadius(targetCell)) return false;
         View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
         return willAddToExistingUserFolder(dragInfo, dropOverView);
 
     }
+
     boolean willAddToExistingUserFolder(ItemInfo dragInfo, View dropOverView) {
         if (dropOverView != null) {
             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) dropOverView.getLayoutParams();
@@ -1894,7 +1925,7 @@
     }
 
     @Override
-    public void prepareAccessibilityDrop() { }
+    public void prepareAccessibilityDrop() {}
 
     @Override
     public void onDrop(final DragObject d, DragOptions options) {
@@ -1912,8 +1943,8 @@
         boolean resizeOnDrop = false;
         Runnable onCompleteRunnable = null;
         if (d.dragSource != this || mDragInfo == null) {
-            final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0],
-                    (int) mDragViewVisualCenter[1] };
+            final int[] touchXY = new int[]{(int) mDragViewVisualCenter[0],
+                    (int) mDragViewVisualCenter[1]};
             onDropExternal(touchXY, dropTargetLayout, d);
         } else {
             final View cell = mDragInfo.cell;
@@ -2311,8 +2342,9 @@
 
     /**
      * Updates the point in {@param xy} to point to the co-ordinate space of {@param layout}
+     *
      * @param layout either hotseat of a page in workspace
-     * @param xy the point location in workspace co-ordinate space
+     * @param xy     the point location in workspace co-ordinate space
      */
     private void mapPointFromDropLayout(CellLayout layout, float[] xy) {
         if (mLauncher.isHotseatLayout(layout)) {
@@ -2379,7 +2411,7 @@
             manageFolderFeedback(targetCellDistance, d);
 
             boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
-                    mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
+                            mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
                     item.spanY, child, mTargetCell);
 
             manageReorderOnDragOver(d, targetCellDistance, nearestDropOccupied, minSpanX, minSpanY,
@@ -2400,11 +2432,15 @@
         ItemInfo item = d.dragInfo;
         final View child = (mDragInfo == null) ? null : mDragInfo.cell;
         if (!nearestDropOccupied) {
+            int[] span = new int[2];
             mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
                     (int) mDragViewVisualCenter[1], minSpanX, minSpanY, item.spanX, item.spanY,
-                    child, mTargetCell, new int[2], CellLayout.MODE_SHOW_REORDER_HINT);
-            mDragTargetLayout.visualizeDropLocation(mTargetCell[0], mTargetCell[1],
-                    item.spanX, item.spanY, d);
+                    child, mTargetCell, span, CellLayout.MODE_SHOW_REORDER_HINT);
+            mDragTargetLayout.visualizeDropLocation(mTargetCell[0], mTargetCell[1], span[0],
+                    span[1], d);
+            nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
+                            mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
+                    item.spanY, child, mTargetCell);
         } else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
                 && (mLastReorderX != reorderX || mLastReorderY != reorderY)
                 && targetCellDistance < mDragTargetLayout.getReorderRadius(mTargetCell, item.spanX,
@@ -2423,6 +2459,7 @@
             mReorderAlarm.setAlarm(REORDER_TIMEOUT);
         }
     }
+
     /**
      * Updates {@link #mDragTargetLayout} and {@link #mDragOverlappingLayout}
      * based on the DragObject's position.
@@ -2604,7 +2641,7 @@
         final View child;
 
         public ReorderAlarmListener(float[] dragViewCenter, int minSpanX, int minSpanY, int spanX,
-                int spanY, DragObject dragObject, View child) {
+                                    int spanY, DragObject dragObject, View child) {
             this.dragViewCenter = dragViewCenter;
             this.minSpanX = minSpanX;
             this.minSpanY = minSpanY;
@@ -2621,8 +2658,8 @@
                     mTargetCell);
 
             mTargetCell = mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
-                (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
-                child, mTargetCell, resultSpan, CellLayout.MODE_DRAG_OVER);
+                    (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
+                    child, mTargetCell, resultSpan, CellLayout.MODE_DRAG_OVER);
 
             if (mTargetCell[0] < 0 || mTargetCell[1] < 0) {
                 mDragTargetLayout.revertTempState();
@@ -2646,7 +2683,7 @@
      * Drop an item that didn't originate on one of the workspace screens.
      * It may have come from Launcher (e.g. from all apps or customize), or it may have
      * come from another app altogether.
-     *
+     * <p>
      * NOTE: This can also be called when we are outside of a drag event, when we want
      * to add an item to one of the workspace screens.
      */
@@ -2688,7 +2725,7 @@
                         mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);
                 if (willCreateUserFolder(d.dragInfo, cellLayout, mTargetCell, distance, true)
                         || willAddToExistingUserFolder(
-                                d.dragInfo, cellLayout, mTargetCell, distance)) {
+                        d.dragInfo, cellLayout, mTargetCell, distance)) {
                     findNearestVacantCell = false;
                 }
             }
@@ -2923,7 +2960,7 @@
             Drawable crossFadeDrawable = createWidgetDrawable(info, finalView);
             dragView.crossFadeContent(crossFadeDrawable, (int) (duration * 0.8f));
         } else if (isWidget && external) {
-            scaleXY[0] = scaleXY[1] = Math.min(scaleXY[0],  scaleXY[1]);
+            scaleXY[0] = scaleXY[1] = Math.min(scaleXY[0], scaleXY[1]);
         }
 
         DragLayer dragLayer = mLauncher.getDragLayer();
@@ -2962,6 +2999,7 @@
             setScaleY(mStateTransitionAnimation.getFinalScale());
         }
     }
+
     public void resetTransitionTransform() {
         if (isSwitchingState()) {
             setScaleX(mCurrentScale);
@@ -2973,7 +3011,6 @@
      * Return the current CellInfo describing our current drag; this method exists
      * so that Launcher can sync this object with the correct info when the activity is created/
      * destroyed
-     *
      */
     public CellLayout.CellInfo getDragInfo() {
         return mDragInfo;
@@ -2981,11 +3018,12 @@
 
     /**
      * Calculate the nearest cell where the given object would be dropped.
-     *
+     * <p>
      * pixelX and pixelY should be in the coordinate system of layout
      */
-    @Thunk int[] findNearestArea(int pixelX, int pixelY,
-            int spanX, int spanY, CellLayout layout, int[] recycle) {
+    @Thunk
+    int[] findNearestArea(int pixelX, int pixelY,
+                          int spanX, int spanY, CellLayout layout, int[] recycle) {
         return layout.findNearestAreaIgnoreOccupied(
                 pixelX, pixelY, spanX, spanY, recycle);
     }
@@ -3003,7 +3041,7 @@
      * Called at the end of a drag which originated on the workspace.
      */
     public void onDropCompleted(final View target, final DragObject d,
-            final boolean success) {
+                                final boolean success) {
         if (success) {
             if (target != this && mDragInfo != null) {
                 removeWorkspaceItem(mDragInfo.cell);
@@ -3050,6 +3088,7 @@
 
     /**
      * Removed widget from workspace by appWidgetId
+     *
      * @param appWidgetId
      */
     public void removeWidget(int appWidgetId) {
@@ -3256,6 +3295,7 @@
 
     /**
      * Perform {param operator} over all the items in a given {param layout}.
+     *
      * @return The first item that satisfies the operator or null.
      */
     public View mapOverCellLayout(CellLayout layout, ItemOperator operator) {
@@ -3310,10 +3350,10 @@
     /**
      * Remove workspace icons & widget information related to items in matcher.
      *
-     * @param matcher  the matcher generated by the caller.
+     * @param matcher the matcher generated by the caller.
      */
     public void persistRemoveItemsByMatcher(Predicate<ItemInfo> matcher,
-            @Nullable final String reason) {
+                                            @Nullable final String reason) {
         mLauncher.getModelWriter().deleteItemsFromDatabase(matcher, reason);
         removeItemsByMatcher(matcher);
     }
@@ -3358,7 +3398,9 @@
         return mOverlayShown;
     }
 
-    /** Calls {@link #snapToPage(int)} on the {@link #DEFAULT_PAGE}, then requests focus on it. */
+    /**
+     * Calls {@link #snapToPage(int)} on the {@link #DEFAULT_PAGE}, then requests focus on it.
+     */
     public void moveToDefaultScreen() {
         int page = DEFAULT_PAGE;
         if (!workspaceInModalState() && getNextPage() != page) {
@@ -3458,7 +3500,7 @@
         private boolean mRefreshPending;
 
         DeferredWidgetRefresh(ArrayList<LauncherAppWidgetInfo> infos,
-                LauncherWidgetHolder holder) {
+                              LauncherWidgetHolder holder) {
             mInfos = infos;
             mWidgetHolder = holder;
             mHandler = mLauncher.mHandler;
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index ea0d1b9..c3d24f9 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -155,6 +155,7 @@
     private int mHeaderColor;
     private int mBottomSheetBackgroundColor;
     private int mTabsProtectionAlpha;
+    @Nullable private AllAppsTransitionController mAllAppsTransitionController;
 
     public ActivityAllAppsContainerView(Context context) {
         this(context, null);
@@ -292,6 +293,11 @@
         animateToSearchState(goingToSearch, DEFAULT_SEARCH_TRANSITION_DURATION_MS);
     }
 
+    public void setAllAppsTransitionController(
+            AllAppsTransitionController allAppsTransitionController) {
+        mAllAppsTransitionController = allAppsTransitionController;
+    }
+
     private void animateToSearchState(boolean goingToSearch, long durationMs) {
         if (!mSearchTransitionController.isRunning() && goingToSearch == isSearching()) {
             return;
@@ -299,6 +305,9 @@
         if (goingToSearch) {
             // Fade out the button to pause work apps.
             mWorkManager.onActivePageChanged(SEARCH);
+        } else if (mAllAppsTransitionController != null) {
+            // If exiting search, revert predictive back scale on all apps
+            mAllAppsTransitionController.animateAllAppsToNoScale();
         }
         mSearchTransitionController.animateToSearchState(goingToSearch, durationMs,
                 /* onEndRunnable = */ () -> {
@@ -477,7 +486,7 @@
         }
         setupHeader();
 
-        if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) {
+        if (isSearchBarOnBottom()) {
             // Keep the scroller above the search bar.
             RelativeLayout.LayoutParams scrollerLayoutParams =
                     (LayoutParams) findViewById(R.id.fast_scroller).getLayoutParams();
@@ -536,7 +545,7 @@
         removeCustomRules(getSearchRecyclerView());
         if (!isSearchSupported()) {
             layoutWithoutSearchContainer(rvContainer, showTabs);
-        } else if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) {
+        } else if (isSearchBarOnBottom()) {
             alignParentTop(rvContainer, showTabs);
             alignParentTop(getSearchRecyclerView(), /* tabs= */ false);
             layoutAboveSearchContainer(rvContainer);
@@ -571,7 +580,7 @@
         removeCustomRules(mHeader);
         if (!isSearchSupported()) {
             layoutWithoutSearchContainer(mHeader, false /* includeTabsMargin */);
-        } else if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) {
+        } else if (isSearchBarOnBottom()) {
             alignParentTop(mHeader, false /* includeTabsMargin */);
         } else {
             layoutBelowSearchContainer(mHeader, false /* includeTabsMargin */);
@@ -610,6 +619,19 @@
                 (int) (mSearchContainer.getAlpha() * 255));
     }
 
+    /**
+     * 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;
+    }
+
     private void layoutBelowSearchContainer(View v, boolean includeTabsMargin) {
         if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
             return;
@@ -908,7 +930,7 @@
             setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0);
         } else {
             int topPadding = grid.allAppsTopPadding;
-            if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get() && !grid.isTablet) {
+            if (isSearchBarOnBottom() && !grid.isTablet) {
                 topPadding += getResources().getDimensionPixelSize(
                         R.dimen.all_apps_additional_top_padding_floating_search);
             }
@@ -1109,7 +1131,7 @@
         FloatingHeaderView headerView = getFloatingHeaderView();
         if (isTablet) {
             // Start adding header protection if search bar or tabs will attach to the top.
-            if (!FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get() || mUsingTabs) {
+            if (!isSearchBarOnBottom() || mUsingTabs) {
                 View panel = (View) mBottomSheetBackground;
                 float translationY = ((View) panel.getParent()).getTranslationY();
                 mTmpRectF.set(panel.getLeft(), panel.getTop() + translationY, panel.getRight(),
@@ -1151,7 +1173,7 @@
     /** Returns the position of the bottom edge of the header */
     public int getHeaderBottom() {
         int bottom = (int) getTranslationY() + mHeader.getClipTop();
-        if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) {
+        if (isSearchBarOnBottom()) {
             if (mActivityContext.getDeviceProfile().isTablet) {
                 return bottom + mBottomSheetBackground.getTop();
             }
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 394a7d7..b618724 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -32,6 +32,7 @@
 import android.animation.Animator;
 import android.animation.Animator.AnimatorListener;
 import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.util.FloatProperty;
 import android.view.HapticFeedbackConstants;
 import android.view.View;
@@ -47,17 +48,21 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.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;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.touch.AllAppsSwipeController;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.views.ScrimView;
 
 /**
@@ -78,6 +83,8 @@
     private static final int REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS = 200;
 
     private static final float NAV_BAR_COLOR_FORCE_UPDATE_THRESHOLD = 0.1f;
+    private static final float SWIPE_DRAG_COMMIT_THRESHOLD =
+            1 - AllAppsSwipeController.ALL_APPS_STATE_TRANSITION_MANUAL;
 
     public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
             new FloatProperty<AllAppsTransitionController>("allAppsProgress") {
@@ -181,6 +188,7 @@
     private boolean mIsTablet;
 
     private boolean mHasScaleEffect;
+    private final VibratorWrapper mVibratorWrapper;
 
     public AllAppsTransitionController(Launcher l) {
         mLauncher = l;
@@ -193,6 +201,7 @@
 
         setShiftRange(dp.allAppsShiftRange);
         mLauncher.addOnDeviceProfileChangeListener(this);
+        mVibratorWrapper = VibratorWrapper.INSTANCE.get(mLauncher.getApplicationContext());
     }
 
     public float getShiftRange() {
@@ -301,9 +310,21 @@
         }
     }
 
+    /** Animate all apps view to 1f scale. */
+    public void animateAllAppsToNoScale() {
+        mAllAppScale.animateToValue(1f)
+                .setDuration(REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS)
+                .start();
+    }
+
     /**
      * Creates an animation which updates the vertical transition progress and updates all the
      * dependent UI using various animation events
+     *
+     * This method also dictates where along the progress the haptics should be played. As the user
+     * scrolls up from workspace or down from AllApps, a drag haptic is being played until the
+     * commit point where it plays a commit haptic. Where we play the haptics differs when going
+     * from workspace -> allApps and vice versa.
      */
     @Override
     public void setStateWithAnimation(LauncherState toState,
@@ -332,6 +353,20 @@
             });
         }
 
+        if(FeatureFlags.ENABLE_HAPTICS_ALL_APPS.get() && config.userControlled
+                && Utilities.ATLEAST_S) {
+            if (toState == ALL_APPS) {
+                builder.addOnFrameListener(
+                        new VibrationAnimatorUpdateListener(this, mVibratorWrapper,
+                                SWIPE_DRAG_COMMIT_THRESHOLD, 1));
+            } else {
+                builder.addOnFrameListener(
+                        new VibrationAnimatorUpdateListener(this, mVibratorWrapper,
+                                0, SWIPE_DRAG_COMMIT_THRESHOLD));
+            }
+            builder.addEndListener(mVibratorWrapper::cancelVibrate);
+        }
+
         float targetProgress = toState.getVerticalProgress(mLauncher);
         if (Float.compare(mProgress, targetProgress) == 0) {
             setAlphas(toState, config, builder);
@@ -349,7 +384,7 @@
 
         setAlphas(toState, config, builder);
 
-        if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL)) {
+        if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL) && !(Utilities.ATLEAST_S)) {
             mLauncher.getAppsView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
                     HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
         }
@@ -487,4 +522,45 @@
             }
         }
     }
+
+    /**
+     * This VibrationAnimatorUpdateListener class takes in four parameters, a controller, start
+     * threshold, end threshold, and a Vibrator wrapper. We use the progress given by the controller
+     * as it gives an accurate progress that dictates where the vibrator should vibrate.
+     * Note: once the user begins a gesture and does the commit haptic, there should not be anymore
+     * haptics played for that gesture.
+     */
+    private static class VibrationAnimatorUpdateListener implements
+            ValueAnimator.AnimatorUpdateListener {
+        private final VibratorWrapper mVibratorWrapper;
+        private final AllAppsTransitionController mController;
+        private final float mStartThreshold;
+        private final float mEndThreshold;
+        private boolean mHasCommitted;
+
+        VibrationAnimatorUpdateListener(AllAppsTransitionController controller,
+                                        VibratorWrapper vibratorWrapper, float startThreshold,
+                                        float endThreshold) {
+            mController = controller;
+            mVibratorWrapper = vibratorWrapper;
+            mStartThreshold = startThreshold;
+            mEndThreshold = endThreshold;
+        }
+
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            if (mHasCommitted) {
+                return;
+            }
+            float currentProgress =
+                    AllAppsTransitionController.ALL_APPS_PROGRESS.get(mController);
+            if (currentProgress > mStartThreshold && currentProgress < mEndThreshold) {
+                mVibratorWrapper.vibrateForDragTexture();
+            } else if (!(currentProgress == 0 || currentProgress == 1)) {
+                // This check guards against committing at the location of the start of the gesture
+                mVibratorWrapper.vibrateForDragCommit();
+                mHasCommitted = true;
+            }
+        }
+    }
 }
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 9587447..2dc3e70 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -316,8 +316,11 @@
             "SCROLL_TOP_TO_RESET", true, "Bring up IME and focus on "
             + "input when scroll to top if 'Always show keyboard' is enabled or in prefix state");
 
-    public static final BooleanFlag POPUP_MATERIAL_U = new DeviceFlag(
-            "POPUP_MATERIAL_U", false, "Switch popup UX to use material U");
+    public static final BooleanFlag ENABLE_MATERIAL_U_POPUP = getDebugFlag(
+            "ENABLE_MATERIAL_U_POPUP", false, "Switch popup UX to use material U");
+
+    public static final BooleanFlag ENABLE_SEARCH_UNINSTALLED_APPS = new DeviceFlag(
+            "ENABLE_SEARCH_UNINSTALLED_APPS", false, "Search uninstalled app results.");
 
     public static final BooleanFlag SHOW_HOME_GARDENING = getDebugFlag(
             "SHOW_HOME_GARDENING", false,
@@ -335,10 +338,6 @@
             "ENABLE_DOWNLOAD_APP_UX_V3", false, "Updates the download app UX"
             + " to have better visuals, improve contrast, and color");
 
-    public static final BooleanFlag ENABLE_TASKBAR_REVISED_THRESHOLDS = getDebugFlag(
-            "ENABLE_TASKBAR_REVISED_THRESHOLDS", true,
-            "Uses revised thresholds for transient taskbar.");
-
     public static final BooleanFlag FORCE_PERSISTENT_TASKBAR = getDebugFlag(
             "FORCE_PERSISTENT_TASKBAR", false, "Forces taskbar to be persistent, even in gesture"
                     + " nav mode and when transient taskbar is enabled.");
@@ -372,6 +371,8 @@
             "ENABLE_LAUNCH_FROM_STAGED_APP", true,
             "Enable the ability to tap a staged app during split select to launch it in full screen"
     );
+    public static final BooleanFlag ENABLE_HAPTICS_ALL_APPS = getDebugFlag(
+            "ENABLE_HAPTICS_ALL_APPS", false, "Enables haptics opening/closing All apps");
 
     public static final BooleanFlag ENABLE_FORCED_MONO_ICON = getDebugFlag(
             "ENABLE_FORCED_MONO_ICON", false,
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 9a34478..d31a646 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -176,7 +176,7 @@
                 MAIN_EXECUTOR.submit(() ->
                         Launcher.ACTIVITY_TRACKER.getCreatedActivity().getRotationHelper()
                                 .forceAllowRotationForTesting(Boolean.parseBoolean(arg)));
-                return null;
+                return response;
 
             case TestProtocol.REQUEST_WORKSPACE_CELL_LAYOUT_SIZE:
                 return getLauncherUIProperty(Bundle::putIntArray, launcher -> {
@@ -232,6 +232,13 @@
                         l -> l.getAppsView().getActiveRecyclerView().getClipBounds().top);
             }
 
+            case TestProtocol.REQUEST_ALL_APPS_BOTTOM_PADDING: {
+                return getLauncherUIProperty(Bundle::putInt,
+                        l -> l.getAppsView().getBottom()
+                                - l.getAppsView().getActiveRecyclerView().getBottom()
+                                + l.getAppsView().getActiveRecyclerView().getPaddingBottom());
+            }
+
             default:
                 return null;
         }
diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java
index bfd0e1b..a53751f 100644
--- a/src/com/android/launcher3/touch/AllAppsSwipeController.java
+++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java
@@ -129,10 +129,7 @@
             Interpolators.clampToProgress(
                     Interpolators.mapToProgress(EMPHASIZED_DECELERATE, 0.4f, 1f),
                     ALL_APPS_STATE_TRANSITION_ATOMIC, 1f);
-    public static final Interpolator ALL_APPS_VERTICAL_PROGRESS_MANUAL =
-            Interpolators.clampToProgress(
-                    Interpolators.mapToProgress(LINEAR, ALL_APPS_STATE_TRANSITION_MANUAL, 1f),
-                    ALL_APPS_STATE_TRANSITION_MANUAL, 1f);
+    public static final Interpolator ALL_APPS_VERTICAL_PROGRESS_MANUAL = LINEAR;
 
     // --------
 
diff --git a/src/com/android/launcher3/util/GridOccupancy.java b/src/com/android/launcher3/util/GridOccupancy.java
index 1301460..43e486c 100644
--- a/src/com/android/launcher3/util/GridOccupancy.java
+++ b/src/com/android/launcher3/util/GridOccupancy.java
@@ -81,4 +81,16 @@
     public void clear() {
         markCells(0, 0, mCountX, mCountY, false);
     }
+
+    @Override
+    public String toString() {
+        StringBuilder s = new StringBuilder("Grid: \n");
+        for (int y = 0; y < mCountY; y++) {
+            for (int x = 0; x < mCountX; x++) {
+                s.append(cells[x][y] ? 1 : 0).append(" ");
+            }
+            s.append("\n");
+        }
+        return s.toString();
+    }
 }
diff --git a/src/com/android/launcher3/util/ResourceHelper.kt b/src/com/android/launcher3/util/ResourceHelper.kt
new file mode 100644
index 0000000..0ca7888
--- /dev/null
+++ b/src/com/android/launcher3/util/ResourceHelper.kt
@@ -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.util
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.content.res.XmlResourceParser
+import android.util.AttributeSet
+import kotlin.IntArray
+
+/**
+ * This class is a helper that can be subclassed in tests to provide a way to parse attributes
+ * correctly.
+ */
+open class ResourceHelper(private val context: Context, private val specsFileId: Int) {
+    open fun getXml(): XmlResourceParser {
+        return context.resources.getXml(specsFileId)
+    }
+
+    open fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray {
+        return context.obtainStyledAttributes(attrs, styleId)
+    }
+}
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index 932bcfc..ceba0db 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -17,7 +17,6 @@
 
 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;
 
@@ -28,12 +27,17 @@
 import android.database.ContentObserver;
 import android.media.AudioAttributes;
 import android.os.Build;
+import android.os.SystemClock;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.provider.Settings;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.Utilities;
-import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.anim.PendingAnimation;
+
+import java.util.function.Consumer;
 
 /**
  * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary.
@@ -52,6 +56,21 @@
     public static final VibrationEffect EFFECT_CLICK =
             createPredefined(VibrationEffect.EFFECT_CLICK);
 
+    private static final float DRAG_TEXTURE_SCALE = 0.03f;
+    private static final float DRAG_COMMIT_SCALE = 0.5f;
+    private static final float DRAG_BUMP_SCALE = 0.4f;
+    private static final int DRAG_TEXTURE_EFFECT_SIZE = 200;
+
+    @Nullable
+    private final VibrationEffect mDragEffect;
+    @Nullable
+    private final VibrationEffect mCommitEffect;
+    @Nullable
+    private final VibrationEffect mBumpEffect;
+
+    private long mLastDragTime;
+    private final int mThresholdUntilNextDragCallMillis;
+
     /**
      * Haptic when entering overview.
      */
@@ -62,7 +81,7 @@
 
     private boolean mIsHapticFeedbackEnabled;
 
-    public VibratorWrapper(Context context) {
+    private VibratorWrapper(Context context) {
         mVibrator = context.getSystemService(Vibrator.class);
         mHasVibrator = mVibrator.hasVibrator();
         if (mHasVibrator) {
@@ -75,12 +94,88 @@
                 }
             };
             resolver.registerContentObserver(Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED),
-                    false /* notifyForDescendents */, observer);
+                    false /* notifyForDescendants */, observer);
         } else {
             mIsHapticFeedbackEnabled = false;
         }
+
+        if (Utilities.ATLEAST_S && mVibrator.areAllPrimitivesSupported(
+                VibrationEffect.Composition.PRIMITIVE_LOW_TICK)) {
+
+            // Drag texture, Commit, and Bump should only be used for premium phones.
+            // Before using these haptics make sure check if the device can use it
+            VibrationEffect.Composition dragEffect = VibrationEffect.startComposition();
+            for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) {
+                dragEffect.addPrimitive(
+                        VibrationEffect.Composition.PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE);
+            }
+            mDragEffect = dragEffect.compose();
+            mCommitEffect = VibrationEffect.startComposition().addPrimitive(
+                    VibrationEffect.Composition.PRIMITIVE_TICK, DRAG_COMMIT_SCALE).compose();
+            mBumpEffect = VibrationEffect.startComposition().addPrimitive(
+                    VibrationEffect.Composition.PRIMITIVE_LOW_TICK, DRAG_BUMP_SCALE).compose();
+            int primitiveDuration = mVibrator.getPrimitiveDurations(
+                    VibrationEffect.Composition.PRIMITIVE_LOW_TICK)[0];
+
+            mThresholdUntilNextDragCallMillis =
+                    DRAG_TEXTURE_EFFECT_SIZE * primitiveDuration + 100;
+        } else {
+            mDragEffect = null;
+            mCommitEffect = null;
+            mBumpEffect = null;
+            mThresholdUntilNextDragCallMillis = 0;
+        }
     }
 
+    /**
+     *  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) {
+            return;
+        }
+        long currentTime = SystemClock.elapsedRealtime();
+        long elapsedTimeSinceDrag = currentTime - mLastDragTime;
+        if (elapsedTimeSinceDrag >= mThresholdUntilNextDragCallMillis) {
+            vibrate(mDragEffect);
+            mLastDragTime = currentTime;
+        }
+    }
+
+    /**
+     *  This is used when user reaches the commit threshold when swiping to/from from all apps.
+     */
+    public void vibrateForDragCommit() {
+        if (mCommitEffect != null) {
+            vibrate(mCommitEffect);
+        }
+        // resetting dragTexture timestamp to be able to play dragTexture again
+        mLastDragTime = 0;
+    }
+
+    /**
+     *  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) {
+            vibrate(mBumpEffect);
+        }
+    }
+
+    /**
+     * 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)}.
+     */
+    public void cancelVibrate(boolean unused) {
+        UI_HELPER_EXECUTOR.execute(mVibrator::cancel);
+        // 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/workspace/WorkspaceSpecs.kt b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
new file mode 100644
index 0000000..0f6e1b0
--- /dev/null
+++ b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
@@ -0,0 +1,252 @@
+/*
+ * 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 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
+            }
+        }
+    }
+}
+
+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/tests/Android.bp b/tests/Android.bp
index 39bd307..7144d65 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -117,3 +117,15 @@
     test_config: "Launcher3Tests.xml",
     data: [":Launcher3"]
 }
+
+// Shared between tests and launcher
+android_library {
+    name: "launcher-testing-shared",
+    srcs: [
+        "shared/com/android/launcher3/testing/shared/**/*.java"
+    ],
+    resource_dirs: [ ],
+    manifest: "shared/AndroidManifest.xml",
+    sdk_version: "current",
+    min_sdk_version: min_launcher3_sdk_version,
+ }
\ No newline at end of file
diff --git a/tests/res/values/attrs.xml b/tests/res/values/attrs.xml
new file mode 100644
index 0000000..2310d9e
--- /dev/null
+++ b/tests/res/values/attrs.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+/* Copyright 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.
+*/
+-->
+
+<!-- Attributes have to be copied to test for correct parsing of files -->
+<resources>
+    <!--  Responsive grids attributes  -->
+    <declare-styleable name="WorkspaceSpec">
+        <attr name="specType" format="integer">
+            <enum name="height" value="0" />
+            <enum name="width" value="1" />
+        </attr>
+        <attr name="maxAvailableSize" format="dimension" />
+    </declare-styleable>
+
+    <declare-styleable name="SpecSize">
+        <attr name="fixedSize" format="dimension" />
+        <attr name="ofAvailableSpace" format="float" />
+        <attr name="ofRemainderSpace" format="float" />
+    </declare-styleable>
+
+</resources>
diff --git a/tests/res/xml/invalid_workspace_file_case_1.xml b/tests/res/xml/invalid_workspace_file_case_1.xml
new file mode 100644
index 0000000..0be704b
--- /dev/null
+++ b/tests/res/xml/invalid_workspace_file_case_1.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.
+  -->
+
+<workspaceSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <workspaceSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="648dp">
+        <!--  missing startPadding  -->
+        <endPadding
+            launcher:ofAvailableSpace="0.05" />
+        <gutter
+            launcher:fixedSize="16dp" />
+        <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/invalid_workspace_file_case_2.xml b/tests/res/xml/invalid_workspace_file_case_2.xml
new file mode 100644
index 0000000..5a37d97
--- /dev/null
+++ b/tests/res/xml/invalid_workspace_file_case_2.xml
@@ -0,0 +1,59 @@
+<?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" />
+        <!--  more than 1 value in one tag -->
+        <gutter
+            launcher:ofAvailableSpace="0.0125"
+            launcher:fixedSize="16dp" />
+        <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/invalid_workspace_file_case_3.xml b/tests/res/xml/invalid_workspace_file_case_3.xml
new file mode 100644
index 0000000..3e68edb
--- /dev/null
+++ b/tests/res/xml/invalid_workspace_file_case_3.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" />
+        <gutter
+            launcher:fixedSize="16dp" />
+        <!--  value bigger than 1 -->
+        <cellSize
+            launcher:ofRemainderSpace="1.001" />
+    </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/valid_workspace_file.xml b/tests/res/xml/valid_workspace_file.xml
new file mode 100644
index 0000000..91a3e48
--- /dev/null
+++ b/tests/res/xml/valid_workspace_file.xml
@@ -0,0 +1,57 @@
+<?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" />
+        <gutter
+            launcher:fixedSize="16dp" />
+        <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/shared/AndroidManifest.xml b/tests/shared/AndroidManifest.xml
new file mode 100644
index 0000000..1cd0cb3
--- /dev/null
+++ b/tests/shared/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+<manifest package="com.android.launcher3.testing.shared">
+</manifest>
diff --git a/src/com/android/launcher3/testing/shared/HotseatCellCenterRequest.java b/tests/shared/com/android/launcher3/testing/shared/HotseatCellCenterRequest.java
similarity index 100%
rename from src/com/android/launcher3/testing/shared/HotseatCellCenterRequest.java
rename to tests/shared/com/android/launcher3/testing/shared/HotseatCellCenterRequest.java
diff --git a/src/com/android/launcher3/testing/shared/ResourceUtils.java b/tests/shared/com/android/launcher3/testing/shared/ResourceUtils.java
similarity index 100%
rename from src/com/android/launcher3/testing/shared/ResourceUtils.java
rename to tests/shared/com/android/launcher3/testing/shared/ResourceUtils.java
diff --git a/src/com/android/launcher3/testing/shared/TestInformationRequest.java b/tests/shared/com/android/launcher3/testing/shared/TestInformationRequest.java
similarity index 100%
rename from src/com/android/launcher3/testing/shared/TestInformationRequest.java
rename to tests/shared/com/android/launcher3/testing/shared/TestInformationRequest.java
diff --git a/src/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
similarity index 98%
rename from src/com/android/launcher3/testing/shared/TestProtocol.java
rename to tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 9b2ce9a..11363a2 100644
--- a/src/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -120,6 +120,7 @@
     public static final String REQUEST_TASKBAR_ALL_APPS_TOP_PADDING =
             "taskbar-all-apps-top-padding";
     public static final String REQUEST_ALL_APPS_TOP_PADDING = "all-apps-top-padding";
+    public static final String REQUEST_ALL_APPS_BOTTOM_PADDING = "all-apps-bottom-padding";
 
     public static final String REQUEST_WORKSPACE_CELL_LAYOUT_SIZE = "workspace-cell-layout-size";
     public static final String REQUEST_WORKSPACE_CELL_CENTER = "workspace-cell-center";
diff --git a/src/com/android/launcher3/testing/shared/WorkspaceCellCenterRequest.java b/tests/shared/com/android/launcher3/testing/shared/WorkspaceCellCenterRequest.java
similarity index 100%
rename from src/com/android/launcher3/testing/shared/WorkspaceCellCenterRequest.java
rename to tests/shared/com/android/launcher3/testing/shared/WorkspaceCellCenterRequest.java
diff --git a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
new file mode 100644
index 0000000..dcc669b
--- /dev/null
+++ b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -0,0 +1,272 @@
+/*
+ * 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
+
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Point
+import android.graphics.Rect
+import android.util.DisplayMetrics
+import android.view.Surface
+import androidx.test.core.app.ApplicationProvider
+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 kotlin.math.max
+import kotlin.math.min
+import org.junit.After
+import org.junit.Before
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when` as whenever
+
+/**
+ * This is an abstract class for DeviceProfile tests that create an InvariantDeviceProfile based on
+ * a real device spec.
+ *
+ * For an implementation that mocks InvariantDeviceProfile, use [FakeInvariantDeviceProfileTest]
+ */
+abstract class AbstractDeviceProfileTest {
+    protected var context: Context? = null
+    protected open val runningContext: Context = ApplicationProvider.getApplicationContext()
+    private var displayController: DisplayController = mock(DisplayController::class.java)
+    private var windowManagerProxy: WindowManagerProxy = mock(WindowManagerProxy::class.java)
+    private lateinit var originalDisplayController: DisplayController
+    private lateinit var originalWindowManagerProxy: WindowManagerProxy
+
+    @Before
+    fun setUp() {
+        val appContext: Context = ApplicationProvider.getApplicationContext()
+        originalWindowManagerProxy = WindowManagerProxy.INSTANCE.get(appContext)
+        originalDisplayController = DisplayController.INSTANCE.get(appContext)
+        WindowManagerProxy.INSTANCE.initializeForTesting(windowManagerProxy)
+        DisplayController.INSTANCE.initializeForTesting(displayController)
+    }
+
+    @After
+    fun tearDown() {
+        WindowManagerProxy.INSTANCE.initializeForTesting(originalWindowManagerProxy)
+        DisplayController.INSTANCE.initializeForTesting(originalDisplayController)
+    }
+
+    class DeviceSpec(
+        val naturalSize: Pair<Int, Int>,
+        val densityDpi: Int,
+        val statusBarNaturalPx: Int,
+        val statusBarRotatedPx: Int,
+        val gesturePx: Int,
+        val cutoutPx: Int
+    )
+
+    open val deviceSpecs =
+        mapOf(
+            "phone" to
+                DeviceSpec(
+                    Pair(1080, 2400),
+                    densityDpi = 420,
+                    statusBarNaturalPx = 118,
+                    statusBarRotatedPx = 74,
+                    gesturePx = 63,
+                    cutoutPx = 118
+                ),
+            "tablet" to
+                DeviceSpec(
+                    Pair(2560, 1600),
+                    densityDpi = 320,
+                    statusBarNaturalPx = 104,
+                    statusBarRotatedPx = 104,
+                    gesturePx = 0,
+                    cutoutPx = 0
+                ),
+        )
+
+    protected fun initializeVarsForPhone(
+        deviceSpec: DeviceSpec,
+        isGestureMode: Boolean = true,
+        isVerticalBar: Boolean = false
+    ) {
+        val (naturalX, naturalY) = deviceSpec.naturalSize
+        val windowsBounds = phoneWindowsBounds(deviceSpec, isGestureMode, naturalX, naturalY)
+        val displayInfo =
+            CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0))
+        val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds)
+
+        initializeCommonVars(
+            perDisplayBoundsCache,
+            displayInfo,
+            rotation = if (isVerticalBar) Surface.ROTATION_90 else Surface.ROTATION_0,
+            isGestureMode,
+            densityDpi = deviceSpec.densityDpi
+        )
+    }
+
+    protected fun initializeVarsForTablet(
+        deviceSpec: DeviceSpec,
+        isLandscape: Boolean = false,
+        isGestureMode: Boolean = true
+    ) {
+        val (naturalX, naturalY) = deviceSpec.naturalSize
+        val windowsBounds = tabletWindowsBounds(deviceSpec, naturalX, naturalY)
+        val displayInfo =
+            CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0))
+        val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds)
+
+        initializeCommonVars(
+            perDisplayBoundsCache,
+            displayInfo,
+            rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
+            isGestureMode,
+            densityDpi = deviceSpec.densityDpi
+        )
+    }
+
+    protected fun initializeVarsForTwoPanel(
+        deviceTabletSpec: DeviceSpec,
+        deviceSpec: DeviceSpec,
+        isLandscape: Boolean = false,
+        isGestureMode: Boolean = true
+    ) {
+        val (tabletNaturalX, tabletNaturalY) = deviceTabletSpec.naturalSize
+        val tabletWindowsBounds =
+            tabletWindowsBounds(deviceTabletSpec, tabletNaturalX, tabletNaturalY)
+        val tabletDisplayInfo =
+            CachedDisplayInfo(
+                Point(tabletNaturalX, tabletNaturalY),
+                Surface.ROTATION_0,
+                Rect(0, 0, 0, 0)
+            )
+
+        val (phoneNaturalX, phoneNaturalY) = deviceSpec.naturalSize
+        val phoneWindowsBounds =
+            phoneWindowsBounds(deviceSpec, isGestureMode, phoneNaturalX, phoneNaturalY)
+        val phoneDisplayInfo =
+            CachedDisplayInfo(
+                Point(phoneNaturalX, phoneNaturalY),
+                Surface.ROTATION_0,
+                Rect(0, 0, 0, 0)
+            )
+
+        val perDisplayBoundsCache =
+            mapOf(tabletDisplayInfo to tabletWindowsBounds, phoneDisplayInfo to phoneWindowsBounds)
+
+        initializeCommonVars(
+            perDisplayBoundsCache,
+            tabletDisplayInfo,
+            rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
+            isGestureMode,
+            densityDpi = deviceTabletSpec.densityDpi
+        )
+    }
+
+    private fun phoneWindowsBounds(
+        deviceSpec: DeviceSpec,
+        isGestureMode: Boolean,
+        naturalX: Int,
+        naturalY: Int
+    ): Array<WindowBounds> {
+        val buttonsNavHeight = Utilities.dpToPx(48f, deviceSpec.densityDpi)
+
+        val rotation0Insets =
+            Rect(
+                0,
+                max(deviceSpec.statusBarNaturalPx, deviceSpec.cutoutPx),
+                0,
+                if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight
+            )
+        val rotation90Insets =
+            Rect(
+                deviceSpec.cutoutPx,
+                deviceSpec.statusBarRotatedPx,
+                if (isGestureMode) 0 else buttonsNavHeight,
+                if (isGestureMode) deviceSpec.gesturePx else 0
+            )
+        val rotation180Insets =
+            Rect(
+                0,
+                deviceSpec.statusBarNaturalPx,
+                0,
+                max(
+                    if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight,
+                    deviceSpec.cutoutPx
+                )
+            )
+        val rotation270Insets =
+            Rect(
+                if (isGestureMode) 0 else buttonsNavHeight,
+                deviceSpec.statusBarRotatedPx,
+                deviceSpec.cutoutPx,
+                if (isGestureMode) deviceSpec.gesturePx else 0
+            )
+
+        return arrayOf(
+            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),
+            WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270)
+        )
+    }
+
+    private fun tabletWindowsBounds(
+        deviceSpec: DeviceSpec,
+        naturalX: Int,
+        naturalY: Int
+    ): Array<WindowBounds> {
+        val naturalInsets = Rect(0, deviceSpec.statusBarNaturalPx, 0, 0)
+        val rotatedInsets = Rect(0, deviceSpec.statusBarRotatedPx, 0, 0)
+
+        return arrayOf(
+            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),
+            WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270)
+        )
+    }
+
+    private fun initializeCommonVars(
+        perDisplayBoundsCache: Map<CachedDisplayInfo, Array<WindowBounds>>,
+        displayInfo: CachedDisplayInfo,
+        rotation: Int,
+        isGestureMode: Boolean = true,
+        densityDpi: Int
+    ) {
+        val windowsBounds = perDisplayBoundsCache[displayInfo]!!
+        val realBounds = windowsBounds[rotation]
+        whenever(windowManagerProxy.getDisplayInfo(ArgumentMatchers.any())).thenReturn(displayInfo)
+        whenever(windowManagerProxy.getRealBounds(ArgumentMatchers.any(), ArgumentMatchers.any()))
+            .thenReturn(realBounds)
+        whenever(windowManagerProxy.getRotation(ArgumentMatchers.any())).thenReturn(rotation)
+        whenever(windowManagerProxy.getNavigationMode(ArgumentMatchers.any()))
+            .thenReturn(
+                if (isGestureMode) NavigationMode.NO_BUTTON else NavigationMode.THREE_BUTTONS
+            )
+
+        val density = densityDpi / DisplayMetrics.DENSITY_DEFAULT.toFloat()
+        val config =
+            Configuration(runningContext.resources.configuration).apply {
+                this.densityDpi = densityDpi
+                screenWidthDp = (realBounds.bounds.width() / density).toInt()
+                screenHeightDp = (realBounds.bounds.height() / density).toInt()
+                smallestScreenWidthDp = min(screenWidthDp, screenHeightDp)
+            }
+        context = runningContext.createConfigurationContext(config)
+
+        val info = DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache)
+        whenever(displayController.info).thenReturn(info)
+        whenever(displayController.isTransientTaskbar).thenReturn(isGestureMode)
+    }
+}
diff --git a/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
similarity index 95%
rename from tests/src/com/android/launcher3/DeviceProfileBaseTest.kt
rename to tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
index 0aecfb2..a5f33c0 100644
--- a/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt
+++ b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
@@ -31,7 +31,13 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.`when` as whenever
 
-abstract class DeviceProfileBaseTest {
+/**
+ * This is an abstract class for DeviceProfile tests that don't need the real Context and mock an
+ * InvariantDeviceProfile instead of creating one based on real values.
+ *
+ * For an implementation that creates InvariantDeviceProfile, use [AbstractDeviceProfileTest]
+ */
+abstract class FakeInvariantDeviceProfileTest {
 
     protected var context: Context? = null
     protected var inv: InvariantDeviceProfile? = null
@@ -120,6 +126,7 @@
 
                 horizontalMargin = FloatArray(4) { 22f }
 
+                allAppsStyle = R.style.AllAppsStyleDefault
                 allAppsCellSize =
                     listOf(
                             PointF(80f, 104f),
@@ -198,6 +205,7 @@
 
                 horizontalMargin = floatArrayOf(54f, 120f, 54f, 54f)
 
+                allAppsStyle = R.style.AllAppsStyleDefault
                 allAppsCellSize =
                     listOf(
                             PointF(96f, 142f),
@@ -277,6 +285,7 @@
 
                 horizontalMargin = floatArrayOf(21.5f, 21.5f, 22.5f, 30.5f)
 
+                allAppsStyle = R.style.AllAppsStyleDefault
                 allAppsCellSize =
                     listOf(PointF(0f, 0f), PointF(0f, 0f), PointF(68f, 104f), PointF(80f, 104f))
                         .toTypedArray()
diff --git a/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt b/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt
index 951f5f8..2a27487 100644
--- a/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt
+++ b/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt
@@ -18,7 +18,7 @@
 import android.graphics.Rect
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.launcher3.DeviceProfileBaseTest
+import com.android.launcher3.FakeInvariantDeviceProfileTest
 import com.android.launcher3.util.WindowBounds
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -26,7 +26,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class HotseatWidthCalculationTest : DeviceProfileBaseTest() {
+class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() {
 
     /**
      * This is a case when after setting the hotseat, the space needs to be recalculated but it
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 4b04c7e..401c25a4 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -169,9 +169,9 @@
                     flingBackwardY < flingForwardY));
 
             // Test scrolling down to YouTube.
-            assertNotNull("All apps: can't fine YouTube", allApps.getAppIcon("YouTube"));
+            assertNotNull("All apps: can't find YouTube", allApps.getAppIcon("YouTube"));
             // Test scrolling up to Camera.
-            assertNotNull("All apps: can't fine Camera", allApps.getAppIcon("Camera"));
+            assertNotNull("All apps: can't find Camera", allApps.getAppIcon("Camera"));
             // Test failing to find a non-existing app.
             final AllApps allAppsFinal = allApps;
             expectFail("All apps: could find a non-existing app",
@@ -263,8 +263,7 @@
             assertNotNull("AppIcon.launch returned null", app.launch(getAppPackageName()));
             test.executeOnLauncher(launcher -> assertTrue(
                     "Launcher activity is the top activity; expecting another activity to be the "
-                            + "top "
-                            + "one",
+                            + "top one",
                     test.isInLaunchedApp(launcher)));
         } finally {
             allApps.unfreeze();
diff --git a/tests/src/com/android/launcher3/util/TestResourceHelper.kt b/tests/src/com/android/launcher3/util/TestResourceHelper.kt
new file mode 100644
index 0000000..fb03fe1
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/TestResourceHelper.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.TypedArray
+import android.util.AttributeSet
+import com.android.launcher3.R
+import com.android.launcher3.tests.R as TestR
+import kotlin.IntArray
+
+class TestResourceHelper(private val context: Context, private val 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
+        return context.obtainStyledAttributes(attrs, clone)
+    }
+}
diff --git a/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt b/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
new file mode 100644
index 0000000..0fd8a54
--- /dev/null
+++ b/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.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.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(2)
+        assertThat(workspaceSpecs.workspaceHeightSpecList[0].toString())
+            .isEqualTo(
+                "WorkspaceSpec(" +
+                    "maxAvailableSize=1701, " +
+                    "specType=HEIGHT, " +
+                    "startPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0125, " +
+                    "ofRemainderSpace=0.0), " +
+                    "endPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.05, " +
+                    "ofRemainderSpace=0.0), " +
+                    "gutter=SizeSpec(fixedSize=42.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0), " +
+                    "cellSize=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.2)" +
+                    ")"
+            )
+        assertThat(workspaceSpecs.workspaceHeightSpecList[1].toString())
+            .isEqualTo(
+                "WorkspaceSpec(" +
+                    "maxAvailableSize=26247, " +
+                    "specType=HEIGHT, " +
+                    "startPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0306, " +
+                    "ofRemainderSpace=0.0), " +
+                    "endPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.068, " +
+                    "ofRemainderSpace=0.0), " +
+                    "gutter=SizeSpec(fixedSize=42.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0), " +
+                    "cellSize=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.2)" +
+                    ")"
+            )
+        assertThat(workspaceSpecs.workspaceWidthSpecList.size).isEqualTo(1)
+        assertThat(workspaceSpecs.workspaceWidthSpecList[0].toString())
+            .isEqualTo(
+                "WorkspaceSpec(" +
+                    "maxAvailableSize=26247, " +
+                    "specType=WIDTH, " +
+                    "startPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.21436226), " +
+                    "endPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.21436226), " +
+                    "gutter=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.11425509), " +
+                    "cellSize=SizeSpec(fixedSize=315.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0)" +
+                    ")"
+            )
+    }
+
+    @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/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 6f6428a..2d4d2cd 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -34,6 +34,9 @@
 
 import com.android.launcher3.testing.shared.TestProtocol;
 
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
 import java.util.stream.Collectors;
 
 /**
@@ -102,10 +105,10 @@
                 iconCenter.x, iconCenter.y);
     }
 
-    private boolean iconCenterInRecyclerTopPadding(UiObject2 appListRecycler, UiObject2 icon) {
+    private boolean iconCenterInRecyclerTopPadding(UiObject2 appsListRecycler, UiObject2 icon) {
         final Point iconCenter = icon.getVisibleCenter();
 
-        return iconCenter.y <= mLauncher.getVisibleBounds(appListRecycler).top
+        return iconCenter.y <= mLauncher.getVisibleBounds(appsListRecycler).top
                 + getAppsListRecyclerTopPadding();
     }
 
@@ -137,15 +140,11 @@
                             bottomGestureStartOnScreen)) {
                         mLauncher.scrollToLastVisibleRow(
                                 allAppsContainer,
-                                mLauncher.getObjectsInContainer(allAppsContainer, "icon")
-                                        .stream()
-                                        .filter(icon ->
-                                                mLauncher.getVisibleBounds(icon).top
-                                                        < bottomGestureStartOnScreen)
-                                        .collect(Collectors.toList()),
+                                getBottomVisibleIconBounds(allAppsContainer),
                                 mLauncher.getVisibleBounds(appListRecycler).top
                                         + getAppsListRecyclerTopPadding()
-                                        - mLauncher.getVisibleBounds(allAppsContainer).top);
+                                        - mLauncher.getVisibleBounds(allAppsContainer).top,
+                                getAppsListRecyclerBottomPadding());
                         verifyActiveContainer();
                         final int newScroll = getAllAppsScroll();
                         mLauncher.assertTrue(
@@ -175,6 +174,28 @@
         }
     }
 
+    /** @return visible bounds of the top-most visible icon in the container. */
+    protected Rect getTopVisibleIconBounds(UiObject2 allAppsContainer) {
+        return mLauncher.getVisibleBounds(Collections.min(getVisibleIcons(allAppsContainer),
+                Comparator.comparingInt(i -> mLauncher.getVisibleBounds(i).top)));
+    }
+
+    /** @return visible bounds of the bottom-most visible icon in the container. */
+    protected Rect getBottomVisibleIconBounds(UiObject2 allAppsContainer) {
+        return mLauncher.getVisibleBounds(Collections.max(getVisibleIcons(allAppsContainer),
+                Comparator.comparingInt(i -> mLauncher.getVisibleBounds(i).top)));
+    }
+
+    @NonNull
+    private List<UiObject2> getVisibleIcons(UiObject2 allAppsContainer) {
+        return mLauncher.getObjectsInContainer(allAppsContainer, "icon")
+                .stream()
+                .filter(icon ->
+                        mLauncher.getVisibleBounds(icon).top
+                                < mLauncher.getBottomGestureStartOnScreen())
+                .collect(Collectors.toList());
+    }
+
     /**
      * Finds an icon. Fails if the icon doesn't exist. Scrolls the app list when needed to make
      * sure the icon is visible.
@@ -196,20 +217,23 @@
 
     protected abstract int getAppsListRecyclerTopPadding();
 
+    protected int getAppsListRecyclerBottomPadding() {
+        return mLauncher.getTestInfo(TestProtocol.REQUEST_ALL_APPS_BOTTOM_PADDING)
+                .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
     private void scrollBackToBeginning() {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to scroll back in all apps")) {
             LauncherInstrumentation.log("Scrolling to the beginning");
             final UiObject2 allAppsContainer = verifyActiveContainer();
-            final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer);
 
             int attempts = 0;
             final Rect margins = new Rect(
                     /* left= */ 0,
-                    mLauncher.getVisibleBounds(appListRecycler).top
-                            + getAppsListRecyclerTopPadding() + 1,
+                    getTopVisibleIconBounds(allAppsContainer).bottom,
                     /* right= */ 0,
-                    /* bottom= */ 5);
+                    /* bottom= */ getAppsListRecyclerBottomPadding());
 
             for (int scroll = getAllAppsScroll();
                     scroll != 0;
@@ -240,7 +264,7 @@
                 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
-    private UiObject2 getAppListRecycler(UiObject2 allAppsContainer) {
+    protected UiObject2 getAppListRecycler(UiObject2 allAppsContainer) {
         return mLauncher.waitForObjectInContainer(allAppsContainer, "apps_list_view");
     }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
index 50b03aa..e0c4c19 100644
--- a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
@@ -17,15 +17,11 @@
 
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 
-import android.graphics.Rect;
-
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.UiObject2;
 
 import com.android.launcher3.testing.shared.TestProtocol;
 
-import java.util.Objects;
-
 public class HomeAllApps extends AllApps {
     private static final String BOTTOM_SHEET_RES_ID = "bottom_sheet_background";
 
@@ -45,10 +41,8 @@
                      mLauncher.addContextLayer("want to switch from all apps to workspace")) {
             UiObject2 allAppsContainer = verifyActiveContainer();
 
-            final Rect searchBoxBounds = Objects.requireNonNull(
-                    mLauncher.getVisibleBounds(getSearchBox(allAppsContainer)));
-            final int startX = searchBoxBounds.centerX();
-            final int startY = searchBoxBounds.bottom;
+            final int startX = allAppsContainer.getVisibleCenter().x;
+            final int startY = getTopVisibleIconBounds(allAppsContainer).centerY();
             final int endY = mLauncher.getDevice().getDisplayHeight();
             LauncherInstrumentation.log(
                     "switchToWorkspace: startY = " + startY + ", endY = " + endY
@@ -59,7 +53,7 @@
                     startY,
                     startX,
                     endY,
-                    12 /* steps */,
+                    5 /* steps */,
                     NORMAL_STATE_ORDINAL, LauncherInstrumentation.GestureScope.INSIDE);
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 2d1c963..52994a5 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -76,8 +76,6 @@
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.Deque;
 import java.util.LinkedList;
 import java.util.List;
@@ -1486,19 +1484,21 @@
     }
 
     void scrollToLastVisibleRow(
-            UiObject2 container, Collection<UiObject2> items, int topPaddingInContainer) {
-        final UiObject2 lowestItem = Collections.max(items, (i1, i2) ->
-                Integer.compare(getVisibleBounds(i1).top, getVisibleBounds(i2).top));
-
-        final int itemRowCurrentTopOnScreen = getVisibleBounds(lowestItem).top;
+            UiObject2 container, Rect bottomVisibleIconBounds, int topPaddingInContainer,
+            int appsListBottomPadding) {
+        final int itemRowCurrentTopOnScreen = bottomVisibleIconBounds.top;
         final Rect containerRect = getVisibleBounds(container);
         final int itemRowNewTopOnScreen = containerRect.top + topPaddingInContainer;
         final int distance = itemRowCurrentTopOnScreen - itemRowNewTopOnScreen + getTouchSlop();
 
-        scrollDownByDistance(container, distance);
+        scrollDownByDistance(container, distance, appsListBottomPadding);
     }
 
     void scrollDownByDistance(UiObject2 container, int distance) {
+        scrollDownByDistance(container, distance, 0);
+    }
+
+    void scrollDownByDistance(UiObject2 container, int distance, int bottomPadding) {
         final Rect containerRect = getVisibleBounds(container);
         final int bottomGestureMarginInContainer = getBottomGestureMarginInContainer(container);
         scroll(
@@ -1508,7 +1508,7 @@
                         0,
                         containerRect.height() - distance - bottomGestureMarginInContainer,
                         0,
-                        bottomGestureMarginInContainer),
+                        bottomGestureMarginInContainer + bottomPadding),
                 /* steps= */ 10,
                 /* slowDown= */ true);
     }