Merge "Fix broken gesture nav edu on tablets in portrait." into udc-qpr-dev
diff --git a/OWNERS b/OWNERS
index b684460..7834396 100644
--- a/OWNERS
+++ b/OWNERS
@@ -13,6 +13,8 @@
 winsonc@google.com
 jonmiranda@google.com
 alexchau@google.com
+patmanning@google.com
+tsuharesu@google.com
 
 per-file FeatureFlags.java, globs = set noparent
 per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com, captaincole@google.com
diff --git a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
index 4e67629..69e1574 100644
--- a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
+++ b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
@@ -23,7 +23,7 @@
     android:importantForAccessibility="yes"
     android:background="@drawable/keyboard_quick_switch_task_view_background"
     android:clipToOutline="true"
-    launcher:borderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content"
diff --git a/quickstep/res/layout/keyboard_quick_switch_overview.xml b/quickstep/res/layout/keyboard_quick_switch_overview.xml
index e7b1f23..4a9b023 100644
--- a/quickstep/res/layout/keyboard_quick_switch_overview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_overview.xml
@@ -22,7 +22,7 @@
     android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
     android:clipToOutline="true"
     android:importantForAccessibility="yes"
-    launcher:borderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content"
diff --git a/quickstep/res/layout/keyboard_quick_switch_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_taskview.xml
index 4d213fa..6ed3c6e 100644
--- a/quickstep/res/layout/keyboard_quick_switch_taskview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_taskview.xml
@@ -23,7 +23,7 @@
     android:importantForAccessibility="yes"
     android:background="@drawable/keyboard_quick_switch_task_view_background"
     android:clipToOutline="true"
-    launcher:borderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content"
diff --git a/quickstep/res/layout/split_instructions_view.xml b/quickstep/res/layout/split_instructions_view.xml
index 91fb05c..c663bf4 100644
--- a/quickstep/res/layout/split_instructions_view.xml
+++ b/quickstep/res/layout/split_instructions_view.xml
@@ -24,12 +24,15 @@
     android:paddingTop="@dimen/split_instructions_vertical_padding"
     android:paddingBottom="@dimen/split_instructions_vertical_padding"
     android:elevation="@dimen/split_instructions_elevation"
-    android:visibility="gone">
+    android:visibility="gone"
+    android:importantForAccessibility="yes">
     <androidx.appcompat.widget.AppCompatTextView
         android:id="@+id/split_instructions_text"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content"
         android:gravity="center"
         android:textColor="?androidprv:attr/textColorOnAccent"
+        android:drawableEnd="@drawable/ic_split_exit"
+        android:drawablePadding="@dimen/split_instructions_drawable_padding"
         android:text="@string/toast_split_select_app" />
 </com.android.quickstep.views.SplitInstructionsView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 4865aef..29c9992 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -24,7 +24,8 @@
     android:clipChildren="false"
     android:defaultFocusHighlightEnabled="false"
     android:focusable="true"
-    launcher:borderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
+    launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
 
     <com.android.quickstep.views.TaskThumbnailView
         android:id="@+id/snapshot"
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index fd82c66..06f4d06 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -25,7 +25,8 @@
     android:clipToOutline="true"
     android:defaultFocusHighlightEnabled="false"
     android:focusable="true"
-    launcher:borderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
+    launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
 
     <View
         android:id="@+id/background"
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index c9fa9c0..75ff626 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -29,7 +29,8 @@
     android:clipChildren="false"
     android:defaultFocusHighlightEnabled="false"
     android:focusable="true"
-    launcher:borderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
+    launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
 
     <com.android.quickstep.views.TaskThumbnailView
         android:id="@+id/snapshot"
diff --git a/quickstep/res/values-bs/strings.xml b/quickstep/res/values-bs/strings.xml
index 0497fe4..c186550 100644
--- a/quickstep/res/values-bs/strings.xml
+++ b/quickstep/res/values-bs/strings.xml
@@ -68,7 +68,7 @@
     <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Idite na početni ekran"</string>
     <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Prevucite s dna ekrana prema gore"</string>
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Sjajno!"</string>
-    <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Prevucite prema gore s donjeg ruba ekrana"</string>
+    <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Vodite računa da prevučete s donjeg ruba ekrana prema gore"</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Pokušajte zadržati prozor duže prije puštanja"</string>
     <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Prevucite ravno nagore, a zatim zastanite"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Naučili ste kako koristiti pokrete. Idite u Postavke da isključite pokrete."</string>
diff --git a/quickstep/res/values-ca/strings.xml b/quickstep/res/values-ca/strings.xml
index 32a338e..d446190 100644
--- a/quickstep/res/values-ca/strings.xml
+++ b/quickstep/res/values-ca/strings.xml
@@ -46,29 +46,29 @@
     <string name="hotseat_prediction_content_description" msgid="4582028296938078419">"Predicció d\'aplicació: <xliff:g id="TITLE">%1$s</xliff:g>"</string>
     <string name="gesture_tutorial_rotation_prompt_title" msgid="7537946781362766964">"Gira el dispositiu"</string>
     <string name="gesture_tutorial_rotation_prompt" msgid="1664493449851960691">"Gira el dispositiu per completar el tutorial de navegació amb gestos"</string>
-    <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Assegura\'t de lliscar des de l\'extrem dret o esquerre de la pantalla"</string>
+    <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Assegura\'t de lliscar des de l\'extrem dret o esquerre de la pantalla."</string>
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Assegura\'t de lliscar des de la vora dreta o esquerra cap al centre de la pantalla i deixar anar"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Has après a lliscar des de la dreta per tornar enrere. Ara, descobreix com pots canviar d\'aplicació."</string>
     <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Has completat el gest per tornar enrere"</string>
-    <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Assegura\'t de no lliscar massa a prop de la part inferior de la pantalla"</string>
+    <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Assegura\'t de no lliscar massa a prop de la part inferior de la pantalla."</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"Per canviar la sensibilitat del gest, ves a Configuració"</string>
     <string name="back_gesture_intro_title" msgid="19551256430224428">"Llisca per anar enrere"</string>
     <string name="back_gesture_intro_subtitle" msgid="7912576483031802797">"Per tornar a la darrera pantalla, llisca des de la vora esquerra o dreta cap al centre de la pantalla."</string>
     <string name="back_gesture_spoken_intro_subtitle" msgid="2162043199263088592">"Per tornar a la pantalla anterior, llisca amb dos dits des de l\'extrem esquerre o dret cap al centre de la pantalla."</string>
     <string name="back_gesture_tutorial_title" msgid="1944737946101059789">"Torna enrere"</string>
-    <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"Llisca des de la vora esquerra o dreta cap al centre de la pantalla"</string>
+    <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"Llisca des de la vora esquerra o dreta cap al centre de la pantalla."</string>
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Assegura\'t de lliscar cap amunt des de la part inferior de la pantalla"</string>
-    <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Assegura\'t de no aturar-te abans de deixar anar"</string>
-    <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Assegura\'t de lliscar directament cap amunt"</string>
+    <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Assegura\'t de no aturar-te abans de deixar anar."</string>
+    <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Assegura\'t de lliscar recte cap amunt."</string>
     <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Has completat el gest per anar a la pantalla d\'inici. Ara, descobreix com pots tornar enrere."</string>
     <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Has completat el gest per anar a la pantalla d\'inici"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"Llisca per anar a la pantalla d\'inici"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Llisca cap amunt des de la part inferior de la pantalla. Aquest gest et porta a la pantalla d\'inici."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Llisca amb dos dits cap amunt des de la part inferior. Aquest gest sempre porta a la pantalla d\'inici."</string>
     <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Ves a la pàgina d\'inici"</string>
-    <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Llisca cap amunt des de la part inferior de la pantalla"</string>
+    <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Llisca cap amunt des de la part inferior de la pantalla."</string>
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Ben fet!"</string>
-    <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Assegura\'t de lliscar cap amunt des de la part inferior de la pantalla"</string>
+    <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Assegura\'t de lliscar cap amunt des de la part inferior de la pantalla."</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Prova de mantenir premuda la finestra durant més temps abans de deixar-la anar"</string>
     <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Assegura\'t de lliscar directament cap amunt i després aturar-te"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Has après a utilitzar els gestos. Per desactivar-los, ves a Configuració."</string>
@@ -77,7 +77,7 @@
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Per canviar entre aplicacions, llisca cap amunt des de la part inferior, mantén premut i deixa anar."</string>
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Per canviar entre apps, llisca amb dos dits cap amunt des de la part inferior, mantén i deixa anar."</string>
     <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"Canvia d\'aplicació"</string>
-    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Llisca cap amunt des de la part inferior de la pantalla, mantén premut i, a continuació, deixa anar"</string>
+    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Llisca cap amunt des de la part inferior de la pantalla, mantén premut i, a continuació, deixa anar."</string>
     <string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"Ben fet!"</string>
     <string name="gesture_tutorial_confirm_title" msgid="6201516182040074092">"Tot a punt"</string>
     <string name="gesture_tutorial_action_button_label" msgid="6249846312991332122">"Fet"</string>
diff --git a/quickstep/res/values-et/strings.xml b/quickstep/res/values-et/strings.xml
index ed17516..1bf92c4 100644
--- a/quickstep/res/values-et/strings.xml
+++ b/quickstep/res/values-et/strings.xml
@@ -50,25 +50,25 @@
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Pühkige ekraani paremast või vasakust servast keskele ja eemaldage sõrm"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Õppisite, kuidas tagasiliikumiseks paremalt pühkida. Nüüd vaadake, kuidas rakenduste vahel vahetada."</string>
     <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Tegite tagasiliikumise liigutuse"</string>
-    <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Veenduge, et te ei pühiks liiga ekraani allosa lähedalt"</string>
+    <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Veenduge, et te ei pühiks liiga ekraani allosa lähedalt."</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"Tagasiliigutuse tundlikkuse muutmiseks avage menüü Seaded"</string>
     <string name="back_gesture_intro_title" msgid="19551256430224428">"Tagasiliikumiseks pühkige"</string>
     <string name="back_gesture_intro_subtitle" msgid="7912576483031802797">"Eelmisele ekraanikuvale naasmiseks pühkige vasakust või paremast servast ekraanikuva keskele."</string>
     <string name="back_gesture_spoken_intro_subtitle" msgid="2162043199263088592">"Eelmisele ekraanikuvale naasmiseks pühkige vasakust või paremast servast kahe sõrmega ekraanikuva keskele."</string>
     <string name="back_gesture_tutorial_title" msgid="1944737946101059789">"Tagasiliikumine"</string>
-    <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"Pühkige ekraani paremast või vasakust servast keskele"</string>
-    <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Pühkige kindlasti ekraani alumisest servast üles"</string>
-    <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Veenduge, et te enne vabastamist liigutust ei peataks"</string>
-    <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Pühkige kindlasti otse üles"</string>
+    <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"Pühkige ekraani paremast või vasakust servast keskele."</string>
+    <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Pühkige kindlasti ekraani alumisest servast üles."</string>
+    <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Veenduge, et te enne vabastamist liigutust ei peataks."</string>
+    <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Pühkige kindlasti otse üles."</string>
     <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Tegite avakuvale minemise liigutuse. Järgmisena vaadake, kuidas minna tagasi."</string>
     <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Tegite avakuvale minemise liigutuse"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"Pühkige avakuvale minemiseks"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Pühkige ekraani alaosast üles. See liigutus viib teid alati tagasi avakuvale."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Pühkige ekraanikuva alumisest servast 2 sõrmega üles. See liigutus viib teid alati tagasi avakuvale."</string>
     <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Avakuvale"</string>
-    <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Pühkige ekraani allosast üles"</string>
+    <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Pühkige ekraani allosast üles."</string>
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Väga hea!"</string>
-    <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Pühkige kindlasti ekraani alumisest servast üles"</string>
+    <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Pühkige kindlasti ekraani alumisest servast üles."</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Hoidke sõrme aknal pisut kauem, enne kui vabastate"</string>
     <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Pühkige kindlasti otse üles, seejärel peatuge"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Õppisite liigutusi kasutama. Liigutuste väljalülitamiseks avage seaded."</string>
@@ -77,7 +77,7 @@
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Rakenduste vahel vahetamiseks pühkige ekraanikuva alaosast üles, hoidke ja seejärel vabastage."</string>
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Rakenduste vahel vahetamiseks pühkige kuva alaosast kahe sõrmega üles, hoidke ja seejärel vabastage."</string>
     <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"Rakenduste vahetamine"</string>
-    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Pühkige ekraani allosast üles, hoidke ja seejärel vabastage"</string>
+    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Pühkige ekraani allosast üles, hoidke ja seejärel vabastage."</string>
     <string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"Hästi tehtud!"</string>
     <string name="gesture_tutorial_confirm_title" msgid="6201516182040074092">"Valmis"</string>
     <string name="gesture_tutorial_action_button_label" msgid="6249846312991332122">"Valmis"</string>
diff --git a/quickstep/res/values-eu/strings.xml b/quickstep/res/values-eu/strings.xml
index b876cf4..c801297 100644
--- a/quickstep/res/values-eu/strings.xml
+++ b/quickstep/res/values-eu/strings.xml
@@ -60,7 +60,7 @@
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Ziurtatu hatza pantailaren beheko ertzetik gora pasatzen duzula"</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Ziurtatu ez duzula mugimendua gelditzen askatu arte"</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Ziurtatu hatza zuzen pasatzen duzula gora"</string>
-    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Ikasi duzu hasierako pantailara joateko keinua. Jarraian, ikasi atzera egiten."</string>
+    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Ikasi duzu hasierako pantailara joateko keinua. Orain, ikasi atzera egiten."</string>
     <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Ikasi duzu hasierako pantailara joateko keinua"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"Pasatu hatza hasierako pantailara joateko"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Pasatu hatza pantailaren behealdetik gora. Keinu horrek hasierako pantailara eramango zaitu beti."</string>
@@ -77,7 +77,7 @@
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Aplikazio batetik bestera joateko, pasatu hatza pantailaren behealdetik gora, eduki pantaila sakatuta eta altxatu hatza."</string>
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Aplikazio batetik bestera joateko, pasatu bi hatz pantailaren behealdetik gora, eduki pantaila sakatuta eta altxatu hatza."</string>
     <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"Aldatu aplikazioa"</string>
-    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Pasatu hatza pantailaren behealdetik gora, eduki sakatuta pantaila eta jaso hatza"</string>
+    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Pasatu hatza pantailaren behealdetik gora, eduki sakatuta une batez, eta jaso hatza"</string>
     <string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"Oso ongi!"</string>
     <string name="gesture_tutorial_confirm_title" msgid="6201516182040074092">"Dena prest"</string>
     <string name="gesture_tutorial_action_button_label" msgid="6249846312991332122">"Eginda"</string>
diff --git a/quickstep/res/values-kk/strings.xml b/quickstep/res/values-kk/strings.xml
index fd8db85..cfdf461 100644
--- a/quickstep/res/values-kk/strings.xml
+++ b/quickstep/res/values-kk/strings.xml
@@ -50,7 +50,7 @@
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Экранның оң немесе сол жиегінен ортасына қарай сырғытып, саусағыңызды жіберіңіз."</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Оңнан солға сырғыту арқылы артқа қайтуды үйрендіңіз. Енді қолданбаларды ауыстыруды үйреніңіз."</string>
     <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Артқа қайту қимылын аяқтадыңыз."</string>
-    <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Саусағыңызбен сырғыту кезінде экранның төменгі жағына тым жақындамаңыз."</string>
+    <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Сырғытқанда саусақты экранның төменгі жағына қатты жақындатпаңыз."</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"Артқа қайту қимылы сезгіштігін параметрлерден өзгертіңіз."</string>
     <string name="back_gesture_intro_title" msgid="19551256430224428">"Артқа қайту үшін сырғытыңыз"</string>
     <string name="back_gesture_intro_subtitle" msgid="7912576483031802797">"Соңғы ашылған экранға оралу үшін экранның сол немесе оң жақ шетінен ортасына қарай сырғытыңыз."</string>
diff --git a/quickstep/res/values-kn/strings.xml b/quickstep/res/values-kn/strings.xml
index 0da707a..acf66a8 100644
--- a/quickstep/res/values-kn/strings.xml
+++ b/quickstep/res/values-kn/strings.xml
@@ -60,8 +60,8 @@
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"ಸ್ಕ್ರೀನ್‌ನ ಕೆಳಗಿನ ಅಂಚಿನಿಂದ ನೀವು ಸ್ವೈಪ್ ಮಾಡುತ್ತಿದ್ದೀರಿ ಎಂದು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಿ"</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"ವಿರಾಮಗೊಳಿಸದೆ ನಿಮ್ಮ ಬೆರಳನ್ನು ಸ್ಕ್ರೀನ್‌ನಿಂದ ಮೇಲೆತ್ತಿ"</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"ನೀವು ನೇರವಾಗಿ ಸ್ವೈಪ್ ಮಾಡುತ್ತಿದ್ದೀರಿ ಎಂದು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಿ"</string>
-    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"ನೀವು ಗೋ ಹೋಮ್ ಗೆಸ್ಚರ್ ಅನ್ನು ಪೂರ್ಣಗೊಳಿಸಿದ್ದೀರಿ. ಮುಂದೆ, ಹಿಂದಕ್ಕೆ ಹೋಗುವುದು ಹೇಗೆ ಎಂದು ತಿಳಿಯಿರಿ."</string>
-    <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"ನೀವು ಗೋ ಹೋಮ್ ಗೆಸ್ಚರ್ ಅನ್ನು ಪೂರ್ಣಗೊಳಿಸಿದ್ದೀರಿ"</string>
+    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"ನೀವು ಹೋಮ್‌ಗೆ ಹೋಗಿ ಗೆಸ್ಚರ್ ಅನ್ನು ಪೂರ್ಣಗೊಳಿಸಿದ್ದೀರಿ. ಮುಂದೆ, ಹಿಂದಕ್ಕೆ ಹೋಗುವುದು ಹೇಗೆ ಎಂದು ತಿಳಿಯಿರಿ."</string>
+    <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"ನೀವು ಹೋಮ್‌ಗೆ ಹೋಗಿ ಗೆಸ್ಚರ್ ಅನ್ನು ಪೂರ್ಣಗೊಳಿಸಿದ್ದೀರಿ"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"ಹೋಮ್ ಸ್ಕ್ರೀನ್‌ಗೆ ಹಿಂತಿರುಗಲು ಸ್ವೈಪ್ ಮಾಡಿ"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"ಸ್ಕ್ರೀನ್‌ನ ಕೆಳಗಿನಿಂದ ಮೇಲೆ ಸ್ವೈಪ್ ಮಾಡಿ. ಈ ಗೆಸ್ಚರ್ ಯಾವಾಗಲೂ ನಿಮ್ಮನ್ನು ಹೋಮ್‌ ಸ್ಕ್ರೀನ್‌ಗೆ ಕರೆದೊಯ್ಯುತ್ತದೆ."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"2 ಬೆರಳುಗಳಿಂದ ಸ್ಕ್ರೀನ್‌ನ ಕೆಳಗಿನಿಂದ ಮೇಲಕ್ಕೆ ಸ್ವೈಪ್ ಮಾಡಿ. ಈ ಗೆಸ್ಚರ್ ಯಾವಾಗಲೂ ನಿಮ್ಮನ್ನು ಹೋಮ್ ಸ್ಕ್ರೀನ್‌ಗೆ ಕರೆದೊಯ್ಯುತ್ತದೆ."</string>
diff --git a/quickstep/res/values-ko/strings.xml b/quickstep/res/values-ko/strings.xml
index 5b23744..9f44a35 100644
--- a/quickstep/res/values-ko/strings.xml
+++ b/quickstep/res/values-ko/strings.xml
@@ -56,7 +56,7 @@
     <string name="back_gesture_intro_subtitle" msgid="7912576483031802797">"이전 화면으로 돌아가려면 왼쪽 또는 오른쪽 가장자리에서 화면 중앙으로 스와이프하세요."</string>
     <string name="back_gesture_spoken_intro_subtitle" msgid="2162043199263088592">"마지막 화면으로 돌아가려면 두 손가락을 사용해 왼쪽 또는 오른쪽 가장자리에서 화면 중앙으로 스와이프하세요"</string>
     <string name="back_gesture_tutorial_title" msgid="1944737946101059789">"뒤로"</string>
-    <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"왼쪽 또는 오른쪽 가장자리에서 화면 중앙으로 스와이프하세요"</string>
+    <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"왼쪽 또는 오른쪽 가장자리에서 화면 중앙으로 스와이프하세요."</string>
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"화면 하단 가장자리에서 위로 스와이프하세요."</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"손가락을 떼기 전에 멈추지 않아야 합니다."</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"위로 곧게 스와이프하세요."</string>
@@ -66,7 +66,7 @@
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"화면 하단에서 위로 스와이프합니다. 이 동작을 사용하면 언제든지 홈 화면으로 이동할 수 있습니다."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"두 손가락을 사용해 화면 하단에서 위로 스와이프하세요. 이 동작을 사용하면 언제든지 홈 화면으로 이동할 수 있습니다"</string>
     <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"홈으로 이동"</string>
-    <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"화면 하단에서 위로 스와이프하세요"</string>
+    <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"화면 하단에서 위로 스와이프하세요."</string>
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"아주 좋습니다"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"화면 하단 가장자리에서 위로 스와이프하세요."</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"창을 더 오래 누르고 있다가 손가락을 떼 보세요."</string>
@@ -77,7 +77,7 @@
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"앱 간에 전환하려면 화면 하단에서 위로 스와이프하고 잠시 멈춘 다음 손가락을 떼세요."</string>
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"앱 간에 전환하려면 두 손가락을 사용해 화면 하단에서 위로 스와이프하고 잠시 멈춘 다음 손가락을 떼세요"</string>
     <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"앱 전환"</string>
-    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"화면 하단에서 위로 스와이프하고 잠시 멈춘 다음 손가락을 떼세요"</string>
+    <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"화면 하단에서 위로 스와이프하고 잠시 멈춘 다음 손가락을 떼세요."</string>
     <string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"잘하셨습니다"</string>
     <string name="gesture_tutorial_confirm_title" msgid="6201516182040074092">"설정 완료"</string>
     <string name="gesture_tutorial_action_button_label" msgid="6249846312991332122">"완료"</string>
diff --git a/quickstep/res/values-mk/strings.xml b/quickstep/res/values-mk/strings.xml
index 9b16ea9..b3ee4ea 100644
--- a/quickstep/res/values-mk/strings.xml
+++ b/quickstep/res/values-mk/strings.xml
@@ -50,7 +50,7 @@
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Повлечете од десниот или левиот раб кон средината на екранот и пуштете"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Научивте како да повлекувате оддесно за враќање назад. Научете и како да се префрлате помеѓу апликациите."</string>
     <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Завршивте со упатството за враќање назад"</string>
-    <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Не повлекувајте преблиску до долниот раб на екранот"</string>
+    <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Не повлекувајте преблиску до дното на екранот"</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"За да ја промените чувствителноста, одете во „Поставки“"</string>
     <string name="back_gesture_intro_title" msgid="19551256430224428">"Повлечете за да се вратите назад"</string>
     <string name="back_gesture_intro_subtitle" msgid="7912576483031802797">"За да се вратите на последниот екран, повлечете од левиот или десниот раб кон средината на екранот."</string>
@@ -60,7 +60,7 @@
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Повлечете нагоре од долниот раб на екранот"</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Не правете пауза пред да пуштите"</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Повлечете право нагоре"</string>
-    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Го научивте движењето за враќање на почетниот екран. Следно, дознајте како да се вратите назад."</string>
+    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Го научивте движењето за отворање на почетниот екран. Научете го и движењето за враќање назад."</string>
     <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Го научивте движењето за враќање на почетниот екран"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"Повлечете за да одите на почетниот екран"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Повлечете нагоре од долниот раб на екранот. Ова движење секогаш ќе ве одведе на почетниот екран."</string>
diff --git a/quickstep/res/values-or/strings.xml b/quickstep/res/values-or/strings.xml
index 52777c2..ac45415 100644
--- a/quickstep/res/values-or/strings.xml
+++ b/quickstep/res/values-or/strings.xml
@@ -60,7 +60,7 @@
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"ଆପଣ ସ୍କ୍ରିନର ତଳ ଧାରରୁ ଉପରକୁ ସ୍ୱାଇପ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ"</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"ଆପଣ ଛାଡ଼ିବା ପୂର୍ବରୁ ବିରତ କରୁନଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ।"</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"ଆପଣ ସିଧା ଉପରକୁ ସ୍ୱାଇପ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ"</string>
-    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"ଆପଣ \'ମୂଳପୃଷ୍ଠାକୁ ଯାଆନ୍ତୁ\' ଜେଶ୍ଚର୍ ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି। ତା\'ପରେ, ପଛକୁ କିପରି ଫେରିବେ ତାହା ଜାଣନ୍ତୁ।"</string>
+    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"ଆପଣ \'ହୋମକୁ ଯାଆନ୍ତୁ\' ଜେଶ୍ଚର ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି। ତା\'ପରେ, ପଛକୁ କିପରି ଫେରିବେ ତାହା ଜାଣନ୍ତୁ।"</string>
     <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"ଆପଣ \'ମୂଳପୃଷ୍ଠାକୁ ଯାଆନ୍ତୁ\' ଜେଶ୍ଚର୍ ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି।"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"ହୋମକୁ ଯିବା ପାଇଁ ସ୍ୱାଇପ କରନ୍ତୁ"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"ଆପଣଙ୍କ ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ କରନ୍ତୁ। ଏହି ଜେଶ୍ଚର ସର୍ବଦା ଆପଣଙ୍କୁ ହୋମ ସ୍କ୍ରିନକୁ ନେଇଥାଏ।"</string>
diff --git a/quickstep/res/values-pt-rPT/strings.xml b/quickstep/res/values-pt-rPT/strings.xml
index 19b7e4b..a82f29a 100644
--- a/quickstep/res/values-pt-rPT/strings.xml
+++ b/quickstep/res/values-pt-rPT/strings.xml
@@ -58,8 +58,8 @@
     <string name="back_gesture_tutorial_title" msgid="1944737946101059789">"Voltar"</string>
     <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"Deslize rapidamente a partir da extremidade esquerda ou direita para o meio do ecrã"</string>
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Deslize rapidamente com o dedo a partir do limite inferior do ecrã"</string>
-    <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Garanta que não faz uma pausa antes de soltar"</string>
-    <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Garanta que desliza rapidamente com o dedo para cima"</string>
+    <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Não faça uma pausa antes de soltar"</string>
+    <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Deslize rapidamente com o dedo para cima"</string>
     <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Concluiu o gesto para aceder ao ecrã principal. A seguir, saiba como retroceder."</string>
     <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Concluiu o gesto para aceder ao ecrã principal"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"Deslize rapidamente com o dedo para aceder ao ecrã principal"</string>
diff --git a/quickstep/res/values-pt/strings.xml b/quickstep/res/values-pt/strings.xml
index 75790ce..8ff0a04 100644
--- a/quickstep/res/values-pt/strings.xml
+++ b/quickstep/res/values-pt/strings.xml
@@ -59,8 +59,8 @@
     <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"Deslize da borda esquerda ou direita até o meio da tela"</string>
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Deslize da borda inferior da tela para cima"</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Não pare antes de soltar"</string>
-    <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Deslize para cima"</string>
-    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Você concluiu o gesto para acessar a tela inicial A seguir, aprenda a voltar."</string>
+    <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Deslize para cima em linha reta"</string>
+    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Você concluiu o gesto para acessar a tela inicial. Agora, aprenda a voltar."</string>
     <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Você concluiu o gesto para acessar a tela inicial"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"Deslizar para voltar à tela inicial"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Deslize de baixo para cima na tela. Esse gesto sempre leva você para a tela inicial."</string>
diff --git a/quickstep/res/values-sk/strings.xml b/quickstep/res/values-sk/strings.xml
index b34cf93..11b091e 100644
--- a/quickstep/res/values-sk/strings.xml
+++ b/quickstep/res/values-sk/strings.xml
@@ -60,8 +60,8 @@
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Musíte potiahnuť nahor z dolného okraja obrazovky"</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Pred uvoľnením nesmiete zastať"</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Musíte potiahnuť priamo nahor"</string>
-    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Dokončili ste gesto na prechod na plochu. V ďalšom kroku sa naučíte, ako sa vrátiť späť."</string>
-    <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Dokončili ste gesto na prechod na plochu"</string>
+    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Dokončili ste gesto prechodu na plochu. Teraz sa naučíte, ako sa vrátiť späť."</string>
+    <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Dokončili ste gesto prechodu na plochu"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"Prechod na plochu potiahnutím"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Potiahnite nahor zdola obrazovky. Týmto gestom sa vždy vrátite na plochu."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Postiahnite dvoma prstami z dolnej časti obrazovky. Týmto gestom sa vždy vrátite na plochu."</string>
diff --git a/quickstep/res/values-th/strings.xml b/quickstep/res/values-th/strings.xml
index 3202588..2b9906e 100644
--- a/quickstep/res/values-th/strings.xml
+++ b/quickstep/res/values-th/strings.xml
@@ -50,16 +50,16 @@
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"ตรวจสอบว่าปัดจากขอบด้านขวาหรือซ้ายไปตรงกลางหน้าจอ แล้วยกนิ้วขึ้น"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"คุณรู้วิธีปัดจากด้านขวาเพื่อย้อนกลับแล้ว ต่อไปดูวิธีสลับแอป"</string>
     <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"คุณทำท่าทางสัมผัสเพื่อย้อนกลับเสร็จแล้ว"</string>
-    <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"ตรวจสอบว่าไม่ได้ปัดใกล้กับด้านล่างของหน้าจอมากเกินไป"</string>
+    <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"ไม่ปัดใกล้กับด้านล่างของหน้าจอมากเกินไป"</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"เปลี่ยนความไวของท่าทางสัมผัสเพื่อย้อนกลับได้ที่การตั้งค่า"</string>
     <string name="back_gesture_intro_title" msgid="19551256430224428">"ปัดเพื่อย้อนกลับ"</string>
     <string name="back_gesture_intro_subtitle" msgid="7912576483031802797">"หากต้องการย้อนกลับไปที่หน้าจอล่าสุด ให้ปัดจากขอบด้านซ้ายหรือขวาไปตรงกลางหน้าจอ"</string>
     <string name="back_gesture_spoken_intro_subtitle" msgid="2162043199263088592">"หากต้องการย้อนกลับไปที่หน้าจอล่าสุด ให้ใช้ 2 นิ้วปัดจากขอบด้านซ้ายหรือขวาไปตรงกลางหน้าจอ"</string>
     <string name="back_gesture_tutorial_title" msgid="1944737946101059789">"ย้อนกลับ"</string>
     <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"ปัดจากขอบด้านซ้ายหรือขวาไปตรงกลางหน้าจอ"</string>
-    <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"ตรวจสอบว่าปัดขึ้นจากขอบด้านล่างของหน้าจอ"</string>
-    <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"ตรวจสอบว่าไม่มีการหยุดชั่วคราวก่อนยกนิ้วขึ้น"</string>
-    <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"ตรวจสอบว่าปัดขึ้นในแนวตรง"</string>
+    <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"ปัดขึ้นจากขอบด้านล่างของหน้าจอ"</string>
+    <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"ไม่ต้องหยุดชั่วคราวก่อนยกนิ้วขึ้น"</string>
+    <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"ปัดขึ้นในแนวตรง"</string>
     <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"คุณทำท่าทางสัมผัสเพื่อไปที่หน้าแรกเสร็จแล้ว ต่อไปดูวิธีย้อนกลับ"</string>
     <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"คุณทำท่าทางสัมผัสเพื่อไปที่หน้าแรกเสร็จแล้ว"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"ปัดเพื่อไปที่หน้าแรก"</string>
@@ -68,7 +68,7 @@
     <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"ไปที่หน้าจอหลัก"</string>
     <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"ปัดขึ้นจากด้านล่างของหน้าจอ"</string>
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"เก่งมาก"</string>
-    <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"ตรวจสอบว่าปัดขึ้นจากขอบด้านล่างของหน้าจอ"</string>
+    <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"ปัดขึ้นจากขอบด้านล่างของหน้าจอ"</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"ลองแตะหน้าต่างค้างไว้นานขึ้นก่อนปล่อยนิ้ว"</string>
     <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"ตรวจสอบว่าปัดขึ้นในแนวตรง แล้วหยุดชั่วคราว"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"คุณรู้วิธีใช้ท่าทางสัมผัสแล้ว หากต้องการปิดท่าทางสัมผัส ให้ไปที่การตั้งค่า"</string>
diff --git a/quickstep/res/values-tr/strings.xml b/quickstep/res/values-tr/strings.xml
index 606eb9b..242ee0c 100644
--- a/quickstep/res/values-tr/strings.xml
+++ b/quickstep/res/values-tr/strings.xml
@@ -55,7 +55,7 @@
     <string name="back_gesture_intro_title" msgid="19551256430224428">"Geri dönmek için kaydırma"</string>
     <string name="back_gesture_intro_subtitle" msgid="7912576483031802797">"Son ekrana geri gitmek için sol veya sağ kenardan ekranın ortasına doğru kaydırın."</string>
     <string name="back_gesture_spoken_intro_subtitle" msgid="2162043199263088592">"Son ekrana geri gitmek için sol veya sağ kenardan ekranın ortasına doğru 2 parmağınızla kaydırın."</string>
-    <string name="back_gesture_tutorial_title" msgid="1944737946101059789">"Geri dönün"</string>
+    <string name="back_gesture_tutorial_title" msgid="1944737946101059789">"Geri dönme"</string>
     <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"Sol veya sağ kenardan ekranın ortasına doğru kaydırın"</string>
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Ekranın alt kenarından yukarı kaydırdığınızdan emin olun"</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Bırakmadan önce parmağınızı duraklatmadığınızdan emin olun"</string>
@@ -76,7 +76,7 @@
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Uygulamalar arasında geçiş yapmak için kaydırma"</string>
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Uygulamalar arasında geçiş yapmak için ekranınızın altından yukarı kaydırıp basılı tutun ve sonra bırakın."</string>
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Uygulamalara geçiş yapmak için ekranın altından 2 parmakla yukarı kaydırıp basılı tutun ve bırakın."</string>
-    <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"Uygulamalar arasında geçiş yapın"</string>
+    <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"Uygulamalar arasında geçiş yapma"</string>
     <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Ekranınızın alt tarafından yukarı doğru kaydırın, tutun ve sonra bırakın"</string>
     <string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"Tebrikler!"</string>
     <string name="gesture_tutorial_confirm_title" msgid="6201516182040074092">"Hepsi bu kadar"</string>
diff --git a/quickstep/res/values-zh-rCN/strings.xml b/quickstep/res/values-zh-rCN/strings.xml
index d51f46a..5db869d 100644
--- a/quickstep/res/values-zh-rCN/strings.xml
+++ b/quickstep/res/values-zh-rCN/strings.xml
@@ -58,7 +58,7 @@
     <string name="back_gesture_tutorial_title" msgid="1944737946101059789">"返回"</string>
     <string name="back_gesture_tutorial_subtitle" msgid="6639993416000920142">"从屏幕左侧或右侧边缘滑动到中间"</string>
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"确保从屏幕底部边缘向上滑动"</string>
-    <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"松开手指前,确保不要停下来"</string>
+    <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"松开手指前,请勿中途停顿"</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"确保笔直向上滑动"</string>
     <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"您完成了“转到主屏幕”手势。接下来了解如何返回。"</string>
     <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"您完成了“转到主屏幕”手势"</string>
@@ -71,7 +71,7 @@
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"确保从屏幕底部边缘向上滑动"</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"尝试按住窗口较长时间,然后再松开手指"</string>
     <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"确保笔直向上滑动,然后停住"</string>
-    <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"您已了解如何使用手势了。如要关闭手势,请转到“设置”。"</string>
+    <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"您已了解如何使用手势了。如要关闭手势,请前往“设置”。"</string>
     <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"您完成了“切换应用”手势"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"滑动即可切换应用"</string>
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"如需在应用之间切换,请从屏幕底部向上滑动,按住,然后松开。"</string>
diff --git a/quickstep/res/values/attrs.xml b/quickstep/res/values/attrs.xml
index fb51919..7288774 100644
--- a/quickstep/res/values/attrs.xml
+++ b/quickstep/res/values/attrs.xml
@@ -26,7 +26,8 @@
      -->
     <declare-styleable name="TaskView">
         <!-- Border color for a keyboard quick switch task views -->
-        <attr name="borderColor" format="color" />
+        <attr name="focusBorderColor" format="color" />
+        <attr name="hoverBorderColor" format="color" />
     </declare-styleable>
 
     <!--
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index e4f6555..b0e91a1 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -38,6 +38,8 @@
     <dimen name="task_thumbnail_icon_size">48dp</dimen>
     <!--  The icon size for the focused task, placed in center of touch target  -->
     <dimen name="task_thumbnail_icon_drawable_size">44dp</dimen>
+    <!--  The border width shown when task is hovered  -->
+    <dimen name="task_hover_border_width">4dp</dimen>
     <!--  The space under the focused task icon  -->
     <dimen name="overview_task_margin">16dp</dimen>
     <!--  The horizontal space between tasks  -->
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 77799e6..dd9fc63 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -230,6 +230,7 @@
     <string name="action_split">Split</string>
     <!-- Label for toast with instructions for split screen selection mode. [CHAR_LIMIT=50] -->
     <string name="toast_split_select_app">Tap another app to use split screen</string>
+    <string name="toast_split_select_cont_desc">Exit split screen selection</string>
     <!-- Label for toast when app selected for split isn't supported. [CHAR_LIMIT=50] -->
     <string name="toast_split_app_unsupported">Choose another app to use split screen</string>
     <!-- Message shown when an action is blocked by a policy enforced by the app or the organization managing the device. [CHAR_LIMIT=NONE] -->
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index 21b7fd5..fc03704 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -298,4 +298,9 @@
     <style name="rotate_prompt_subtitle" parent="TextAppearance.GestureTutorial.Dialog.Subtitle">
         <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
     </style>
+
+    <style name="ArrowTipTaskbarStyle">
+        <item name="arrowTipBackground">?androidprv:attr/materialColorSurfaceContainer</item>
+        <item name="arrowTipTextColor">?androidprv:attr/materialColorOnSurface</item>
+    </style>
 </resources>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 153c1ac..4489eaf 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -158,6 +158,7 @@
 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
 import com.android.wm.shell.startingsurface.IStartingWindowListener;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -477,6 +478,9 @@
         });
     }
 
+    /** Dump debug logs to bug report. */
+    public void dump(@NonNull String prefix, @NonNull PrintWriter printWriter) {}
+
     /**
      * Content is everything on screen except the background and the floating view (if any).
      *
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index 8a11b57..3e1a6ae 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -80,7 +80,7 @@
         setWillNotDraw(false);
 
         mBorderColor = ta.getColor(
-                R.styleable.TaskView_borderColor, DEFAULT_BORDER_COLOR);
+                R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR);
         ta.recycle();
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
index 70999e7..8ab2ffa 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
@@ -45,6 +45,8 @@
     public static final int FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER = 1 << 4;
     // Transient Taskbar is temporarily unstashed (pending a timeout).
     public static final int FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR = 1 << 5;
+    // User has hovered the taskbar.
+    public static final int FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS = 1 << 6;
 
     @IntDef(flag = true, value = {
             FLAG_AUTOHIDE_SUSPEND_FULLSCREEN,
@@ -53,6 +55,7 @@
             FLAG_AUTOHIDE_SUSPEND_EDU_OPEN,
             FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER,
             FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR,
+            FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AutohideSuspendFlag {}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index 64ba5aa..3c7196a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -20,11 +20,13 @@
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SEARCH_ACTION;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
+import android.app.PendingIntent;
 import android.content.ClipData;
 import android.content.ClipDescription;
 import android.content.Intent;
@@ -73,6 +75,7 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.views.BubbleTextHolder;
 import com.android.quickstep.util.LogUtils;
 import com.android.quickstep.util.MultiValueUpdateListener;
 import com.android.systemui.shared.recents.model.Task;
@@ -149,6 +152,9 @@
             View view,
             @Nullable DragPreviewProvider dragPreviewProvider,
             @Nullable Point iconShift) {
+        if (view instanceof BubbleTextHolder) {
+            view = ((BubbleTextHolder) view).getBubbleText();
+        }
         if (!(view instanceof BubbleTextView) || mDisallowLongClick) {
             return false;
         }
@@ -193,13 +199,21 @@
         dragLayerY += dragRect.top;
 
         DragOptions dragOptions = new DragOptions();
-        dragOptions.preDragCondition = null;
-        PopupContainerWithArrow<BaseTaskbarContext> popupContainer =
-                mControllers.taskbarPopupController.showForIcon(btv);
-        if (popupContainer != null) {
-            dragOptions.preDragCondition = popupContainer.createPreDragCondition(false);
-        }
+        // First, see if view is a search result that needs custom pre-drag conditions.
+        dragOptions.preDragCondition =
+                mControllers.taskbarAllAppsController.createPreDragConditionForSearch(btv);
+
         if (dragOptions.preDragCondition == null) {
+            // See if view supports a popup container.
+            PopupContainerWithArrow<BaseTaskbarContext> popupContainer =
+                    mControllers.taskbarPopupController.showForIcon(btv);
+            if (popupContainer != null) {
+                dragOptions.preDragCondition = popupContainer.createPreDragCondition(false);
+            }
+        }
+
+        if (dragOptions.preDragCondition == null) {
+            // Fallback pre-drag condition.
             dragOptions.preDragCondition = new DragOptions.PreDragCondition() {
                 private DragView mDragView;
 
@@ -213,13 +227,8 @@
                     mDragView = dragObject.dragView;
 
                     if (!shouldStartDrag(0)) {
-                        mDragView.setOnAnimationEndCallback(() -> {
-                            // Drag might be cancelled during the DragView animation, so check
-                            // mIsPreDrag again.
-                            if (mIsInPreDrag) {
-                                callOnDragStart();
-                            }
-                        });
+                        mDragView.setOnScaleAnimEndCallback(
+                                TaskbarDragController.this::onPreDragAnimationEnd);
                     }
                 }
 
@@ -230,12 +239,13 @@
             };
         }
 
+        Point dragOffset = dragOptions.preDragCondition.getDragOffset();
         return startDrag(
                 drawable,
                 /* view = */ null,
                 /* originalView = */ btv,
-                dragLayerX,
-                dragLayerY,
+                dragLayerX + dragOffset.x,
+                dragLayerY + dragOffset.y,
                 (View target, DropTarget.DragObject d, boolean success) -> {} /* DragSource */,
                 (ItemInfo) btv.getTag(),
                 dragRect,
@@ -290,6 +300,11 @@
         mDragObject.dragInfo = dragInfo;
         mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy();
 
+        if (mOptions.preDragCondition != null) {
+            dragView.setHasDragOffset(mOptions.preDragCondition.getDragOffset().x != 0
+                    || mOptions.preDragCondition.getDragOffset().y != 0);
+        }
+
         if (dragRegion != null) {
             dragView.setDragRegion(new Rect(dragRegion));
         }
@@ -308,6 +323,14 @@
         return dragView;
     }
 
+    /** Invoked when an animation running as part of pre-drag finishes. */
+    public void onPreDragAnimationEnd() {
+        // Drag might be cancelled during the DragView animation, so check mIsPreDrag again.
+        if (mIsInPreDrag) {
+            callOnDragStart();
+        }
+    }
+
     @Override
     protected void callOnDragStart() {
         super.callOnDragStart();
@@ -383,6 +406,17 @@
                                 item.user));
                 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, item.getIntent().getPackage());
                 intent.putExtra(Intent.EXTRA_SHORTCUT_ID, deepShortcutId);
+            } else if (item.itemType == ITEM_TYPE_SEARCH_ACTION) {
+                // TODO(b/289261756): Buggy behavior when split opposite to an existing search pane.
+                intent.putExtra(
+                        ClipDescription.EXTRA_PENDING_INTENT,
+                        PendingIntent.getActivityAsUser(
+                                mActivity,
+                                /* requestCode= */ 0,
+                                item.getIntent(),
+                                PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
+                                /* options= */ null,
+                                item.user));
             } else {
                 intent.putExtra(ClipDescription.EXTRA_PENDING_INTENT,
                         launcherApps.getMainActivityLaunchIntent(item.getIntent().getComponent(),
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index f6de926..e521154 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.RectF;
+import android.media.permission.SafeCloseable;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.view.KeyEvent;
@@ -31,6 +32,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.app.viewcapture.SettingsAwareViewCapture;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -68,6 +70,7 @@
 
     // Initialized in init.
     private TaskbarDragLayerController.TaskbarDragLayerCallbacks mControllerCallbacks;
+    private SafeCloseable mViewCaptureCloseable;
 
     private float mTaskbarBackgroundOffset;
 
@@ -128,12 +131,14 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         getViewTreeObserver().addOnComputeInternalInsetsListener(mTaskbarInsetsComputer);
+        mViewCaptureCloseable = SettingsAwareViewCapture.getInstance(getContext())
+                .startCapture(getRootView(), ".Taskbar");
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-
+        mViewCaptureCloseable.close();
         onDestroy(true);
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
new file mode 100644
index 0000000..363f915
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static android.view.MotionEvent.ACTION_HOVER_ENTER;
+import static android.view.MotionEvent.ACTION_HOVER_EXIT;
+import static android.view.View.ALPHA;
+import static android.view.View.SCALE_Y;
+import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT;
+
+import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_EXCEPT_ON_BOARD_POPUP;
+import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS;
+import static com.android.launcher3.views.ArrowTipView.TEXT_ALPHA;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.ContextThemeWrapper;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.app.animation.Interpolators;
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.views.ArrowTipView;
+
+/**
+ * Controls showing a tooltip in the taskbar above each icon when it is hovered.
+ */
+public class TaskbarHoverToolTipController implements View.OnHoverListener {
+
+    private static final int HOVER_TOOL_TIP_REVEAL_START_DELAY = 400;
+    private static final int HOVER_TOOL_TIP_REVEAL_DURATION = 300;
+    private static final int HOVER_TOOL_TIP_EXIT_DURATION = 150;
+
+    private final Handler mHoverToolTipHandler = new Handler(Looper.getMainLooper());
+    private final Runnable mRevealHoverToolTipRunnable = this::revealHoverToolTip;
+    private final Runnable mHideHoverToolTipRunnable = this::hideHoverToolTip;
+
+    private final TaskbarActivityContext mActivity;
+    private final TaskbarView mTaskbarView;
+    private final View mHoverView;
+    private final ArrowTipView mHoverToolTipView;
+    private final String mToolTipText;
+
+    public TaskbarHoverToolTipController(TaskbarActivityContext activity, TaskbarView taskbarView,
+            View hoverView) {
+        mActivity = activity;
+        mTaskbarView = taskbarView;
+        mHoverView = hoverView;
+
+        if (mHoverView instanceof BubbleTextView) {
+            mToolTipText = ((BubbleTextView) mHoverView).getText().toString();
+        } else if (mHoverView instanceof FolderIcon
+                && ((FolderIcon) mHoverView).mInfo.title != null) {
+            mToolTipText = ((FolderIcon) mHoverView).mInfo.title.toString();
+        } else {
+            mToolTipText = null;
+        }
+
+        ContextThemeWrapper arrowContextWrapper = new ContextThemeWrapper(mActivity,
+                R.style.ArrowTipTaskbarStyle);
+        mHoverToolTipView = new ArrowTipView(arrowContextWrapper, /* isPointingUp = */ false,
+                R.layout.arrow_toast);
+
+        AnimatorSet hoverCloseAnimator = new AnimatorSet();
+        ObjectAnimator textCloseAnimator = ObjectAnimator.ofInt(mHoverToolTipView, TEXT_ALPHA, 0);
+        textCloseAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0, 0.33f));
+        ObjectAnimator alphaCloseAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, ALPHA, 0);
+        alphaCloseAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0.33f, 0.66f));
+        ObjectAnimator scaleCloseAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, SCALE_Y, 0);
+        scaleCloseAnimator.setInterpolator(Interpolators.STANDARD);
+        hoverCloseAnimator.playTogether(
+                textCloseAnimator,
+                alphaCloseAnimator,
+                scaleCloseAnimator);
+        hoverCloseAnimator.setStartDelay(0);
+        hoverCloseAnimator.setDuration(HOVER_TOOL_TIP_EXIT_DURATION);
+        mHoverToolTipView.setCustomCloseAnimation(hoverCloseAnimator);
+
+        AnimatorSet hoverOpenAnimator = new AnimatorSet();
+        ObjectAnimator textOpenAnimator = ObjectAnimator.ofInt(mHoverToolTipView, TEXT_ALPHA, 255);
+        textOpenAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0.33f, 1f));
+        ObjectAnimator scaleOpenAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, SCALE_Y, 1f);
+        scaleOpenAnimator.setInterpolator(Interpolators.EMPHASIZED);
+        ObjectAnimator alphaOpenAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, ALPHA, 1f);
+        alphaOpenAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0.1f, 0.33f));
+        hoverOpenAnimator.playTogether(
+                scaleOpenAnimator,
+                textOpenAnimator,
+                alphaOpenAnimator);
+        hoverOpenAnimator.setStartDelay(HOVER_TOOL_TIP_REVEAL_START_DELAY);
+        hoverOpenAnimator.setDuration(HOVER_TOOL_TIP_REVEAL_DURATION);
+        mHoverToolTipView.setCustomOpenAnimation(hoverOpenAnimator);
+
+        mHoverToolTipView.addOnLayoutChangeListener(
+                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                    mHoverToolTipView.setPivotY(bottom);
+                    mHoverToolTipView.setY(mTaskbarView.getTop() - (bottom - top));
+                });
+        mHoverToolTipView.setScaleY(0f);
+        mHoverToolTipView.setAlpha(0f);
+    }
+
+    @Override
+    public boolean onHover(View v, MotionEvent event) {
+        boolean isAnyOtherFloatingViewOpen =
+                AbstractFloatingView.hasOpenView(mActivity, TYPE_ALL_EXCEPT_ON_BOARD_POPUP);
+        if (isAnyOtherFloatingViewOpen) {
+            mHoverToolTipHandler.removeCallbacksAndMessages(null);
+        }
+        // If hover leaves a taskbar icon animate the tooltip closed.
+        if (event.getAction() == ACTION_HOVER_EXIT) {
+            startHideHoverToolTip();
+            mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, false);
+            return true;
+        } else if (!isAnyOtherFloatingViewOpen && event.getAction() == ACTION_HOVER_ENTER) {
+            // If hovering above a taskbar icon starts, animate the tooltip open. Do not
+            // reveal if any floating views such as folders or edu pop-ups are open.
+            startRevealHoverToolTip();
+            mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, true);
+            return true;
+        }
+        return false;
+    }
+
+    private void startRevealHoverToolTip() {
+        mActivity.setTaskbarWindowFullscreen(true);
+        mHoverToolTipHandler.postDelayed(mRevealHoverToolTipRunnable,
+                HOVER_TOOL_TIP_REVEAL_START_DELAY);
+    }
+
+    private void revealHoverToolTip() {
+        if (mHoverView == null || mToolTipText == null) {
+            return;
+        }
+        if (mHoverView instanceof FolderIcon && !((FolderIcon) mHoverView).getIconVisible()) {
+            return;
+        }
+        Rect iconViewBounds = Utilities.getViewBounds(mHoverView);
+        mHoverToolTipView.showAtLocation(mToolTipText, iconViewBounds.centerX(),
+                mTaskbarView.getTop(), /* shouldAutoClose= */ false);
+    }
+
+    private void startHideHoverToolTip() {
+        mHoverToolTipHandler.removeCallbacks(mRevealHoverToolTipRunnable);
+        int accessibilityHideTimeout = AccessibilityManagerCompat.getRecommendedTimeoutMillis(
+                mActivity, /* originalTimeout= */ 0, FLAG_CONTENT_TEXT);
+        mHoverToolTipHandler.postDelayed(mHideHoverToolTipRunnable, accessibilityHideTimeout);
+    }
+
+    private void hideHoverToolTip() {
+        mHoverToolTipView.close(/* animate = */ true);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 4ad3d5a..296e0db 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -249,7 +249,6 @@
 
         mIconAlignment.finishAnimation();
 
-        Log.d("b/260135164", "onDestroy - updateIconAlphaForHome(1)");
         mLauncher.getHotseat().setIconsAlpha(1f);
         mLauncher.getStateManager().removeStateListener(mStateListener);
 
@@ -645,8 +644,6 @@
                 public void onAnimationEnd(Animator animation) {
                     if (isInStashedState && committed) {
                         // Reset hotseat alpha to default
-                        Log.d("b/260135164",
-                                "playStateTransitionAnim#onAnimationEnd - setIconsAlpha(1)");
                         mLauncher.getHotseat().setIconsAlpha(1);
                     }
                 }
@@ -714,8 +711,6 @@
 
     private void updateIconAlphaForHome(float alpha) {
         if (mControllers.taskbarActivityContext.isDestroyed()) {
-            Log.e("b/260135164", "updateIconAlphaForHome is called after Taskbar is destroyed",
-                    new Exception());
             return;
         }
         mIconAlphaForHome.setValue(alpha);
@@ -726,9 +721,6 @@
          * Hide Launcher Hotseat icons when Taskbar icons have opacity. Both icon sets
          * should not be visible at the same time.
          */
-        Log.d("b/260135164",
-                "updateIconAlphaForHome - setIconsAlpha(" + (hotseatVisible ? 1 : 0)
-                        + "), isTaskbarPresent: " + mLauncher.getDeviceProfile().isTaskbarPresent);
         mLauncher.getHotseat().setIconsAlpha(hotseatVisible ? 1 : 0);
         if (mIsQsbInline) {
             mLauncher.getHotseat().setQsbAlpha(hotseatVisible ? 1 : 0);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index bf3b932..074cbe1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -18,6 +18,7 @@
 import static android.content.pm.PackageManager.FEATURE_PC;
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
 
 import android.content.Context;
@@ -319,6 +320,9 @@
                 }
             }
             setClickAndLongClickListenersForIcon(hotseatView);
+            if (ENABLE_CURSOR_HOVER_STATES.get()) {
+                setHoverListenerForIcon(hotseatView);
+            }
             nextViewIndex++;
         }
         // Remove remaining views
@@ -366,6 +370,13 @@
         icon.setOnLongClickListener(mIconLongClickListener);
     }
 
+    /**
+     * Sets OnHoverListener for the given view.
+     */
+    private void setHoverListenerForIcon(View icon) {
+        icon.setOnHoverListener(mControllerCallbacks.getIconOnHoverListener(icon));
+    }
+
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         int count = getChildCount();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 1b45404..3d22e78 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -690,6 +690,11 @@
                     .updateAndAnimateIsManuallyStashedInApp(true);
         }
 
+        /** Gets the hover listener for the provided icon view. */
+        public View.OnHoverListener getIconOnHoverListener(View icon) {
+            return new TaskbarHoverToolTipController(mActivity, mTaskbarView, icon);
+        }
+
         /**
          * Get the first chance to handle TaskbarView#onTouchEvent, and return whether we want to
          * consume the touch so TaskbarView treats it as an ACTION_CANCEL.
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
index cf0b36a..544f9bf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
@@ -15,11 +15,14 @@
  */
 package com.android.launcher3.taskbar.allapps;
 
+import android.view.View;
+
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
 import com.android.launcher3.appprediction.PredictionRowView;
+import com.android.launcher3.dragndrop.DragOptions.PreDragCondition;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.taskbar.TaskbarControllers;
@@ -161,7 +164,7 @@
             cleanUpOverlay();
         });
         TaskbarAllAppsViewController viewController = new TaskbarAllAppsViewController(
-                mOverlayContext, mSlideInView, mControllers);
+                mOverlayContext, mSlideInView, mControllers, mSearchSessionController);
 
         viewController.show(animate);
         mAppsView = mOverlayContext.getAppsView();
@@ -208,4 +211,12 @@
         // Allow null-pointer since this should only be null if the apps view is not showing.
         return mAppsView.getActiveRecyclerView().computeVerticalScrollOffset();
     }
+
+    /** @see TaskbarSearchSessionController#createPreDragConditionForSearch(View) */
+    @Nullable
+    public PreDragCondition createPreDragConditionForSearch(View view) {
+        return mSearchSessionController != null
+                ? mSearchSessionController.createPreDragConditionForSearch(view)
+                : null;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
index 84cc002..4f75ef5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
@@ -17,6 +17,8 @@
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.graphics.Canvas;
@@ -63,14 +65,23 @@
         }
         mIsOpen = true;
         attachToContainer();
+        mAllAppsCallbacks.onAllAppsTransitionStart(true);
 
         if (animate) {
             mOpenCloseAnimator.setValues(
                     PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
             mOpenCloseAnimator.setInterpolator(EMPHASIZED);
+            mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mOpenCloseAnimator.removeListener(this);
+                    mAllAppsCallbacks.onAllAppsTransitionEnd(true);
+                }
+            });
             mOpenCloseAnimator.setDuration(mAllAppsCallbacks.getOpenDuration()).start();
         } else {
             mTranslationShift = TRANSLATION_SHIFT_OPENED;
+            mAllAppsCallbacks.onAllAppsTransitionEnd(true);
         }
     }
 
@@ -81,10 +92,19 @@
 
     @Override
     protected void handleClose(boolean animate) {
+        if (mIsOpen) {
+            mAllAppsCallbacks.onAllAppsTransitionStart(false);
+        }
         handleClose(animate, mAllAppsCallbacks.getCloseDuration());
     }
 
     @Override
+    protected void onCloseComplete() {
+        mAllAppsCallbacks.onAllAppsTransitionEnd(false);
+        super.onCloseComplete();
+    }
+
+    @Override
     protected Interpolator getIdleInterpolator() {
         return EMPHASIZED;
     }
@@ -194,4 +214,11 @@
     protected boolean isEventOverContent(MotionEvent ev) {
         return getPopupContainer().isEventOverView(mAppsView.getVisibleContainerView(), ev);
     }
+
+    @Override
+    public void onBackInvoked() {
+        if (!mAllAppsCallbacks.handleSearchBackInvoked()) {
+            super.onBackInvoked();
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
index a851734..f43169b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT;
 
 import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.allapps.AllAppsTransitionListener;
 import com.android.launcher3.appprediction.AppsDividerView;
 import com.android.launcher3.taskbar.NavbarButtonsViewController;
 import com.android.launcher3.taskbar.TaskbarControllers;
@@ -43,7 +44,8 @@
     TaskbarAllAppsViewController(
             TaskbarOverlayContext context,
             TaskbarAllAppsSlideInView slideInView,
-            TaskbarControllers taskbarControllers) {
+            TaskbarControllers taskbarControllers,
+            TaskbarSearchSessionController searchSessionController) {
 
         mContext = context;
         mSlideInView = slideInView;
@@ -52,7 +54,7 @@
         mNavbarButtonsViewController = taskbarControllers.navbarButtonsViewController;
         mOverlayController = taskbarControllers.taskbarOverlayController;
 
-        mSlideInView.init(new TaskbarAllAppsCallbacks());
+        mSlideInView.init(new TaskbarAllAppsCallbacks(searchSessionController));
         setUpAppDivider();
         setUpTaskbarStashing();
     }
@@ -94,7 +96,13 @@
         });
     }
 
-    class TaskbarAllAppsCallbacks {
+    class TaskbarAllAppsCallbacks implements AllAppsTransitionListener {
+        private final TaskbarSearchSessionController mSearchSessionController;
+
+        private TaskbarAllAppsCallbacks(TaskbarSearchSessionController searchSessionController) {
+            mSearchSessionController = searchSessionController;
+        }
+
         int getOpenDuration() {
             return mOverlayController.getOpenDuration();
         }
@@ -102,5 +110,20 @@
         int getCloseDuration() {
             return mOverlayController.getCloseDuration();
         }
+
+        @Override
+        public void onAllAppsTransitionStart(boolean toAllApps) {
+            mSearchSessionController.onAllAppsTransitionStart(toAllApps);
+        }
+
+        @Override
+        public void onAllAppsTransitionEnd(boolean toAllApps) {
+            mSearchSessionController.onAllAppsTransitionEnd(toAllApps);
+        }
+
+        /** Invoked on back press, returning {@code true} if the search session handled it. */
+        boolean handleSearchBackInvoked() {
+            return mSearchSessionController.handleBackInvoked();
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt
index 324c1a2..c26977f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt
@@ -17,26 +17,38 @@
 package com.android.launcher3.taskbar.allapps
 
 import android.content.Context
+import android.view.View
 import com.android.launcher3.R
+import com.android.launcher3.allapps.AllAppsTransitionListener
 import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.dragndrop.DragOptions.PreDragCondition
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.util.ResourceBasedOverride
 import com.android.launcher3.util.ResourceBasedOverride.Overrides
 
 /** Stub for managing the Taskbar search session. */
-open class TaskbarSearchSessionController : ResourceBasedOverride {
+open class TaskbarSearchSessionController : ResourceBasedOverride, AllAppsTransitionListener {
 
     /** Start the search session lifecycle. */
-    open fun startLifecycle() {}
+    open fun startLifecycle() = Unit
 
     /** Destroy the search session. */
-    open fun onDestroy() {}
+    open fun onDestroy() = Unit
 
     /** Updates the predicted items shown in the zero-state. */
-    open fun setZeroStatePredictedItems(items: List<ItemInfo>) {}
+    open fun setZeroStatePredictedItems(items: List<ItemInfo>) = Unit
 
     /** Updates the search suggestions shown in the zero-state. */
-    open fun setZeroStateSearchSuggestions(items: List<ItemInfo>) {}
+    open fun setZeroStateSearchSuggestions(items: List<ItemInfo>) = Unit
+
+    override fun onAllAppsTransitionStart(toAllApps: Boolean) = Unit
+
+    override fun onAllAppsTransitionEnd(toAllApps: Boolean) = Unit
+
+    /** Creates a [PreDragCondition] for [view], if it is a search result that requires one. */
+    open fun createPreDragConditionForSearch(view: View): PreDragCondition? = null
+
+    open fun handleBackInvoked(): Boolean = false
 
     companion object {
         @JvmStatic
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 6818db6..012a362 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -277,6 +277,7 @@
     private void applyViewChanges(BubbleBarViewUpdate update) {
         final boolean isCollapsed = (update.expandedChanged && !update.expanded)
                 || (!update.expandedChanged && !mBubbleBarViewController.isExpanded());
+        BubbleBarItem previouslySelectedBubble = mSelectedBubble;
         BubbleBarBubble bubbleToSelect = null;
         if (!update.removedBubbles.isEmpty()) {
             for (int i = 0; i < update.removedBubbles.size(); i++) {
@@ -321,6 +322,11 @@
         mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty());
         mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty());
 
+        if (mBubbles.isEmpty()) {
+            // all bubbles were removed. clear the selected bubble
+            mSelectedBubble = null;
+        }
+
         if (update.updatedBubble != null) {
             // Updates mean the dot state may have changed; any other changes were updated in
             // the populateBubble step.
@@ -357,6 +363,11 @@
         if (bubbleToSelect != null) {
             setSelectedBubble(bubbleToSelect);
         }
+
+        if (previouslySelectedBubble == null) {
+            mBubbleStashController.animateToInitialState(update.expanded);
+        }
+
         if (update.expandedChanged) {
             if (update.expanded != mBubbleBarViewController.isExpanded()) {
                 mBubbleBarViewController.setExpandedFromSysui(update.expanded);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index eec334a..a8e6849 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -306,7 +306,10 @@
         if (!isExpanded()) {
             for (int i = 0; i < viewOrder.size(); i++) {
                 View child = viewOrder.get(i);
-                if (child != null) {
+                // this child view may have already been removed so verify that it still exists
+                // before reordering it, otherwise it will be re-added.
+                int indexOfChild = indexOfChild(child);
+                if (child != null && indexOfChild >= 0) {
                     removeViewInLayout(child);
                     addViewInLayout(child, i, child.getLayoutParams());
                 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 8e7fda8..f5e2ddc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -206,10 +206,12 @@
 
     // TODO: (b/273592694) animate it
     private void updateVisibilityForStateChange() {
-        if (!mHiddenForSysui && !mBubbleStashController.isStashed() && !mHiddenForNoBubbles) {
+        if (!mHiddenForSysui && !mHiddenForNoBubbles) {
             mBarView.setVisibility(VISIBLE);
         } else {
             mBarView.setVisibility(INVISIBLE);
+            mBarView.setAlpha(0);
+            mBarView.setExpanded(false);
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 8af4ff9..a267211 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -71,6 +71,7 @@
     private int mUnstashedHeight;
     private boolean mBubblesShowingOnHome;
     private boolean mBubblesShowingOnOverview;
+    private boolean mIsSysuiLocked;
 
     @Nullable
     private AnimatorSet mAnimator;
@@ -95,14 +96,6 @@
 
         mStashedHeight = mHandleViewController.getStashedHeight();
         mUnstashedHeight = mHandleViewController.getUnstashedHeight();
-
-        bubbleControllers.runAfterInit(() -> {
-            if (mTaskbarStashController.isStashed()) {
-                stashBubbleBar();
-            } else {
-                showBubbleBar(false /* expandBubbles */);
-            }
-        });
     }
 
     /**
@@ -120,6 +113,40 @@
     }
 
     /**
+     * Animates the bubble bar and handle to their initial state, transitioning from the state where
+     * both views are invisible. Called when the first bubble is added or when the device is
+     * unlocked.
+     *
+     * <p>Normally either the bubble bar or the handle is visible,
+     * and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to transition
+     * between these two states. But the transition from the state where both the bar and handle
+     * are invisible is slightly different.
+     *
+     * <p>The initial state will depend on the current state of the device, i.e. overview, home etc
+     * and whether bubbles are requested to be expanded.
+     */
+    public void animateToInitialState(boolean expanding) {
+        AnimatorSet animatorSet = new AnimatorSet();
+        if (expanding || mBubblesShowingOnHome || mBubblesShowingOnOverview) {
+            mIsStashed = false;
+            animatorSet.playTogether(mIconScaleForStash.animateToValue(1),
+                    mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()),
+                    mIconAlphaForStash.animateToValue(1));
+        } else {
+            mIsStashed = true;
+            animatorSet.playTogether(mBubbleStashedHandleAlpha.animateToValue(1));
+        }
+
+        animatorSet.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                onIsStashedChanged();
+            }
+        });
+        animatorSet.setDuration(BAR_STASH_DURATION).start();
+    }
+
+    /**
      * Called when launcher enters or exits the home page. Bubbles are unstashed on home.
      */
     public void setBubblesShowingOnHome(boolean onHome) {
@@ -172,12 +199,11 @@
 
     /** Called when sysui locked state changes, when locked, bubble bar is stashed. */
     public void onSysuiLockedStateChange(boolean isSysuiLocked) {
-        if (isSysuiLocked) {
-            // TODO: should the normal path flip mBubblesOnHome / check if this is needed
-            // If we're locked, we're no longer showing on home.
-            mBubblesShowingOnHome = false;
-            mBubblesShowingOnOverview = false;
-            stashBubbleBar();
+        if (isSysuiLocked != mIsSysuiLocked) {
+            mIsSysuiLocked = isSysuiLocked;
+            if (!mIsSysuiLocked) {
+                animateToInitialState(false /* expanding */);
+            }
         }
     }
 
@@ -256,11 +282,7 @@
             firstHalfDurationScale = 0.5f;
             secondHalfDurationScale = 0.75f;
 
-            // If we're on home, adjust the translation so the bubble bar aligns with hotseat.
-            // Otherwise we're either showing in an app or in overview. In either case adjust it so
-            // the bubble bar aligns with the taskbar.
-            final float translationY = mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat()
-                    : getBubbleBarTranslationYForTaskbar();
+            final float translationY = getBubbleBarTranslationY();
 
             fullLengthAnimatorSet.playTogether(
                     mIconScaleForStash.animateToValue(1),
@@ -317,6 +339,9 @@
     }
 
     float getBubbleBarTranslationY() {
+        // If we're on home, adjust the translation so the bubble bar aligns with hotseat.
+        // Otherwise we're either showing in an app or in overview. In either case adjust it so
+        // the bubble bar aligns with the taskbar.
         return mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat()
                 : getBubbleBarTranslationYForTaskbar();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
index 26756d4..4c197f6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
@@ -52,6 +52,7 @@
     private BubbleStashController mBubbleStashController;
     private RegionSamplingHelper mRegionSamplingHelper;
     private int mBarSize;
+    private int mStashedTaskbarHeight;
     private int mStashedHandleWidth;
     private int mStashedHandleHeight;
 
@@ -92,7 +93,7 @@
 
         mTaskbarStashedHandleAlpha.get(0).setValue(0);
 
-        final int stashedTaskbarHeight = resources.getDimensionPixelSize(
+        mStashedTaskbarHeight = resources.getDimensionPixelSize(
                 R.dimen.bubblebar_stashed_size);
         mStashedHandleView.setOutlineProvider(new ViewOutlineProvider() {
             @Override
@@ -115,22 +116,25 @@
                     }
                 }, Executors.UI_HELPER_EXECUTOR);
 
-        mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) -> {
-            // As more bubbles get added, the icon bounds become larger. To ensure a consistent
-            // handle bar position, we pin it to the edge of the screen.
-            Rect bubblebarRect = mBarViewController.getBubbleBarBounds();
-            final int stashedCenterY = view.getHeight() - stashedTaskbarHeight / 2;
+        mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) ->
+                updateBounds());
+    }
 
-            mStashedHandleBounds.set(
-                    bubblebarRect.right - mStashedHandleWidth,
-                    stashedCenterY - mStashedHandleHeight / 2,
-                    bubblebarRect.right,
-                    stashedCenterY + mStashedHandleHeight / 2);
-            mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
+    private void updateBounds() {
+        // As more bubbles get added, the icon bounds become larger. To ensure a consistent
+        // handle bar position, we pin it to the edge of the screen.
+        Rect bubblebarRect = mBarViewController.getBubbleBarBounds();
+        final int stashedCenterY = mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2;
 
-            view.setPivotX(view.getWidth());
-            view.setPivotY(view.getHeight() - stashedTaskbarHeight / 2f);
-        });
+        mStashedHandleBounds.set(
+                bubblebarRect.right - mStashedHandleWidth,
+                stashedCenterY - mStashedHandleHeight / 2,
+                bubblebarRect.right,
+                stashedCenterY + mStashedHandleHeight / 2);
+        mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
+
+        mStashedHandleView.setPivotX(mStashedHandleView.getWidth());
+        mStashedHandleView.setPivotY(mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2f);
     }
 
     public void onDestroy() {
@@ -188,6 +192,7 @@
             mStashedHandleView.setVisibility(VISIBLE);
         } else {
             mStashedHandleView.setVisibility(INVISIBLE);
+            mStashedHandleView.setAlpha(0);
         }
         updateRegionSampling();
     }
@@ -235,6 +240,9 @@
      */
     public Animator createRevealAnimToIsStashed(boolean isStashed) {
         Rect bubbleBarBounds = new Rect(mBarViewController.getBubbleBarBounds());
+        // the bubble bar may have been invisible when the bounds were previously calculated,
+        // update them again to ensure they're correct.
+        updateBounds();
 
         // Account for the full visual height of the bubble bar
         int heightDiff = (mBarSize - bubbleBarBounds.height()) / 2;
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
index add7279..ff00560 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
@@ -21,6 +21,7 @@
 
 import android.content.Context;
 import android.graphics.Insets;
+import android.media.permission.SafeCloseable;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
@@ -29,6 +30,7 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.app.viewcapture.SettingsAwareViewCapture;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -44,6 +46,7 @@
         BaseDragLayer<TaskbarOverlayContext> implements
         ViewTreeObserver.OnComputeInternalInsetsListener {
 
+    private SafeCloseable mViewCaptureCloseable;
     private final List<OnClickListener> mOnClickListeners = new CopyOnWriteArrayList<>();
     private final TouchController mClickListenerTouchController = new TouchController() {
         @Override
@@ -77,12 +80,15 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+        mViewCaptureCloseable = SettingsAwareViewCapture.getInstance(getContext())
+                .startCapture(getRootView(), ".TaskbarOverlay");
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+        mViewCaptureCloseable.close();
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index fbe0a8f..ffd22b8 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -203,6 +203,8 @@
 
     public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false;
 
+    protected static final String RING_APPEAR_ANIMATION_PREFIX = "RingAppearAnimation\t";
+
     private FixedContainerItems mAllAppsPredictions;
     private HotseatPredictionController mHotseatPredictionController;
     private DepthController mDepthController;
@@ -544,17 +546,9 @@
 
         ArrayList<TouchController> list = new ArrayList<>();
         list.add(getDragController());
-        Consumer<AnimatorSet> splitAnimator = animatorSet -> {
-            AnimatorSet anim = mSplitSelectStateController.getSplitAnimationController()
-                    .createPlaceholderDismissAnim(QuickstepLauncher.this);
-            anim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    mSplitSelectStateController.resetState();
-                }
-            });
-            animatorSet.play(anim);
-        };
+        Consumer<AnimatorSet> splitAnimator = animatorSet ->
+                animatorSet.play(mSplitSelectStateController.getSplitAnimationController()
+                        .createPlaceholderDismissAnim(this));
         switch (mode) {
             case NO_BUTTON:
                 list.add(new NoButtonQuickSwitchTouchController(this));
@@ -673,6 +667,8 @@
                 mSplitSelectStateController.resetState();
             }
         });
+        anim.add(mSplitSelectStateController.getSplitAnimationController()
+                .getShowSplitInstructionsAnim(this).buildAnim());
         anim.buildAnim().start();
     }
 
@@ -1369,5 +1365,8 @@
         if (recentsView != null) {
             recentsView.getSplitSelectController().dump(prefix, writer);
         }
+        if (mAppTransitionManager != null) {
+            mAppTransitionManager.dump(prefix + "\t" + RING_APPEAR_ANIMATION_PREFIX, writer);
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 25909ac..ff757b1 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -134,6 +134,7 @@
 import com.android.quickstep.views.DesktopTaskView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
+import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -150,6 +151,7 @@
 import java.util.HashMap;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.OptionalInt;
 import java.util.function.Consumer;
 
 /**
@@ -656,11 +658,12 @@
 
     protected void notifyGestureAnimationStartToRecents() {
         Task[] runningTasks;
+        TopTaskTracker.CachedTaskInfo cachedTaskInfo = mGestureState.getRunningTask();
         if (mIsSwipeForSplit) {
             int[] splitTaskIds = TopTaskTracker.INSTANCE.get(mContext).getRunningSplitTaskIds();
-            runningTasks = mGestureState.getRunningTask().getPlaceholderTasks(splitTaskIds);
+            runningTasks = cachedTaskInfo.getPlaceholderTasks(splitTaskIds);
         } else {
-            runningTasks = mGestureState.getRunningTask().getPlaceholderTasks();
+            runningTasks = cachedTaskInfo.getPlaceholderTasks();
         }
 
         // Safeguard against any null tasks being sent to recents view, happens when quickswitching
@@ -733,8 +736,11 @@
                 || mRecentsView == null) {
             return;
         }
+        // looking at single target is fine here since either app of a split pair would
+        // have their "isInRecents" field set? (that's what this is used for below)
         RemoteAnimationTarget runningTaskTarget = mRecentsAnimationTargets != null
-                ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId())
+                ? mRecentsAnimationTargets
+                .findTask(mGestureState.getTopRunningTaskId())
                 : null;
         final boolean recentsAttachedToAppWindow;
         if (mIsInAllAppsRegion) {
@@ -1190,7 +1196,7 @@
             return false;
         }
         boolean hasStartedTaskBefore = Arrays.stream(appearedTaskTarget).anyMatch(
-                targetCompat -> targetCompat.taskId == mGestureState.getLastStartedTaskId());
+                mGestureState.mLastStartedTaskIdPredicate);
         if (mStateCallback.hasStates(STATE_START_NEW_TASK) && hasStartedTaskBefore) {
             reset();
             return true;
@@ -1456,9 +1462,12 @@
         @Override
         public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
                 boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
-            if (task.taskId == mGestureState.getRunningTaskId()
-                    && task.configuration.windowConfiguration.getActivityType()
-                    != ACTIVITY_TYPE_HOME) {
+            boolean taskRunningAndNotHome = Arrays.stream(mGestureState
+                            .getRunningTaskIds(true /*getMultipleTasks*/))
+                    .anyMatch(taskId -> task.taskId == taskId
+                            && task.configuration.windowConfiguration.getActivityType()
+                            != ACTIVITY_TYPE_HOME);
+            if (taskRunningAndNotHome) {
                 // Since this is an edge case, just cancel and relaunch with default activity
                 // options (since we don't know if there's an associated app icon to launch from)
                 endRunningWindowAnim(true /* cancel */);
@@ -1500,8 +1509,12 @@
 
         if (mGestureState.getEndTarget() == HOME) {
             getOrientationHandler().adjustFloatingIconStartVelocity(velocityPxPerMs);
+            // Take first task ID, if there are multiple we don't have any special home
+            // animation so doesn't matter for splitscreen.. though the "allowEnterPip" might change
+            // depending on which task it is..
             final RemoteAnimationTarget runningTaskTarget = mRecentsAnimationTargets != null
-                    ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId())
+                    ? mRecentsAnimationTargets
+                    .findTask(mGestureState.getTopRunningTaskId())
                     : null;
             final ArrayList<IBinder> cookies = runningTaskTarget != null
                     ? runningTaskTarget.taskInfo.launchCookies
@@ -1530,7 +1543,8 @@
 
                 // grab a screenshot before the PipContentOverlay gets parented on top of the task
                 UI_HELPER_EXECUTOR.execute(() -> {
-                    final int taskId = mGestureState.getRunningTaskId();
+                    // Directly use top task, split to pip handled on shell side
+                    final int taskId = mGestureState.getTopRunningTaskId();
                     mTaskSnapshotCache.put(taskId,
                             mRecentsAnimationController.screenshotTask(taskId));
                 });
@@ -1994,13 +2008,10 @@
             // If there are no targets, then we don't need to capture anything
             mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
         } else {
-            final int runningTaskId = mGestureState.getRunningTaskId();
             boolean finishTransitionPosted = false;
             // If we already have cached screenshot(s) from running tasks, skip update
             boolean shouldUpdate = false;
-            int[] runningTaskIds = mIsSwipeForSplit
-                    ? TopTaskTracker.INSTANCE.get(mContext).getRunningSplitTaskIds()
-                    : new int[]{runningTaskId};
+            int[] runningTaskIds = mGestureState.getRunningTaskIds(mIsSwipeForSplit);
             for (int id : runningTaskIds) {
                 if (!mTaskSnapshotCache.containsKey(id)) {
                     shouldUpdate = true;
@@ -2205,16 +2216,27 @@
         if (!mCanceled) {
             TaskView nextTask = mRecentsView == null ? null : mRecentsView.getNextPageTaskView();
             if (nextTask != null) {
-                Task.TaskKey nextTaskKey = nextTask.getTask().key;
-                int taskId = nextTaskKey.id;
-                mGestureState.updateLastStartedTaskId(taskId);
-                boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
-                        .contains(taskId);
+                int[] taskIds = nextTask.getTaskIds();
+                StringBuilder nextTaskLog = new StringBuilder();
+                for (TaskIdAttributeContainer c : nextTask.getTaskIdAttributeContainers()) {
+                    if (c == null) {
+                        continue;
+                    }
+                    nextTaskLog
+                            .append("[id: ")
+                            .append(c.getTask().key.id)
+                            .append(", pkg: ")
+                            .append(c.getTask().key.getPackageName())
+                            .append("] | ");
+                }
+                mGestureState.updateLastStartedTaskIds(taskIds);
+                boolean hasTaskPreviouslyAppeared = Arrays.stream(taskIds).anyMatch(
+                                taskId -> mGestureState.getPreviouslyAppearedTaskIds()
+                                        .contains(taskId));
                 if (!hasTaskPreviouslyAppeared) {
                     ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED);
                 }
-                ActiveGestureLog.INSTANCE.addLog("Launching task: id=" + taskId
-                        + " pkg=" + nextTaskKey.getPackageName());
+                ActiveGestureLog.INSTANCE.addLog("Launching task: " + nextTaskLog);
                 nextTask.launchTask(success -> {
                     resultCallback.accept(success);
                     if (success) {
@@ -2284,7 +2306,7 @@
     public void onTasksAppeared(RemoteAnimationTarget[] appearedTaskTargets) {
         if (mRecentsAnimationController != null) {
             boolean hasStartedTaskBefore = Arrays.stream(appearedTaskTargets).anyMatch(
-                    targetCompat -> targetCompat.taskId == mGestureState.getLastStartedTaskId());
+                    mGestureState.mLastStartedTaskIdPredicate);
             if (!mStateCallback.hasStates(STATE_GESTURE_COMPLETED) && !hasStartedTaskBefore) {
                 // This is a special case, if a task is started mid-gesture that wasn't a part of a
                 // previous quickswitch task launch, then cancel the animation back to the app
@@ -2297,8 +2319,7 @@
             } else if (handleTaskAppeared(appearedTaskTargets)) {
                 Optional<RemoteAnimationTarget> taskTargetOptional =
                         Arrays.stream(appearedTaskTargets)
-                                .filter(targetCompat ->
-                                        targetCompat.taskId == mGestureState.getLastStartedTaskId())
+                                .filter(mGestureState.mLastStartedTaskIdPredicate)
                                 .findFirst();
                 if (!taskTargetOptional.isPresent()) {
                     ActiveGestureLog.INSTANCE.addLog("No appeared task matching started task id");
@@ -2367,11 +2388,16 @@
      * resume if we finish the controller.
      */
     protected int getLastAppearedTaskIndex() {
-        return mRecentsView == null
-                ? -1
-                : mGestureState.getLastAppearedTaskId() != -1
-                        ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId())
-                        : mRecentsView.getRunningTaskIndex();
+        if (mRecentsView == null) {
+            return -1;
+        }
+
+        OptionalInt firstValidTaskId = Arrays.stream(mGestureState.getLastAppearedTaskIds())
+                .filter(i -> i != -1)
+                .findFirst();
+        return firstValidTaskId.isPresent()
+                ? mRecentsView.getTaskIndexForId(firstValidTaskId.getAsInt())
+                : mRecentsView.getRunningTaskIndex();
     }
 
     /**
@@ -2379,7 +2405,7 @@
      * but before that task appeared.
      */
     protected boolean hasStartedNewTask() {
-        return mGestureState.getLastStartedTaskId() != -1;
+        return mGestureState.getLastStartedTaskIds()[0] != -1;
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index c7df18f..c2d8c62 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -15,8 +15,9 @@
  */
 package com.android.quickstep;
 
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+
 import static com.android.launcher3.MotionEventsUtils.isTrackpadFourFingerSwipe;
-import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
 import static com.android.launcher3.MotionEventsUtils.isTrackpadThreeFingerSwipe;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
@@ -44,10 +45,12 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Predicate;
 
 /**
  * Manages the state for an active system gesture, listens for events from the system and Launcher,
@@ -56,6 +59,18 @@
 @TargetApi(Build.VERSION_CODES.R)
 public class GestureState implements RecentsAnimationCallbacks.RecentsAnimationListener {
 
+    final Predicate<RemoteAnimationTarget> mLastStartedTaskIdPredicate = new Predicate<>() {
+        @Override
+        public boolean test(RemoteAnimationTarget targetCompat) {
+            for (int taskId : mLastStartedTaskId) {
+                if (targetCompat.taskId == taskId) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    };
+
     /**
      * Defines the end targets of a gesture and the associated state.
      */
@@ -161,9 +176,9 @@
     private TrackpadGestureType mTrackpadGestureType = TrackpadGestureType.NONE;
     private CachedTaskInfo mRunningTask;
     private GestureEndTarget mEndTarget;
-    private RemoteAnimationTarget mLastAppearedTaskTarget;
+    private RemoteAnimationTarget[] mLastAppearedTaskTargets;
     private Set<Integer> mPreviouslyAppearedTaskIds = new HashSet<>();
-    private int mLastStartedTaskId = -1;
+    private int[] mLastStartedTaskId = new int[]{INVALID_TASK_ID, INVALID_TASK_ID};
     private RecentsAnimationController mRecentsAnimationController;
     private HashMap<Integer, ThumbnailData> mRecentsAnimationCanceledSnapshots;
 
@@ -189,7 +204,7 @@
         mGestureId = other.mGestureId;
         mRunningTask = other.mRunningTask;
         mEndTarget = other.mEndTarget;
-        mLastAppearedTaskTarget = other.mLastAppearedTaskTarget;
+        mLastAppearedTaskTargets = other.mLastAppearedTaskTargets;
         mPreviouslyAppearedTaskIds = other.mPreviouslyAppearedTaskIds;
         mLastStartedTaskId = other.mLastStartedTaskId;
     }
@@ -293,10 +308,29 @@
     }
 
     /**
-     * @return the running task id for this gesture.
+     * @param getMultipleTasks Whether multiple tasks or not are to be returned (for split)
+     * @return the running task ids for this gesture.
      */
-    public int getRunningTaskId() {
-        return mRunningTask != null ? mRunningTask.getTaskId() : -1;
+    public int[] getRunningTaskIds(boolean getMultipleTasks) {
+        if (mRunningTask == null) {
+            return new int[]{INVALID_TASK_ID, INVALID_TASK_ID};
+        } else {
+            int cachedTasksSize = mRunningTask.mAllCachedTasks.size();
+            int count = Math.min(cachedTasksSize, getMultipleTasks ? 2 : 1);
+            int[] runningTaskIds = new int[count];
+            for (int i = 0; i < count; i++) {
+                runningTaskIds[i] = mRunningTask.mAllCachedTasks.get(i).taskId;
+            }
+            return runningTaskIds;
+        }
+    }
+
+    /**
+     * @see #getRunningTaskIds(boolean)
+     * @return the single top-most running taskId for this gesture
+     */
+    public int getTopRunningTaskId() {
+        return getRunningTaskIds(false /*getMultipleTasks*/)[0];
     }
 
     /**
@@ -309,18 +343,26 @@
     /**
      * Updates the last task that appeared during this gesture.
      */
-    public void updateLastAppearedTaskTarget(RemoteAnimationTarget lastAppearedTaskTarget) {
-        mLastAppearedTaskTarget = lastAppearedTaskTarget;
-        if (lastAppearedTaskTarget != null) {
-            mPreviouslyAppearedTaskIds.add(lastAppearedTaskTarget.taskId);
+    public void updateLastAppearedTaskTargets(RemoteAnimationTarget[] lastAppearedTaskTargets) {
+        mLastAppearedTaskTargets = lastAppearedTaskTargets;
+        for (RemoteAnimationTarget target : lastAppearedTaskTargets) {
+            if (target == null) {
+                continue;
+            }
+            mPreviouslyAppearedTaskIds.add(target.taskId);
         }
     }
 
     /**
      * @return The id of the task that appeared during this gesture.
      */
-    public int getLastAppearedTaskId() {
-        return mLastAppearedTaskTarget != null ? mLastAppearedTaskTarget.taskId : -1;
+    public int[] getLastAppearedTaskIds() {
+        if (mLastAppearedTaskTargets == null) {
+            return new int[]{INVALID_TASK_ID, INVALID_TASK_ID};
+        } else {
+            return Arrays.stream(mLastAppearedTaskTargets)
+                    .mapToInt(target -> target != null ? target.taskId : INVALID_TASK_ID).toArray();
+        }
     }
 
     public void updatePreviouslyAppearedTaskIds(Set<Integer> previouslyAppearedTaskIds) {
@@ -334,7 +376,7 @@
     /**
      * Updates the last task that we started via startActivityFromRecents() during this gesture.
      */
-    public void updateLastStartedTaskId(int lastStartedTaskId) {
+    public void updateLastStartedTaskIds(int[] lastStartedTaskId) {
         mLastStartedTaskId = lastStartedTaskId;
     }
 
@@ -342,7 +384,7 @@
      * @return The id of the task that was most recently started during this gesture, or -1 if
      * no task has been started yet (i.e. we haven't settled on a new task).
      */
-    public int getLastStartedTaskId() {
+    public int[] getLastStartedTaskIds() {
         return mLastStartedTaskId;
     }
 
@@ -478,8 +520,8 @@
         pw.println("  gestureID=" + mGestureId);
         pw.println("  runningTask=" + mRunningTask);
         pw.println("  endTarget=" + mEndTarget);
-        pw.println("  lastAppearedTaskTargetId=" + getLastAppearedTaskId());
-        pw.println("  lastStartedTaskId=" + mLastStartedTaskId);
+        pw.println("  lastAppearedTaskTargetId=" + Arrays.toString(mLastAppearedTaskTargets));
+        pw.println("  lastStartedTaskId=" + Arrays.toString(mLastStartedTaskId));
         pw.println("  isRecentsAnimationRunning=" + isRecentsAnimationRunning());
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 33a8261..2e1a62c 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -45,6 +45,7 @@
 import android.window.RemoteTransition;
 import android.window.SplashScreen;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.app.animation.Interpolators;
@@ -240,6 +241,11 @@
         }
 
         final TaskView taskView = (TaskView) v;
+        final RecentsView recentsView = taskView.getRecentsView();
+        if (recentsView == null) {
+            return super.getActivityLaunchOptions(v, item);
+        }
+
         RunnableList onEndCallback = new RunnableList();
 
         mActivityLaunchAnimationRunner = new RemoteAnimationFactory() {
@@ -248,7 +254,7 @@
                     RemoteAnimationTarget[] wallpaperTargets,
                     RemoteAnimationTarget[] nonAppTargets, AnimationResult result) {
                 mHandler.removeCallbacks(mAnimationStartTimeoutRunnable);
-                AnimatorSet anim = composeRecentsLaunchAnimator(taskView, appTargets,
+                AnimatorSet anim = composeRecentsLaunchAnimator(recentsView, taskView, appTargets,
                         wallpaperTargets, nonAppTargets);
                 anim.addListener(resetStateListener());
                 result.setAnimation(anim, RecentsActivity.this, onEndCallback::executeAllAndDestroy,
@@ -285,14 +291,16 @@
     /**
      * Composes the animations for a launch from the recents list if possible.
      */
-    private AnimatorSet  composeRecentsLaunchAnimator(TaskView taskView,
+    private AnimatorSet  composeRecentsLaunchAnimator(
+            @NonNull RecentsView recentsView,
+            @NonNull TaskView taskView,
             RemoteAnimationTarget[] appTargets,
             RemoteAnimationTarget[] wallpaperTargets,
             RemoteAnimationTarget[] nonAppTargets) {
         AnimatorSet target = new AnimatorSet();
         boolean activityClosing = taskIsATargetWithMode(appTargets, getTaskId(), MODE_CLOSING);
         PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
-        createRecentsWindowAnimator(taskView, !activityClosing, appTargets,
+        createRecentsWindowAnimator(recentsView, taskView, !activityClosing, appTargets,
                 wallpaperTargets, nonAppTargets, null /* depthController */, pa);
         target.play(pa.buildAnim());
 
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index f8e09e1..8972dc8 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -115,8 +115,8 @@
      * {@link RecentsAnimationCallbacks#onTasksAppeared}}.
      */
     @UiThread
-    public void removeTaskTarget(@NonNull RemoteAnimationTarget target) {
-        UI_HELPER_EXECUTOR.execute(() -> mController.removeTask(target.taskId));
+    public void removeTaskTarget(int targetTaskId) {
+        UI_HELPER_EXECUTOR.execute(() -> mController.removeTask(targetTaskId));
     }
 
     @UiThread
@@ -167,7 +167,6 @@
                 /* event= */ "finishRecentsAnimation",
                 /* extras= */ toRecents,
                 /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
-
         // Finish not yet requested
         mFinishRequested = true;
         mFinishTargetIsLauncher = toRecents;
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 410ba21..c0684d7 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -59,7 +59,7 @@
     private RecentsAnimationTargets mTargets;
     // Temporary until we can hook into gesture state events
     private GestureState mLastGestureState;
-    private RemoteAnimationTarget mLastAppearedTaskTarget;
+    private RemoteAnimationTarget[] mLastAppearedTaskTargets;
     private Runnable mLiveTileCleanUpHandler;
     private Context mCtx;
 
@@ -141,8 +141,15 @@
                 }
                 mController = controller;
                 mTargets = targets;
-                mLastAppearedTaskTarget = mTargets.findTask(mLastGestureState.getRunningTaskId());
-                mLastGestureState.updateLastAppearedTaskTarget(mLastAppearedTaskTarget);
+                // TODO(b/236226779): We can probably get away w/ setting mLastAppearedTaskTargets
+                //  to all appeared targets directly vs just looking at running ones
+                int[] runningTaskIds = mLastGestureState.getRunningTaskIds(targets.apps.length > 1);
+                mLastAppearedTaskTargets = new RemoteAnimationTarget[runningTaskIds.length];
+                for (int i = 0; i < runningTaskIds.length; i++) {
+                    RemoteAnimationTarget task = mTargets.findTask(runningTaskIds[i]);
+                    mLastAppearedTaskTargets[i] = task;
+                }
+                mLastGestureState.updateLastAppearedTaskTargets(mLastAppearedTaskTargets);
             }
 
             @Override
@@ -196,14 +203,18 @@
                             true /*shown*/, null /* animatorHandler */);
                 }
                 if (mController != null) {
-                    if (mLastAppearedTaskTarget == null
-                            || appearedTaskTarget.taskId != mLastAppearedTaskTarget.taskId) {
-                        if (mLastAppearedTaskTarget != null) {
-                            mController.removeTaskTarget(mLastAppearedTaskTarget);
+                    if (mLastAppearedTaskTargets != null) {
+                        for (RemoteAnimationTarget lastTarget : mLastAppearedTaskTargets) {
+                            for (RemoteAnimationTarget appearedTarget : appearedTaskTargets) {
+                                if (lastTarget != null &&
+                                        appearedTarget.taskId != lastTarget.taskId) {
+                                    mController.removeTaskTarget(lastTarget.taskId);
+                                }
+                            }
                         }
-                        mLastAppearedTaskTarget = appearedTaskTarget;
-                        mLastGestureState.updateLastAppearedTaskTarget(mLastAppearedTaskTarget);
                     }
+                    mLastAppearedTaskTargets = appearedTaskTargets;
+                    mLastGestureState.updateLastAppearedTaskTargets(mLastAppearedTaskTargets);
                 }
             }
 
@@ -268,7 +279,7 @@
         mCallbacks.addListener(gestureState);
         gestureState.setState(STATE_RECENTS_ANIMATION_INITIALIZED
                 | STATE_RECENTS_ANIMATION_STARTED);
-        gestureState.updateLastAppearedTaskTarget(mLastAppearedTaskTarget);
+        gestureState.updateLastAppearedTaskTargets(mLastAppearedTaskTargets);
         return mCallbacks;
     }
 
@@ -369,7 +380,7 @@
         mCallbacks = null;
         mTargets = null;
         mLastGestureState = null;
-        mLastAppearedTaskTarget = null;
+        mLastAppearedTaskTargets = null;
     }
 
     @Nullable
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index c9bad38..2d5b8db 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -38,6 +38,8 @@
 import static com.android.launcher3.QuickstepTransitionManager.SPLIT_DIVIDER_ANIM_DURATION;
 import static com.android.launcher3.QuickstepTransitionManager.SPLIT_LAUNCH_DURATION;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
+import static com.android.launcher3.testing.shared.TestProtocol.LAUNCH_SPLIT_PAIR;
+import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.quickstep.views.DesktopTaskView.DESKTOP_MODE_SUPPORTED;
 
@@ -160,13 +162,14 @@
     }
 
     public static void createRecentsWindowAnimator(
-            @NonNull TaskView v, boolean skipViewChanges,
+            @NonNull RecentsView recentsView,
+            @NonNull TaskView v,
+            boolean skipViewChanges,
             @NonNull RemoteAnimationTarget[] appTargets,
             @NonNull RemoteAnimationTarget[] wallpaperTargets,
             @NonNull RemoteAnimationTarget[] nonAppTargets,
             @Nullable DepthController depthController,
             PendingAnimation out) {
-        RecentsView recentsView = v.getRecentsView();
         boolean isQuickSwitch = v.isEndQuickswitchCuj();
         v.setEndQuickswitchCuj(false);
 
@@ -430,6 +433,7 @@
             int initialTaskId, int secondTaskId, @NonNull TransitionInfo transitionInfo,
             SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
         if (launchingTaskView != null) {
+            testLogD(LAUNCH_SPLIT_PAIR, "composeRecentsSplitLaunchAnimator taskView not-null");
             AnimatorSet animatorSet = new AnimatorSet();
             animatorSet.addListener(new AnimatorListenerAdapter() {
                 @Override
@@ -462,7 +466,10 @@
         TransitionInfo.Change splitRoot2 = null;
         for (int i = 0; i < transitionInfo.getChanges().size(); ++i) {
             final TransitionInfo.Change change = transitionInfo.getChanges().get(i);
-            if (change.getTaskInfo() == null) continue;
+            if (change.getTaskInfo() == null) {
+                testLogD(LAUNCH_SPLIT_PAIR, "changeTaskInfo null; change: " + change);
+                continue;
+            }
             final int taskId = change.getTaskInfo().taskId;
             final int mode = change.getMode();
 
@@ -498,6 +505,7 @@
 
     private static void animateSplitRoot(SurfaceControl.Transaction t,
             TransitionInfo.Change splitRoot) {
+        testLogD(LAUNCH_SPLIT_PAIR, "animateSplitRoot: " + splitRoot);
         if (splitRoot != null) {
             t.show(splitRoot.getLeash());
             t.setAlpha(splitRoot.getLeash(), 1.f);
@@ -606,8 +614,8 @@
 
         TaskView taskView = findTaskViewToLaunch(recentsView, v, appTargets);
         PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
-        createRecentsWindowAnimator(taskView, skipLauncherChanges, appTargets, wallpaperTargets,
-                nonAppTargets, depthController, pa);
+        createRecentsWindowAnimator(recentsView, taskView, skipLauncherChanges, appTargets,
+                wallpaperTargets, nonAppTargets, depthController, pa);
         if (launcherClosing) {
             // TODO(b/182592057): differentiate between "restore split" vs "launch fullscreen app"
             TaskViewUtils.createSplitAuxiliarySurfacesAnimator(nonAppTargets, true /*shown*/,
diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index d34cddf..01baed3 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -210,7 +210,7 @@
 
         @Nullable
         private final RunningTaskInfo mTopTask;
-        private final List<RunningTaskInfo> mAllCachedTasks;
+        public final List<RunningTaskInfo> mAllCachedTasks;
 
         CachedTaskInfo(List<RunningTaskInfo> allCachedTasks) {
             mAllCachedTasks = allCachedTasks;
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index a1ecaeb..f1244ff 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -41,7 +41,6 @@
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE;
@@ -792,7 +791,7 @@
                     ActiveGestureLog.INSTANCE.getLogId());
             taskInfo = previousGestureState.getRunningTask();
             gestureState.updateRunningTask(taskInfo);
-            gestureState.updateLastStartedTaskId(previousGestureState.getLastStartedTaskId());
+            gestureState.updateLastStartedTaskIds(previousGestureState.getLastStartedTaskIds());
             gestureState.updatePreviouslyAppearedTaskIds(
                     previousGestureState.getPreviouslyAppearedTaskIds());
         } else {
@@ -889,7 +888,8 @@
                             .append(SUBSTRING_PREFIX)
                             .append("TaskbarActivityContext != null, "
                                     + "using TaskbarUnstashInputConsumer");
-                    base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac);
+                    base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac,
+                            mOverviewCommandHelper);
                 }
             } else if (canStartSystemGesture && FeatureFlags.ENABLE_LONG_PRESS_NAV_HANDLE.get()) {
                 base = new NavHandleLongPressInputConsumer(this, base, mInputMonitorCompat);
@@ -1005,6 +1005,7 @@
         }
 
         reasonString.append(SUBSTRING_PREFIX).append("keyguard is not showing occluded");
+
         // Use overview input consumer for sharesheets on top of home.
         boolean forceOverviewInputConsumer = gestureState.getActivityInterface().isStarted()
                 && gestureState.getRunningTask() != null
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index e9a0761..172c9e9 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.inputconsumers;
 
+import static android.view.MotionEvent.ACTION_BUTTON_RELEASE;
 import static android.view.MotionEvent.INVALID_POINTER_ID;
 
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
@@ -41,6 +42,7 @@
 import com.android.launcher3.touch.OverScroll;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.InputConsumer;
+import com.android.quickstep.OverviewCommandHelper;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 /**
@@ -51,6 +53,7 @@
 public class TaskbarUnstashInputConsumer extends DelegateInputConsumer {
 
     private final TaskbarActivityContext mTaskbarActivityContext;
+    private final OverviewCommandHelper mOverviewCommandHelper;
     private final GestureDetector mLongPressDetector;
     private final float mSquaredTouchSlop;
 
@@ -80,9 +83,11 @@
     private final @Nullable TransitionCallback mTransitionCallback;
 
     public TaskbarUnstashInputConsumer(Context context, InputConsumer delegate,
-            InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext) {
+            InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext,
+            OverviewCommandHelper overviewCommandHelper) {
         super(delegate, inputMonitor);
         mTaskbarActivityContext = taskbarActivityContext;
+        mOverviewCommandHelper = overviewCommandHelper;
         // TODO(b/270395798): remove this when cleaning up old Persistent Taskbar code.
         mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
         mScreenWidth = taskbarActivityContext.getDeviceProfile().widthPx;
@@ -123,7 +128,11 @@
     public void onMotionEvent(MotionEvent ev) {
         mLongPressDetector.onTouchEvent(ev);
         if (mState != STATE_ACTIVE) {
-            mDelegate.onMotionEvent(ev);
+            boolean isStashedTaskbarHovered =
+                    isStashedTaskbarHovered((int) ev.getX(), (int) ev.getY());
+            if (!isStashedTaskbarHovered) {
+                mDelegate.onMotionEvent(ev);
+            }
 
             // Only show the transient task bar if the touch events are on the screen.
             if (mTaskbarActivityContext != null && !isTrackpadMotionEvent(ev)) {
@@ -218,6 +227,11 @@
                         mHasPassedTaskbarNavThreshold = false;
                         mIsInBubbleBarArea = false;
                         break;
+                    case ACTION_BUTTON_RELEASE:
+                        if (isStashedTaskbarHovered) {
+                            mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HOME);
+                        }
+                        break;
                 }
             }
         }
@@ -274,19 +288,17 @@
 
     private void updateHoveredTaskbarState(int x, int y) {
         DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
-        mStashedTaskbarHandleBounds.set(
+        mBottomEdgeBounds.set(
                 (dp.widthPx - (int) mUnstashArea) / 2,
-                dp.heightPx - dp.stashedTaskbarHeight,
+                dp.heightPx - mStashedTaskbarBottomEdge,
                 (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
                 dp.heightPx);
-        mBottomEdgeBounds.set(mStashedTaskbarHandleBounds);
-        mBottomEdgeBounds.top = dp.heightPx - mStashedTaskbarBottomEdge;
 
         if (mBottomEdgeBounds.contains(x, y)) {
             // If hovering stashed taskbar and then hover screen bottom edge, unstash it.
             mTaskbarActivityContext.onSwipeToUnstashTaskbar();
             mIsStashedTaskbarHovered = false;
-        } else if (!mStashedTaskbarHandleBounds.contains(x, y)) {
+        } else if (!isStashedTaskbarHovered(x, y)) {
             // If exit hovering stashed taskbar, remove hint.
             startStashedTaskbarHover(/* isHovered = */ false);
         }
@@ -294,18 +306,13 @@
 
     private void updateUnhoveredTaskbarState(int x, int y) {
         DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
-        mStashedTaskbarHandleBounds.set(
-                (dp.widthPx - (int) mUnstashArea) / 2,
-                dp.heightPx - dp.stashedTaskbarHeight,
-                (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
-                dp.heightPx);
         mBottomEdgeBounds.set(
                 0,
                 dp.heightPx - mBottomScreenEdge,
                 dp.widthPx,
                 dp.heightPx);
 
-        if (mStashedTaskbarHandleBounds.contains(x, y)) {
+        if (isStashedTaskbarHovered(x, y)) {
             // If enter hovering stashed taskbar, start hint.
             startStashedTaskbarHover(/* isHovered = */ true);
         } else if (mBottomEdgeBounds.contains(x, y)) {
@@ -318,4 +325,19 @@
         mTaskbarActivityContext.startTaskbarUnstashHint(isHovered, /* forceUnstash = */ true);
         mIsStashedTaskbarHovered = isHovered;
     }
+
+    private boolean isStashedTaskbarHovered(int x, int y) {
+        if (!mTaskbarActivityContext.isTaskbarStashed()
+                || mTaskbarActivityContext.isTaskbarAllAppsOpen()
+                || !ENABLE_CURSOR_HOVER_STATES.get()) {
+            return false;
+        }
+        DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
+        mStashedTaskbarHandleBounds.set(
+                (dp.widthPx - (int) mUnstashArea) / 2,
+                dp.heightPx - dp.stashedTaskbarHeight,
+                (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
+                dp.heightPx);
+        return mStashedTaskbarHandleBounds.contains(x, y);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index 9f6119c..f11bc81 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -302,7 +302,7 @@
     }
 
     private AnimatedFloat createSwipeUpProxy(GestureState state) {
-        if (state.getRunningTaskId() != getTaskId()) {
+        if (state.getTopRunningTaskId() != getTaskId()) {
             return null;
         }
         mSwipeProgress.updateValue(0);
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 1f06f94..be66637 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -62,14 +62,11 @@
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.Executors;
-import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.LogConfig;
 import com.android.launcher3.views.ActivityContext;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.SysUiStatsLog;
 
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Optional;
 import java.util.OptionalInt;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -564,14 +561,16 @@
      * Helps to construct and log statsd compatible impression events.
      */
     private static class StatsCompatImpressionLogger implements StatsImpressionLogger {
-        private int[] mResultTypeList = new int[]{};
-        private int[] mResultCountList = new int[]{};
-        private final List<Boolean> mAboveKeyboardList = new ArrayList<>();
-        private int[] mUidList = new int[]{};
         private InstanceId mInstanceId = DEFAULT_INSTANCE_ID;
         private State mLauncherState = State.UNKNOWN;
         private int mQueryLength = -1;
 
+        // Fields used for Impression Logging V2.
+        private int mResultType;
+        private boolean mAboveKeyboard = false;
+        private int mUid;
+        private int mResultSource;
+
         @Override
         public StatsImpressionLogger withInstanceId(InstanceId instanceId) {
             this.mInstanceId = instanceId;
@@ -591,69 +590,60 @@
         }
 
         @Override
-        public StatsImpressionLogger withResultType(IntArray resultType) {
-            mResultTypeList = resultType.toArray();
+        public StatsImpressionLogger withResultType(int resultType) {
+            mResultType = resultType;
+            return this;
+        }
+
+
+        @Override
+        public StatsImpressionLogger withAboveKeyboard(boolean aboveKeyboard) {
+            mAboveKeyboard = aboveKeyboard;
             return this;
         }
 
         @Override
-        public StatsImpressionLogger withResultCount(IntArray resultCount) {
-            mResultCountList = resultCount.toArray();
+        public StatsImpressionLogger withUid(int uid) {
+            mUid = uid;
             return this;
         }
 
         @Override
-        public StatsImpressionLogger withAboveKeyboard(List<Boolean> aboveKeyboard) {
-            mAboveKeyboardList.clear();
-            this.mAboveKeyboardList.addAll(aboveKeyboard);
-            return this;
-        }
-
-        @Override
-        public StatsImpressionLogger withUids(IntArray uid) {
-            mUidList = uid.toArray();
+        public StatsImpressionLogger withResultSource(int resultSource) {
+            mResultSource = resultSource;
             return this;
         }
 
         @Override
         public void log(EventEnum event) {
-            boolean[] mAboveKeyboard = new boolean[mAboveKeyboardList.size()];
-            for (int i = 0; i < mAboveKeyboardList.size(); i++) {
-                mAboveKeyboard[i] = mAboveKeyboardList.get(i);
-            }
             if (IS_VERBOSE) {
                 String name = (event instanceof Enum) ? ((Enum) event).name() :
                         event.getId() + "";
                 StringBuilder logStringBuilder = new StringBuilder("\n");
                 logStringBuilder.append(String.format("InstanceId:%s ", mInstanceId));
                 logStringBuilder.append(String.format("ImpressionEvent:%s ", name));
-                logStringBuilder.append(String.format("LauncherState = %s ", mLauncherState));
-                logStringBuilder.append(String.format("QueryLength = %s ", mQueryLength));
-                for (int i = 0; i < mResultTypeList.length; i++) {
-                    logStringBuilder.append(String.format(
-                            "\n ResultType = %s with ResultCount = %s with is_above_keyboard = %s"
-                                    + " with uid = %s",
-                            mResultTypeList[i], mResultCountList[i],
-                            mAboveKeyboard[i], mUidList[i]));
-                }
+                logStringBuilder.append(String.format("\n\tLauncherState = %s ", mLauncherState));
+                logStringBuilder.append(String.format("\tQueryLength = %s ", mQueryLength));
+                logStringBuilder.append(String.format(
+                        "\n\t ResultType = %s is_above_keyboard = %s"
+                                + " uid = %s result_source = %s",
+                        mResultType,
+                        mAboveKeyboard, mUid, mResultSource));
+
                 Log.d(IMPRESSION_TAG, logStringBuilder.toString());
             }
 
 
-
-            SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_IMPRESSION_EVENT,
+            SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_IMPRESSION_EVENT_V2,
                     event.getId(), // event_id
                     mInstanceId.getId(), // instance_id
                     mLauncherState.getLauncherState(), // state
                     mQueryLength, // query_length
-                    //result type list
-                    mResultTypeList,
-                    // result count list
-                    mResultCountList,
-                    // above keyboard list
-                    mAboveKeyboard,
-                    // uid list
-                    mUidList
+                    mResultType, //result type
+                    mAboveKeyboard, // above keyboard
+                    mUid, // uid
+                    mResultSource // result source
+
             );
         }
     }
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 5740991..56d6857 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -26,15 +26,17 @@
 import android.graphics.RectF
 import android.graphics.drawable.Drawable
 import android.view.View
+import com.android.app.animation.Interpolators
 import com.android.launcher3.DeviceProfile
-import com.android.launcher3.Launcher
 import com.android.launcher3.Utilities
 import com.android.launcher3.anim.PendingAnimation
-import com.android.launcher3.dragndrop.DragLayer
+import com.android.launcher3.statemanager.StatefulActivity
 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource
+import com.android.launcher3.views.BaseDragLayer
 import com.android.quickstep.views.FloatingTaskView
 import com.android.quickstep.views.IconView
 import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.SplitInstructionsView
 import com.android.quickstep.views.TaskThumbnailView
 import com.android.quickstep.views.TaskView
 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
@@ -58,6 +60,8 @@
         )
     }
 
+    var splitInstructionsView: SplitInstructionsView? = null
+
     /**
      * Returns different elements to animate for the initial split selection animation
      * depending on the state of the surface from which the split was initiated
@@ -188,31 +192,26 @@
     }
 
     /** Does not play any animation if user is not currently in split selection state. */
-    fun playPlaceholderDismissAnim(launcher: Launcher) {
+    fun playPlaceholderDismissAnim(launcher: StatefulActivity<*>) {
         if (!splitSelectStateController.isSplitSelectActive) {
             return
         }
 
         val anim = createPlaceholderDismissAnim(launcher)
-        anim.addListener(object : AnimatorListenerAdapter() {
-            override fun onAnimationEnd(animation: Animator) {
-                splitSelectStateController.resetState()
-            }
-        })
         anim.start()
     }
 
     /** Returns [AnimatorSet] which slides initial split placeholder view offscreen. */
-    fun createPlaceholderDismissAnim(launcher: Launcher) : AnimatorSet {
+    fun createPlaceholderDismissAnim(launcher: StatefulActivity<*>) : AnimatorSet {
         val animatorSet = AnimatorSet()
         val recentsView : RecentsView<*, *> = launcher.getOverviewPanel()
         val floatingTask: FloatingTaskView = splitSelectStateController.firstFloatingTaskView
                 ?: return animatorSet
 
         // We are in split selection state currently, transitioning to another state
-        val dragLayer: DragLayer = launcher.dragLayer
+        val dragLayer: BaseDragLayer<*> = launcher.dragLayer
         val onScreenRectF = RectF()
-        Utilities.getBoundsForViewInDragLayer(launcher.dragLayer, floatingTask,
+        Utilities.getBoundsForViewInDragLayer(dragLayer, floatingTask,
                 Rect(0, 0, floatingTask.width, floatingTask.height),
                 false, null, onScreenRectF)
         // Get the part of the floatingTask that intersects with the DragLayer (i.e. the
@@ -233,6 +232,42 @@
                                 floatingTask.stagePosition,
                                 launcher.deviceProfile
                         )))
+        animatorSet.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator) {
+                splitSelectStateController.resetState()
+                safeRemoveViewFromDragLayer(launcher, splitInstructionsView)
+            }
+        })
         return animatorSet
     }
+
+    /**
+     * Returns a [PendingAnimation] to animate in the chip to instruct a user to select a second
+     * app for splitscreen
+     */
+    fun getShowSplitInstructionsAnim(launcher: StatefulActivity<*>) : PendingAnimation {
+        safeRemoveViewFromDragLayer(launcher, splitInstructionsView)
+        splitInstructionsView = SplitInstructionsView.getSplitInstructionsView(launcher)
+        val timings = AnimUtils.getDeviceOverviewToSplitTimings(launcher.deviceProfile.isTablet)
+        val anim = PendingAnimation(100 /*duration */)
+        anim.setViewAlpha(splitInstructionsView, 1f,
+                Interpolators.clampToProgress(Interpolators.LINEAR,
+                        timings.instructionsContainerFadeInStartOffset,
+                        timings.instructionsContainerFadeInEndOffset))
+        anim.setViewAlpha(splitInstructionsView!!.textView, 1f,
+                Interpolators.clampToProgress(Interpolators.LINEAR,
+                        timings.instructionsTextFadeInStartOffset,
+                        timings.instructionsTextFadeInEndOffset))
+        anim.addFloat(splitInstructionsView, SplitInstructionsView.UNFOLD, 0.1f, 1f,
+                Interpolators.clampToProgress(Interpolators.EMPHASIZED_DECELERATE,
+                        timings.instructionsUnfoldStartOffset,
+                        timings.instructionsUnfoldEndOffset))
+        return anim
+    }
+
+    private fun safeRemoveViewFromDragLayer(launcher: StatefulActivity<*>, view: View?) {
+        if (view != null) {
+            launcher.dragLayer.removeView(view)
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
index e46012a..ae500a1 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
@@ -374,6 +374,18 @@
         return secondTaskId
     }
 
+    fun getSplitEvent(): EventEnum? {
+        return splitEvent
+    }
+
+    fun getInitialStagePosition(): Int {
+        return initialStagePosition
+    }
+
+    fun getItemInfo(): ItemInfo? {
+        return itemInfo
+    }
+
     private fun isSecondTaskIntentSet(): Boolean {
         return secondTaskId != INVALID_TASK_ID || secondIntent != null
                 || secondPendingIntent != null
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 970cf64..7ba6d42 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -16,14 +16,11 @@
 
 package com.android.quickstep.util;
 
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT;
-import static android.app.PendingIntent.FLAG_MUTABLE;
-
 import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.testing.shared.TestProtocol.LAUNCH_SPLIT_PAIR;
+import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO;
-import static com.android.launcher3.util.SplitConfigurationOptions.getOppositeStagePosition;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_PENDINGINTENT;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_TASK;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SHORTCUT_TASK;
@@ -41,7 +38,6 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
 import android.os.Bundle;
 import android.os.Handler;
@@ -64,13 +60,11 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.SplitSelectionListener;
@@ -100,30 +94,11 @@
     private final SplitAnimationController mSplitAnimationController;
     private final AppPairsController mAppPairsController;
     private final SplitSelectDataHolder mSplitSelectDataHolder;
-    private StatsLogManager mStatsLogManager;
+    private final StatsLogManager mStatsLogManager;
     private final SystemUiProxy mSystemUiProxy;
     private final StateManager mStateManager;
     @Nullable
     private DepthController mDepthController;
-    private @StagePosition int mInitialStagePosition;
-    private ItemInfo mItemInfo;
-    /** {@link #mInitialTaskIntent} and {@link #mInitialUser} (the user of the Intent) are set
-     * together when split is initiated from an Intent. */
-    private Intent mInitialTaskIntent;
-    private UserHandle mInitialUser;
-    private int mInitialTaskId = INVALID_TASK_ID;
-    /** {@link #mSecondTaskIntent} and {@link #mSecondUser} (the user of the Intent) are set
-     * together when split is confirmed with an Intent. Either this or {@link #mSecondPendingIntent}
-     * will be set, but not both
-     */
-    private Intent mSecondTaskIntent;
-    /**
-     * Set when split is confirmed via a widget. Either this or {@link #mSecondTaskIntent} will be
-     * set, but not both
-     */
-    private PendingIntent mSecondPendingIntent;
-    private UserHandle mSecondUser;
-    private int mSecondTaskId = INVALID_TASK_ID;
     private boolean mRecentsAnimationRunning;
     /** If {@code true}, animates the existing task view split placeholder view */
     private boolean mAnimateCurrentTaskDismissal;
@@ -135,8 +110,6 @@
     /** If not null, this is the TaskView we want to launch from */
     @Nullable
     private GroupedTaskView mLaunchingTaskView;
-    /** Represents where split is intended to be invoked from. */
-    private StatsLogManager.EventEnum mSplitEvent;
 
     private FloatingTaskView mFirstFloatingTaskView;
 
@@ -165,19 +138,8 @@
     public void setInitialTaskSelect(@Nullable Intent intent, @StagePosition int stagePosition,
             @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent,
             int alreadyRunningTask) {
-        if (alreadyRunningTask != INVALID_TASK_ID) {
-            mInitialTaskId = alreadyRunningTask;
-        } else {
-            mInitialTaskIntent = intent;
-            mInitialUser = itemInfo.user;
-        }
-
-        setInitialData(stagePosition, splitEvent, itemInfo);
-
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            mSplitSelectDataHolder.setInitialTaskSelect(intent, stagePosition, itemInfo, splitEvent,
-                    alreadyRunningTask);
-        }
+        mSplitSelectDataHolder.setInitialTaskSelect(intent, stagePosition, itemInfo, splitEvent,
+                alreadyRunningTask);
     }
 
     /**
@@ -187,19 +149,7 @@
     public void setInitialTaskSelect(ActivityManager.RunningTaskInfo info,
             @StagePosition int stagePosition, @NonNull ItemInfo itemInfo,
             StatsLogManager.EventEnum splitEvent) {
-        mInitialTaskId = info.taskId;
-        setInitialData(stagePosition, splitEvent, itemInfo);
-
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            mSplitSelectDataHolder.setInitialTaskSelect(info, stagePosition, itemInfo, splitEvent);
-        }
-    }
-
-    private void setInitialData(@StagePosition int stagePosition,
-            StatsLogManager.EventEnum splitEvent, ItemInfo itemInfo) {
-        mItemInfo = itemInfo;
-        mInitialStagePosition = stagePosition;
-        mSplitEvent = splitEvent;
+        mSplitSelectDataHolder.setInitialTaskSelect(info, stagePosition, itemInfo, splitEvent);
     }
 
     /**
@@ -243,7 +193,7 @@
      */
     public boolean isInstanceOfComponent(@Nullable Task task, @NonNull ComponentKey componentKey) {
         // Exclude the task that is already staged
-        if (task == null || task.key.id == mInitialTaskId) {
+        if (task == null || task.key.id == mSplitSelectDataHolder.getInitialTaskId()) {
             return false;
         }
 
@@ -273,20 +223,19 @@
     }
 
     /**
-     * To be called when the actual tasks ({@link #mInitialTaskId}, {@link #mSecondTaskId}) are
-     * to be launched. Call after launcher side animations are complete.
+     * To be called when the both split tasks are ready to be launched. Call after launcher side
+     * animations are complete.
      */
     public void launchSplitTasks(Consumer<Boolean> callback) {
         Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
                 LogUtils.getShellShareableInstanceId();
-        launchTasks(mInitialTaskId, mInitialTaskIntent, mSecondTaskId, mSecondTaskIntent,
-                mInitialStagePosition, callback, false /* freezeTaskList */, DEFAULT_SPLIT_RATIO,
+        launchTasks(callback, false /* freezeTaskList */, DEFAULT_SPLIT_RATIO,
                 instanceIds.first);
 
         mStatsLogManager.logger()
-                .withItemInfo(mItemInfo)
+                .withItemInfo(mSplitSelectDataHolder.getItemInfo())
                 .withInstanceId(instanceIds.second)
-                .log(mSplitEvent);
+                .log(mSplitSelectDataHolder.getSplitEvent());
     }
 
     /**
@@ -294,11 +243,7 @@
      * @param task The second task that will be launched.
      */
     public void setSecondTask(Task task) {
-        mSecondTaskId = task.key.id;
-
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            mSplitSelectDataHolder.setSecondTask(task.key.id);
-        }
+        mSplitSelectDataHolder.setSecondTask(task.key.id);
     }
 
     /**
@@ -307,108 +252,29 @@
      * @param user The user of that intent.
      */
     public void setSecondTask(Intent intent, UserHandle user) {
-        mSecondTaskIntent = intent;
-        mSecondUser = user;
-
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            mSplitSelectDataHolder.setSecondTask(intent, user);
-        }
+        mSplitSelectDataHolder.setSecondTask(intent, user);
     }
 
     /**
      * To be called as soon as user selects the second app (even if animations aren't complete)
-     * Sets {@link #mSecondUser} from that of the pendingIntent
      * @param pendingIntent The second PendingIntent that will be launched.
      */
     public void setSecondTask(PendingIntent pendingIntent) {
-        mSecondPendingIntent = pendingIntent;
-        mSecondUser = pendingIntent.getCreatorUserHandle();
-
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            mSplitSelectDataHolder.setSecondTask(pendingIntent);
-        }
+        mSplitSelectDataHolder.setSecondTask(pendingIntent);
     }
 
     /**
      * To be called when we want to launch split pairs from Overview. Split can be initiated from
      * either Overview or home, or all apps. Either both taskIds are set, or a pending intent + a
      * fill in intent with a taskId2 are set.
-     * @param intent1 is null when split is initiated from Overview
-     * @param stagePosition representing location of task1
      * @param shellInstanceId loggingId to be used by shell, will be non-null for actions that
      *                   create a split instance, null for cases that bring existing instaces to the
      *                   foreground (quickswitch, launching previous pairs from overview)
      */
-    public void launchTasks(int taskId1, @Nullable Intent intent1, int taskId2,
-            @Nullable Intent intent2, @StagePosition int stagePosition,
-            Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio,
+    public void launchTasks(Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio,
             @Nullable InstanceId shellInstanceId) {
         TestLogging.recordEvent(
                 TestProtocol.SEQUENCE_MAIN, "launchSplitTasks");
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            launchTasksRefactored(callback, freezeTaskList, splitRatio, shellInstanceId);
-            return;
-        }
-
-        final ActivityOptions options1 = ActivityOptions.makeBasic();
-        if (freezeTaskList) {
-            options1.setFreezeRecentTasksReordering();
-        }
-        boolean hasSecondaryPendingIntent = mSecondPendingIntent != null;
-        if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
-            final RemoteTransition remoteTransition = getShellRemoteTransition(taskId1, taskId2,
-                    callback);
-            if (intent1 == null && (intent2 == null && !hasSecondaryPendingIntent)) {
-                mSystemUiProxy.startTasks(taskId1, options1.toBundle(), taskId2,
-                        null /* options2 */, stagePosition, splitRatio, remoteTransition,
-                        shellInstanceId);
-            } else if (intent2 == null && !hasSecondaryPendingIntent) {
-                launchIntentOrShortcut(intent1, mInitialUser, options1, taskId2, stagePosition,
-                        splitRatio, remoteTransition, shellInstanceId);
-            } else if (intent1 == null) {
-                launchIntentOrShortcut(intent2, mSecondUser, options1, taskId1,
-                        getOppositeStagePosition(stagePosition), splitRatio, remoteTransition,
-                        shellInstanceId);
-            } else {
-                mSystemUiProxy.startIntents(getPendingIntent(intent1, mInitialUser),
-                        mInitialUser.getIdentifier(), getShortcutInfo(intent1, mInitialUser),
-                        options1.toBundle(), hasSecondaryPendingIntent
-                                ? mSecondPendingIntent
-                                : getPendingIntent(intent2, mSecondUser),
-                        mSecondUser.getIdentifier(), getShortcutInfo(intent2, mSecondUser),
-                        null /* options2 */, stagePosition, splitRatio, remoteTransition,
-                        shellInstanceId);
-            }
-        } else {
-            final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(taskId1, taskId2,
-                    callback);
-
-            if (intent1 == null && (intent2 == null && !hasSecondaryPendingIntent)) {
-                mSystemUiProxy.startTasksWithLegacyTransition(taskId1, options1.toBundle(),
-                        taskId2, null /* options2 */, stagePosition, splitRatio, adapter,
-                        shellInstanceId);
-            } else if (intent2 == null && !hasSecondaryPendingIntent) {
-                launchIntentOrShortcutLegacy(intent1, mInitialUser, options1, taskId2,
-                        stagePosition, splitRatio, adapter, shellInstanceId);
-            } else if (intent1 == null) {
-                launchIntentOrShortcutLegacy(intent2, mSecondUser, options1, taskId1,
-                        getOppositeStagePosition(stagePosition), splitRatio, adapter,
-                        shellInstanceId);
-            } else {
-                mSystemUiProxy.startIntentsWithLegacyTransition(
-                        getPendingIntent(intent1, mInitialUser), mInitialUser.getIdentifier(),
-                        getShortcutInfo(intent1, mInitialUser), options1.toBundle(),
-                        hasSecondaryPendingIntent
-                                ? mSecondPendingIntent
-                                : getPendingIntent(intent2, mSecondUser),
-                        mSecondUser.getIdentifier(), getShortcutInfo(intent2, mSecondUser),
-                        null /* options2 */, stagePosition, splitRatio, adapter, shellInstanceId);
-            }
-        }
-    }
-
-    private void launchTasksRefactored(Consumer<Boolean> callback, boolean freezeTaskList,
-            float splitRatio, @Nullable InstanceId shellInstanceId) {
         final ActivityOptions options1 = ActivityOptions.makeBasic();
         if (freezeTaskList) {
             options1.setFreezeRecentTasksReordering();
@@ -429,7 +295,7 @@
 
         if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
             final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId,
-                    secondTaskId, callback);
+                    secondTaskId, callback, "LaunchSplitPair");
             switch (launchData.getSplitLaunchType()) {
                 case SPLIT_TASK_TASK ->
                         mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
@@ -502,9 +368,8 @@
 
     /**
      * Used to launch split screen from a split pair that already exists (usually accessible through
-     * Overview). This is different than
-     * {@link #launchTasks(int, Intent, int, Intent, int, Consumer, boolean, float, InstanceId)} in
-     * that this only launches split screen that are existing tasks. This doesn't determine which
+     * Overview). This is different than {@link #launchTasks(Consumer, boolean, float, InstanceId)}
+     * in that this only launches split screen that are existing tasks. This doesn't determine which
      * API should be used (i.e. launching split with existing tasks vs intents vs shortcuts, etc).
      *
      * <p/>
@@ -522,7 +387,7 @@
 
         if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
             final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId,
-                    secondTaskId, callback);
+                    secondTaskId, callback, "LaunchExistingPair");
             mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
                     null /* options2 */, stagePosition, splitRatio,
                     remoteTransition, null /*shellInstanceId*/);
@@ -541,11 +406,6 @@
      * split and fullscreen tasks)
      */
     public void launchInitialAppFullscreen(Consumer<Boolean> callback) {
-        if (!FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            launchSplitTasks(callback);
-            return;
-        }
-
         final ActivityOptions options1 = ActivityOptions.makeBasic();
         SplitSelectDataHolder.SplitLaunchData launchData =
                 mSplitSelectDataHolder.getFullscreenLaunchData();
@@ -597,11 +457,11 @@
     }
 
     private RemoteTransition getShellRemoteTransition(int firstTaskId, int secondTaskId,
-            Consumer<Boolean> callback) {
+            Consumer<Boolean> callback, String transitionName) {
         final RemoteSplitLaunchTransitionRunner animationRunner =
                 new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback);
         return new RemoteTransition(animationRunner,
-                ActivityThread.currentActivityThread().getApplicationThread(), "LaunchSplitPair");
+                ActivityThread.currentActivityThread().getApplicationThread(), transitionName);
     }
 
     private RemoteAnimationAdapter getLegacyRemoteAdapter(int firstTaskId, int secondTaskId,
@@ -612,95 +472,18 @@
                 ActivityThread.currentActivityThread().getApplicationThread());
     }
 
-    private void launchIntentOrShortcut(Intent intent, UserHandle user, ActivityOptions options1,
-            int taskId, @StagePosition int stagePosition, float splitRatio,
-            RemoteTransition remoteTransition, @Nullable InstanceId shellInstanceId) {
-        final ShortcutInfo shortcutInfo = getShortcutInfo(intent, user);
-        if (shortcutInfo != null) {
-            mSystemUiProxy.startShortcutAndTask(shortcutInfo,
-                    options1.toBundle(), taskId, null /* options2 */, stagePosition,
-                    splitRatio, remoteTransition, shellInstanceId);
-        } else {
-            mSystemUiProxy.startIntentAndTask(getPendingIntent(intent, user), user.getIdentifier(),
-                    options1.toBundle(), taskId, null /* options2 */, stagePosition, splitRatio,
-                    remoteTransition, shellInstanceId);
-        }
-    }
-
-    private void launchIntentOrShortcutLegacy(Intent intent, UserHandle user,
-            ActivityOptions options1, int taskId, @StagePosition int stagePosition,
-            float splitRatio, RemoteAnimationAdapter adapter,
-            @Nullable InstanceId shellInstanceId) {
-        final ShortcutInfo shortcutInfo = getShortcutInfo(intent, user);
-        if (shortcutInfo != null) {
-            mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(shortcutInfo,
-                    options1.toBundle(), taskId, null /* options2 */, stagePosition,
-                    splitRatio, adapter, shellInstanceId);
-        } else {
-            mSystemUiProxy.startIntentAndTaskWithLegacyTransition(
-                    getPendingIntent(intent, user), user.getIdentifier(), options1.toBundle(),
-                    taskId, null /* options2 */, stagePosition, splitRatio, adapter,
-                    shellInstanceId);
-        }
-    }
-
-    /**
-     * We treat launching by intents as grouped in two ways,
-     * If {@param intent} represents the first app, we always convert the intent to pending intent
-     * It it represents second app, either the second intent OR mSecondPendingIntent will be used
-     *    convert second intent to a pendingIntent OR return mSecondPendingIntent as is
-     */
-    private PendingIntent getPendingIntent(Intent intent, UserHandle user) {
-        boolean isParamFirstIntent = intent != null && intent == mInitialTaskIntent;
-        if (!isParamFirstIntent && mSecondPendingIntent != null) {
-            // Because mSecondPendingIntent and mSecondTaskIntent can't both be set, we know we need
-            // to be using mSecondPendingIntent
-            return mSecondPendingIntent;
-        }
-
-        // intent param must either be mInitialTaskIntent or mSecondTaskIntent, convert either to
-        // a new PendingIntent
-        return intent == null ? null : (user != null
-                ? PendingIntent.getActivityAsUser(mContext, 0, intent,
-                FLAG_MUTABLE | FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT, null /* options */, user)
-                : PendingIntent.getActivity(mContext, 0, intent,
-                        FLAG_MUTABLE | FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT));
-    }
-
     public @StagePosition int getActiveSplitStagePosition() {
-        return mInitialStagePosition;
+        return mSplitSelectDataHolder.getInitialStagePosition();
     }
 
     public StatsLogManager.EventEnum getSplitEvent() {
-        return mSplitEvent;
+        return mSplitSelectDataHolder.getSplitEvent();
     }
 
     public void setRecentsAnimationRunning(boolean running) {
         mRecentsAnimationRunning = running;
     }
 
-    @Nullable
-    private ShortcutInfo getShortcutInfo(Intent intent, UserHandle user) {
-        if (intent == null || intent.getPackage() == null) {
-            return null;
-        }
-
-        final String shortcutId = intent.getStringExtra(ShortcutKey.EXTRA_SHORTCUT_ID);
-        if (shortcutId == null) {
-            return null;
-        }
-
-        try {
-            final Context context = mContext.createPackageContextAsUser(
-                    intent.getPackage(), 0 /* flags */, user);
-            return new ShortcutInfo.Builder(context, shortcutId).build();
-        } catch (PackageManager.NameNotFoundException e) {
-            Log.w(TAG, "Failed to create a ShortcutInfo for " + intent.getPackage());
-        }
-
-        return null;
-    }
-
     public boolean isAnimateCurrentTaskDismissal() {
         return mAnimateCurrentTaskDismissal;
     }
@@ -741,6 +524,7 @@
         public void startAnimation(IBinder transition, TransitionInfo info,
                 SurfaceControl.Transaction t,
                 IRemoteTransitionFinishedCallback finishedCallback) {
+            testLogD(LAUNCH_SPLIT_PAIR, "Received split startAnimation");
             final Runnable finishAdapter = () ->  {
                 try {
                     finishedCallback.onTransitionFinished(null /* wct */, null /* sct */);
@@ -821,24 +605,12 @@
      * other state.
      */
     public void resetState() {
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            mSplitSelectDataHolder.resetState();
-        }
+        mSplitSelectDataHolder.resetState();
         dispatchOnSplitSelectionExit();
-        mInitialTaskId = INVALID_TASK_ID;
-        mInitialTaskIntent = null;
-        mSecondTaskId = INVALID_TASK_ID;
-        mSecondTaskIntent = null;
-        mInitialUser = null;
-        mSecondUser = null;
-        mInitialStagePosition = SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
         mRecentsAnimationRunning = false;
         mLaunchingTaskView = null;
-        mItemInfo = null;
-        mSplitEvent = null;
         mAnimateCurrentTaskDismissal = false;
         mDismissingFromSplitPair = false;
-        mSecondPendingIntent = null;
         mFirstFloatingTaskView = null;
     }
 
@@ -847,11 +619,7 @@
      *         chosen
      */
     public boolean isSplitSelectActive() {
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            return mSplitSelectDataHolder.isSplitSelectActive();
-        } else {
-            return isInitialTaskIntentSet() && !isSecondTaskIntentSet();
-        }
+        return mSplitSelectDataHolder.isSplitSelectActive();
     }
 
     /**
@@ -859,36 +627,15 @@
      *          be launched
      */
     public boolean isBothSplitAppsConfirmed() {
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            return mSplitSelectDataHolder.isBothSplitAppsConfirmed();
-        } else {
-            return isInitialTaskIntentSet() && isSecondTaskIntentSet();
-        }
-    }
-
-    private boolean isInitialTaskIntentSet() {
-        return (mInitialTaskId != INVALID_TASK_ID || mInitialTaskIntent != null);
+        return mSplitSelectDataHolder.isBothSplitAppsConfirmed();
     }
 
     public int getInitialTaskId() {
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            return mSplitSelectDataHolder.getInitialTaskId();
-        } else {
-            return mInitialTaskId;
-        }
+        return mSplitSelectDataHolder.getInitialTaskId();
     }
 
     public int getSecondTaskId() {
-        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
-            return mSplitSelectDataHolder.getSecondTaskId();
-        } else {
-            return mSecondTaskId;
-        }
-    }
-
-    private boolean isSecondTaskIntentSet() {
-        return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null
-                || mSecondPendingIntent != null);
+        return mSplitSelectDataHolder.getSecondTaskId();
     }
 
     public void setFirstFloatingTaskView(FloatingTaskView floatingTaskView) {
diff --git a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
index 5f7d694..c82cdb7 100644
--- a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
+++ b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
@@ -24,7 +24,6 @@
 import android.view.WindowMetrics;
 
 import com.android.internal.policy.SystemBarUtils;
-import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.CachedDisplayInfo;
 import com.android.launcher3.util.window.WindowManagerProxy;
@@ -61,7 +60,6 @@
         WindowManager windowManager = displayInfoContext.getSystemService(WindowManager.class);
         Set<WindowMetrics> possibleMaximumWindowMetrics =
                 windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY);
-        FileLog.d("b/283944974", "possibleMaximumWindowMetrics: " + possibleMaximumWindowMetrics);
         for (WindowMetrics windowMetrics : possibleMaximumWindowMetrics) {
             CachedDisplayInfo info = getDisplayInfo(windowMetrics, Surface.ROTATION_0);
             List<WindowBounds> bounds = estimateWindowBounds(displayInfoContext, info);
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 6f16f41..a21bbe1 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1369,9 +1369,15 @@
             return null;
         }
 
+        // We're looking for a taskView that matches these ids, regardless of order
+        int[] taskIdsCopy = Arrays.copyOf(taskIds, taskIds.length);
+        Arrays.sort(taskIdsCopy);
+
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView taskView = requireTaskViewAt(i);
-            if (Arrays.equals(taskIds, taskView.getTaskIds())) {
+            int[] taskViewIdsCopy = taskView.getTaskIds();
+            Arrays.sort(taskViewIdsCopy);
+            if (Arrays.equals(taskIdsCopy, taskViewIdsCopy)) {
                 return taskView;
             }
         }
@@ -5233,7 +5239,8 @@
                     true /* forDesktop */);
             mRemoteTargetHandles = gluer.assignTargetsForDesktop(recentsAnimationTargets);
         } else {
-            gluer = new RemoteTargetGluer(getContext(), getSizeStrategy());
+            gluer = new RemoteTargetGluer(getContext(), getSizeStrategy(), recentsAnimationTargets,
+                    false);
             mRemoteTargetHandles = gluer.assignTargetsForSplitScreen(recentsAnimationTargets);
         }
         mSplitBoundsConfig = gluer.getSplitBounds();
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index 0d9e412..4ca02e0 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -17,15 +17,21 @@
 package com.android.quickstep.views;
 
 import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
+import android.view.MotionEvent;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 
 import androidx.annotation.Nullable;
 import androidx.appcompat.widget.AppCompatTextView;
 
+import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StatefulActivity;
 
 /**
@@ -65,7 +71,7 @@
         mLauncher = (StatefulActivity) context;
     }
 
-    static SplitInstructionsView getSplitInstructionsView(StatefulActivity launcher) {
+    public static SplitInstructionsView getSplitInstructionsView(StatefulActivity launcher) {
         ViewGroup dragLayer = launcher.getDragLayer();
         final SplitInstructionsView splitInstructionsView =
                 (SplitInstructionsView) launcher.getLayoutInflater().inflate(
@@ -73,9 +79,7 @@
                         dragLayer,
                         false
                 );
-
-        splitInstructionsView.mTextView = splitInstructionsView.findViewById(
-                R.id.split_instructions_text);
+        splitInstructionsView.init();
 
         // Since textview overlays base view, and we sometimes manipulate the alpha of each
         // simultaneously, force overlapping rendering to false prevents redrawing of pixels,
@@ -92,6 +96,71 @@
         ensureProperRotation();
     }
 
+    private void init() {
+        mTextView = findViewById(R.id.split_instructions_text);
+
+        if (!FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+            mTextView.setCompoundDrawables(null, null, null, null);
+            return;
+        }
+
+        mTextView.setOnTouchListener((v, event) -> {
+            if (isTouchInsideRightCompoundDrawable(event)) {
+                if (event.getAction() == MotionEvent.ACTION_UP) {
+                    exitSplitSelection();
+                }
+                return true;
+            }
+            return false;
+        });
+    }
+
+    private void exitSplitSelection() {
+        ((RecentsView) mLauncher.getOverviewPanel()).getSplitSelectController()
+                .getSplitAnimationController().playPlaceholderDismissAnim(mLauncher);
+        mLauncher.getStateManager().goToState(LauncherState.NORMAL);
+    }
+
+    private boolean isTouchInsideRightCompoundDrawable(MotionEvent event) {
+        // Get the right compound drawable of the TextView.
+        Drawable rightDrawable = mTextView.getCompoundDrawablesRelative()[2];
+
+        // Check if the touch event intersects with the drawable's bounds.
+        if (rightDrawable != null) {
+            // We can get away w/o caring about the Y bounds since it's such a small view, if it's
+            // above/below the drawable just assume they meant to touch it. ¯\_(ツ)_/¯
+            return  event.getX() >= (mTextView.getWidth() - rightDrawable.getBounds().width());
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        if (!FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+            return;
+        }
+
+        info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+                R.string.toast_split_select_cont_desc,
+                getResources().getString(R.string.toast_split_select_cont_desc)
+        ));
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle arguments) {
+        if (!FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+            return super.performAccessibilityAction(action, arguments);
+        }
+
+        if (action == R.string.toast_split_select_cont_desc) {
+            exitSplitSelection();
+            return true;
+        }
+        return super.performAccessibilityAction(action, arguments);
+    }
+
     void ensureProperRotation() {
         ((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler()
                 .setSplitInstructionsParams(
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 5301c7c..6b0843c 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -97,7 +97,6 @@
 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
 import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.TaskIconCache;
-import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.TaskViewUtils;
@@ -413,7 +412,9 @@
 
     private boolean mIsClickableAsLiveTile = true;
 
-    @Nullable private final BorderAnimator mBorderAnimator;
+    @Nullable private final BorderAnimator mFocusBorderAnimator;
+
+    @Nullable private final BorderAnimator mHoverBorderAnimator;
 
     public TaskView(Context context) {
         this(context, null);
@@ -439,23 +440,40 @@
         boolean keyboardFocusHighlightEnabled = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
                 || DesktopTaskView.DESKTOP_MODE_SUPPORTED;
 
-        setWillNotDraw(!keyboardFocusHighlightEnabled);
+        boolean willDrawBorder =
+                keyboardFocusHighlightEnabled || FeatureFlags.ENABLE_CURSOR_HOVER_STATES.get();
+        setWillNotDraw(!willDrawBorder);
 
-        TypedArray ta = context.obtainStyledAttributes(
-                attrs, R.styleable.TaskView, defStyleAttr, defStyleRes);
+        if (willDrawBorder) {
+            TypedArray styledAttrs = context.obtainStyledAttributes(
+                    attrs, R.styleable.TaskView, defStyleAttr, defStyleRes);
 
-        mBorderAnimator = !keyboardFocusHighlightEnabled
-                ? null
-                : new BorderAnimator(
-                        /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
-                        /* borderColor= */ ta.getColor(
-                                R.styleable.TaskView_borderColor, DEFAULT_BORDER_COLOR),
-                        /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
-                                /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
-                                        R.dimen.keyboard_quick_switch_border_width),
-                                /* boundsBuilder= */ this::updateBorderBounds,
-                                /* targetView= */ this));
-        ta.recycle();
+            mFocusBorderAnimator = keyboardFocusHighlightEnabled ? new BorderAnimator(
+                    /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
+                    /* borderColor= */ styledAttrs.getColor(
+                            R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR),
+                    /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
+                            /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
+                                    R.dimen.keyboard_quick_switch_border_width),
+                            /* boundsBuilder= */ this::updateBorderBounds,
+                            /* targetView= */ this)) : null;
+
+            mHoverBorderAnimator =
+                    FeatureFlags.ENABLE_CURSOR_HOVER_STATES.get() ? new BorderAnimator(
+                            /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
+                            /* borderColor= */ styledAttrs.getColor(
+                                    R.styleable.TaskView_hoverBorderColor, DEFAULT_BORDER_COLOR),
+                            /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
+                                    /* borderWidthPx= */ context.getResources()
+                                            .getDimensionPixelSize(R.dimen.task_hover_border_width),
+                                    /* boundsBuilder= */ this::updateBorderBounds,
+                                    /* targetView= */ this)) : null;
+
+            styledAttrs.recycle();
+        } else {
+            mFocusBorderAnimator = null;
+            mHoverBorderAnimator = null;
+        }
     }
 
     protected void updateBorderBounds(Rect bounds) {
@@ -509,16 +527,48 @@
     @Override
     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
-        if (mBorderAnimator != null) {
-            mBorderAnimator.buildAnimator(gainFocus).start();
+        if (mFocusBorderAnimator != null) {
+            mFocusBorderAnimator.buildAnimator(gainFocus).start();
+        }
+    }
+
+    @Override
+    public boolean onHoverEvent(MotionEvent event) {
+        if (FeatureFlags.ENABLE_CURSOR_HOVER_STATES.get()) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_HOVER_ENTER:
+                    mHoverBorderAnimator.buildAnimator(/* isAppearing= */ true).start();
+                    break;
+                case MotionEvent.ACTION_HOVER_EXIT:
+                    mHoverBorderAnimator.buildAnimator(/* isAppearing= */ false).start();
+                    break;
+                default:
+                    break;
+            }
+        }
+        return super.onHoverEvent(event);
+    }
+
+    @Override
+    public boolean onInterceptHoverEvent(MotionEvent event) {
+        if (FeatureFlags.ENABLE_CURSOR_HOVER_STATES.get()) {
+            // avoid triggering hover event on child elements which would cause HOVER_EXIT for this
+            // task view
+            return true;
+        } else {
+            return super.onInterceptHoverEvent(event);
         }
     }
 
     @Override
     public void draw(Canvas canvas) {
         super.draw(canvas);
-        if (mBorderAnimator != null) {
-            mBorderAnimator.drawBorder(canvas);
+        if (mFocusBorderAnimator != null) {
+            mFocusBorderAnimator.drawBorder(canvas);
+        }
+
+        if (mHoverBorderAnimator != null) {
+            mHoverBorderAnimator.drawBorder(canvas);
         }
     }
 
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
new file mode 100644
index 0000000..82849be
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Display;
+import android.view.MotionEvent;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.folder.Folder;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.util.ActivityContextWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+/**
+ * Tests for TaskbarHoverToolTipController.
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase {
+
+    private TaskbarHoverToolTipController mTaskbarHoverToolTipController;
+    private TestableLooper mTestableLooper;
+
+    @Mock private TaskbarView mTaskbarView;
+    @Mock private MotionEvent mMotionEvent;
+    @Mock private BubbleTextView mHoverBubbleTextView;
+    @Mock private FolderIcon mHoverFolderIcon;
+    @Mock private Display mDisplay;
+    @Mock private TaskbarDragLayer mTaskbarDragLayer;
+    private Folder mSpyFolderView;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        Context context = getApplicationContext();
+
+        doAnswer((Answer<Object>) invocation -> context.getSystemService(
+                (String) invocation.getArgument(0)))
+                .when(taskbarActivityContext).getSystemService(anyString());
+        when(taskbarActivityContext.getResources()).thenReturn(context.getResources());
+        when(taskbarActivityContext.getApplicationInfo()).thenReturn(
+                context.getApplicationInfo());
+        when(taskbarActivityContext.getDragLayer()).thenReturn(mTaskbarDragLayer);
+        when(taskbarActivityContext.getMainLooper()).thenReturn(context.getMainLooper());
+        when(taskbarActivityContext.getDisplay()).thenReturn(mDisplay);
+
+        when(mTaskbarDragLayer.getChildCount()).thenReturn(1);
+        mSpyFolderView = spy(new Folder(new ActivityContextWrapper(context), null));
+        when(mTaskbarDragLayer.getChildAt(anyInt())).thenReturn(mSpyFolderView);
+        doReturn(false).when(mSpyFolderView).isOpen();
+
+        when(mHoverBubbleTextView.getText()).thenReturn("tooltip");
+        doAnswer((Answer<Void>) invocation -> {
+            Object[] args = invocation.getArguments();
+            ((int[]) args[0])[0] = 0;
+            ((int[]) args[0])[1] = 0;
+            return null;
+        }).when(mHoverBubbleTextView).getLocationOnScreen(any(int[].class));
+        when(mHoverBubbleTextView.getWidth()).thenReturn(100);
+        when(mHoverBubbleTextView.getHeight()).thenReturn(100);
+
+        mHoverFolderIcon.mInfo = new FolderInfo();
+        mHoverFolderIcon.mInfo.title = "tooltip";
+        doAnswer((Answer<Void>) invocation -> {
+            Object[] args = invocation.getArguments();
+            ((int[]) args[0])[0] = 0;
+            ((int[]) args[0])[1] = 0;
+            return null;
+        }).when(mHoverFolderIcon).getLocationOnScreen(any(int[].class));
+        when(mHoverFolderIcon.getWidth()).thenReturn(100);
+        when(mHoverFolderIcon.getHeight()).thenReturn(100);
+
+        when(mTaskbarView.getTop()).thenReturn(200);
+
+        mTaskbarHoverToolTipController = new TaskbarHoverToolTipController(
+                taskbarActivityContext, mTaskbarView, mHoverBubbleTextView);
+        mTestableLooper = TestableLooper.get(this);
+    }
+
+    @Test
+    public void onHover_hoverEnterIcon_revealToolTip() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
+        waitForIdleSync();
+
+        assertThat(hoverHandled).isTrue();
+        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+                true);
+        verify(taskbarActivityContext).setTaskbarWindowFullscreen(true);
+    }
+
+    @Test
+    public void onHover_hoverExitIcon_closeToolTip() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
+        waitForIdleSync();
+
+        assertThat(hoverHandled).isTrue();
+        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+                false);
+    }
+
+    @Test
+    public void onHover_hoverEnterFolderIcon_revealToolTip() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
+        waitForIdleSync();
+
+        assertThat(hoverHandled).isTrue();
+        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+                true);
+        verify(taskbarActivityContext).setTaskbarWindowFullscreen(true);
+    }
+
+    @Test
+    public void onHover_hoverExitFolderIcon_closeToolTip() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
+        waitForIdleSync();
+
+        assertThat(hoverHandled).isTrue();
+        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+                false);
+    }
+
+    @Test
+    public void onHover_hoverExitFolderOpen_closeToolTip() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+        doReturn(true).when(mSpyFolderView).isOpen();
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
+        waitForIdleSync();
+
+        assertThat(hoverHandled).isTrue();
+        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+                false);
+    }
+
+    @Test
+    public void onHover_hoverEnterFolderOpen_noToolTip() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+        doReturn(true).when(mSpyFolderView).isOpen();
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
+
+        assertThat(hoverHandled).isFalse();
+    }
+
+    @Test
+    public void onHover_hoverMove_noUpdate() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_MOVE);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_MOVE);
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
+
+        assertThat(hoverHandled).isFalse();
+    }
+
+    private void waitForIdleSync() {
+        mTestableLooper.processAllMessages();
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index a67d787..bc90db0 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -116,8 +116,7 @@
             Utilities.enableRunningInTestHarnessForTests();
         }
 
-        final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(
-                RecentsActivity.ACTIVITY_TRACKER::getCreatedActivity);
+        final ViewCaptureRule viewCaptureRule = new ViewCaptureRule();
         mOrderSensitiveRules = RuleChain
                 .outerRule(new SamplerRule())
                 .around(new NavigationModeSwitchRule(mLauncher))
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 7350214..bb246c2 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep;
 
+import static com.android.launcher3.testing.shared.TestProtocol.FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP;
 import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT;
 
@@ -27,6 +28,7 @@
 
 import android.content.Intent;
 import android.platform.test.annotations.PlatinumTest;
+import android.util.Log;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -44,6 +46,7 @@
 import com.android.launcher3.tapl.OverviewTask;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.ui.TaplTestsLauncher3;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
@@ -144,7 +147,7 @@
         assertNotNull("overview.getCurrentTask() returned null (1)", task);
         assertNotNull("OverviewTask.open returned null", task.open());
         assertTrue("Test activity didn't open from Overview", mDevice.wait(Until.hasObject(
-                By.pkg(getAppPackageName()).text("TestActivity2")),
+                        By.pkg(getAppPackageName()).text("TestActivity2")),
                 DEFAULT_UI_TIMEOUT));
         executeOnLauncher(launcher -> assertTrue(
                 "Launcher activity is the top activity; expecting another activity to be the top "
@@ -313,7 +316,6 @@
     @Test
     @ScreenRecord // b/242163205
     @PlatinumTest(focusArea = "launcher")
-    @TaskbarModeSwitch(mode = PERSISTENT)
     public void testQuickSwitchToPreviousAppForTablet() throws Exception {
         assumeTrue(mLauncher.isTablet());
         startTestActivity(2);
@@ -331,9 +333,18 @@
 
         assertTrue("The first app we should have quick switched to is not running",
                 isTestActivityRunning(2));
-        // Expect task bar visible when the launched app was the test activity.
+
         launchedAppState = getAndAssertLaunchedApp();
-        launchedAppState.assertTaskbarVisible();
+
+        Log.e(FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP,
+                "is Taskbar Transient : " + DisplayController.isTransientTaskbar(mTargetContext));
+        // TODO(b/286084688): Remove this branching check after test corruption is resolved.
+        // Branching this check because of test corruption.
+        if (DisplayController.isTransientTaskbar(mTargetContext)) {
+            launchedAppState.assertTaskbarHidden();
+        } else {
+            launchedAppState.assertTaskbarVisible();
+        }
     }
 
     private boolean isTestActivityRunning(int activityNumber) {
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
index 4c6874e..7ae3f29 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
@@ -16,12 +16,19 @@
 
 package com.android.quickstep;
 
+import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON;
+
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
+import android.app.Instrumentation;
+
 import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.tapl.LauncherInstrumentation.TrackpadGestureType;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.ui.TaplTestsLauncher3;
@@ -36,6 +43,9 @@
 @RunWith(AndroidJUnit4.class)
 public class TaplTestsTrackpad extends AbstractQuickStepTest {
 
+    private static final String READ_DEVICE_CONFIG_PERMISSION =
+            "android.permission.READ_DEVICE_CONFIG";
+
     @Before
     public void setUp() throws Exception {
         super.setUp();
@@ -60,6 +70,29 @@
 
     @Test
     @PortraitLandscape
+    // TODO(b/291944684): Support back in 3-button mode. It requires triggering the logic to enable
+    //  trackpad gesture back in SysUI. Normally it's triggered by the attachment of a trackpad. We
+    //  need to figure out a way to emulate that in the test, or bypass the logic altogether.
+    @NavigationModeSwitch(mode = ZERO_BUTTON)
+    public void pressBack() throws Exception {
+        assumeTrue(mLauncher.isTablet());
+        assumeFalse(FeatureFlags.ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION.get());
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+
+        try {
+            instrumentation.getUiAutomation().adoptShellPermissionIdentity(
+                    READ_DEVICE_CONFIG_PERMISSION);
+            mLauncher.setTrackpadGestureType(TrackpadGestureType.THREE_FINGER);
+
+            startTestActivity(2);
+            mLauncher.pressBack();
+        } finally {
+            instrumentation.getUiAutomation().dropShellPermissionIdentity();
+        }
+    }
+
+    @Test
+    @PortraitLandscape
     @NavigationModeSwitch
     public void switchToOverview() throws Exception {
         assumeTrue(mLauncher.isTablet());
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
index b58fe29..3869bf7 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
@@ -18,7 +18,6 @@
 import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT;
 
-
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -64,4 +63,15 @@
             throw new RuntimeException(e);
         }
     }
+
+    @Test
+    @TaskbarModeSwitch(mode = TRANSIENT)
+    public void testClickHoveredTaskbarToGoHome() {
+        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_CURSOR_HOVER_STATES, true)) {
+            getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE);
+            mLauncher.getLaunchedAppState().clickStashedTaskbarToGoHome();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
diff --git a/res/drawable/arrow_toast_rounded_background.xml b/res/drawable/arrow_toast_rounded_background.xml
index 1206ddd..d7d6255 100644
--- a/res/drawable/arrow_toast_rounded_background.xml
+++ b/res/drawable/arrow_toast_rounded_background.xml
@@ -14,6 +14,6 @@
     limitations under the License.
 -->
 <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
-    <solid android:color="@color/arrow_tip_view_bg" />
+    <solid android:color="?attr/arrowTipBackground" />
     <corners android:radius="@dimen/dialogCornerRadius" />
 </shape>
diff --git a/res/drawable/ic_split_exit.xml b/res/drawable/ic_split_exit.xml
new file mode 100644
index 0000000..d7e8b03
--- /dev/null
+++ b/res/drawable/ic_split_exit.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="20dp"
+    android:tint="#000000"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="20dp">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
\ No newline at end of file
diff --git a/res/layout/arrow_toast.xml b/res/layout/arrow_toast.xml
index 88a92eb..004e778 100644
--- a/res/layout/arrow_toast.xml
+++ b/res/layout/arrow_toast.xml
@@ -28,7 +28,7 @@
         android:padding="16dp"
         android:background="@drawable/arrow_toast_rounded_background"
         android:elevation="@dimen/arrow_toast_elevation"
-        android:textColor="@color/arrow_tip_view_content"
+        android:textColor="?attr/arrowTipTextColor"
         android:textSize="14sp"/>
 
     <View
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 5113da8..134cbb0 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -47,7 +47,7 @@
     <string name="widgets_full_sheet_cancel_button_description" msgid="5766167035728653605">"Ryd teksten i søgefeltet"</string>
     <string name="no_widgets_available" msgid="4337693382501046170">"Der er ingen tilgængelige widgets eller genveje"</string>
     <string name="no_search_results" msgid="3787956167293097509">"Der blev ikke fundet nogen widgets eller genveje"</string>
-    <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"Personlige"</string>
+    <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"Personlig"</string>
     <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"Arbejde"</string>
     <string name="widget_category_conversations" msgid="8894438636213590446">"Samtaler"</string>
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Notetagning"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index ee9280b..7f5c39f 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -56,7 +56,7 @@
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"વિજેટના સેટિંગ બદલવા માટે ટૅપ કરો"</string>
     <string name="widget_education_close_button" msgid="8676165703104836580">"સમજાઈ ગયું"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"વિજેટના સેટિંગ બદલો"</string>
-    <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"શોધ ઍપ્લિકેશનો"</string>
+    <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ઍપ શોધો"</string>
     <string name="all_apps_loading_message" msgid="5813968043155271636">"ઍપ્લિકેશનો લોડ કરી રહ્યું છે…"</string>
     <string name="all_apps_no_search_results" msgid="3200346862396363786">"\"<xliff:g id="QUERY">%1$s</xliff:g>\"થી મેળ ખાતી કોઈ ઍપ્લિકેશનો મળી નથી"</string>
     <string name="label_application" msgid="8531721983832654978">"ઍપ"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 6866a8e..7c718e59 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -56,7 +56,7 @@
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"विजेट की सेटिंग में बदलाव करने के लिए टैप करें"</string>
     <string name="widget_education_close_button" msgid="8676165703104836580">"ठीक है"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"विजेट की सेटिंग में बदलाव करें"</string>
-    <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ऐप सर्च करें"</string>
+    <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ऐप्लिकेशन खोजें"</string>
     <string name="all_apps_loading_message" msgid="5813968043155271636">"ऐप्लिकेशन लोड हो रहे हैं…"</string>
     <string name="all_apps_no_search_results" msgid="3200346862396363786">"\"<xliff:g id="QUERY">%1$s</xliff:g>\" से मिलता-जुलता कोई ऐप्लिकेशन नहीं मिला"</string>
     <string name="label_application" msgid="8531721983832654978">"ऐप्लिकेशन"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 957cc4f..72ab9b2 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -56,7 +56,7 @@
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ವಿಜೆಟ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
     <string name="widget_education_close_button" msgid="8676165703104836580">"ಅರ್ಥವಾಯಿತು"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ವಿಜೆಟ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಿ"</string>
-    <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ಅಪ್ಲಿಕೇಶನ್‌ಗಳನ್ನು ಹುಡುಕಿ"</string>
+    <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ಆ್ಯಪ್‍ಗಳನ್ನು ಹುಡುಕಿ"</string>
     <string name="all_apps_loading_message" msgid="5813968043155271636">"ಅಪ್ಲಿಕೇಶನ್‌ಗಳನ್ನು ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ..."</string>
     <string name="all_apps_no_search_results" msgid="3200346862396363786">"\"<xliff:g id="QUERY">%1$s</xliff:g>\" ಹೊಂದಿಕೆಯ ಯಾವುದೇ ಅಪ್ಲಿಕೇಶನ್‌ಗಳು ಕಂಡುಬಂದಿಲ್ಲ"</string>
     <string name="label_application" msgid="8531721983832654978">"ಆ್ಯಪ್"</string>
@@ -109,7 +109,7 @@
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ಆನ್ ಆಗಿದೆ"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ಆಫ್ ಆಗಿದೆ"</string>
     <string name="title_missing_notification_access" msgid="7503287056163941064">"ನೋಟಿಫಿಕೇಶನ್ ಆ್ಯಕ್ಸೆಸ್ ಅಗತ್ಯವಿದೆ"</string>
-    <string name="msg_missing_notification_access" msgid="281113995110910548">"ಅಧಿಸೂಚನೆ ಚುಕ್ಕೆಗಳನ್ನು ತೋರಿಸಲು, <xliff:g id="NAME">%1$s</xliff:g> ಗೆ ಅಪ್ಲಿಕೇಶನ್‌ ಅಧಿಸೂಚನೆಗಳನ್ನು ಆನ್‌ ಮಾಡಿ"</string>
+    <string name="msg_missing_notification_access" msgid="281113995110910548">"ನೋಟಿಫಿಕೇಶನ್ ಚುಕ್ಕೆಗಳನ್ನು ತೋರಿಸಲು, <xliff:g id="NAME">%1$s</xliff:g> ಗೆ ಆ್ಯಪ್ ನೋಟಿಫಿಕೇಶನ್‍ಗಳನ್ನು ಆನ್‌ ಮಾಡಿ"</string>
     <string name="title_change_settings" msgid="1376365968844349552">"ಸೆಟ್ಟಿಂಗ್‌‌ಗಳನ್ನು ಬದಲಾಯಿಸಿ"</string>
     <string name="notification_dots_service_title" msgid="4284221181793592871">"ನೋಟಿಫಿಕೇಶನ್ ಡಾಟ್‌ಗಳನ್ನು ತೋರಿಸಿ"</string>
     <string name="developer_options_title" msgid="700788437593726194">"ಡೆವಲಪರ್ ಆಯ್ಕೆಗಳು"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 6aa8fb5..a73f2a3 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -56,7 +56,7 @@
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"विजेटका सेटिङ बदल्न ट्याप गर्नुहोस्"</string>
     <string name="widget_education_close_button" msgid="8676165703104836580">"बुझेँ"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"विजेटका सेटिङ बदल्नुहोस्"</string>
-    <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"खोजसम्बन्धी एपहरू"</string>
+    <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"एपहरू खोज्नुहोस्"</string>
     <string name="all_apps_loading_message" msgid="5813968043155271636">"एपहरू लोड गर्दै…"</string>
     <string name="all_apps_no_search_results" msgid="3200346862396363786">"\"<xliff:g id="QUERY">%1$s</xliff:g>\" सँग मिल्दो कुनै एप भेटिएन"</string>
     <string name="label_application" msgid="8531721983832654978">"एप"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 2493af3..45062fc 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -56,7 +56,7 @@
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ୱିଜେଟ ସେଟିଂସ ପରିବର୍ତ୍ତନ କରିବାକୁ ଟାପ କରନ୍ତୁ"</string>
     <string name="widget_education_close_button" msgid="8676165703104836580">"ବୁଝିଗଲି"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ୱିଜେଟ ସେଟିଂସ ପରିବର୍ତ୍ତନ କରନ୍ତୁ"</string>
-    <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ଆପ୍‌ ଖୋଜନ୍ତୁ"</string>
+    <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ଆପ ସର୍ଚ୍ଚ କରନ୍ତୁ"</string>
     <string name="all_apps_loading_message" msgid="5813968043155271636">"ଆପ୍‌ ଲୋଡ୍‌ ହେଉଛି..."</string>
     <string name="all_apps_no_search_results" msgid="3200346862396363786">"\"<xliff:g id="QUERY">%1$s</xliff:g>\" ସହିତ ମେଳ ହେଉଥିବା କୌଣସି ଆପ୍‌ ମିଳିଲା ନାହିଁ"</string>
     <string name="label_application" msgid="8531721983832654978">"ଆପ୍"</string>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 73e392d..4d75fb8 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -205,13 +205,16 @@
         <!-- File that contains the specs for the workspace.
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="workspaceSpecsId" format="reference" />
+        <attr name="workspaceSpecsTwoPanelId" format="reference" />
         <!-- File that contains the specs for all apps.
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="allAppsSpecsId" format="reference" />
+        <attr name="allAppsSpecsTwoPanelId" format="reference" />
 
         <!-- File that contains the specs for the workspace.
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="folderSpecsId" format="reference" />
+        <attr name="folderSpecsTwoPanelId" format="reference" />
 
         <!-- By default all categories are enabled -->
         <attr name="deviceCategory" format="integer">
@@ -252,7 +255,7 @@
     </declare-styleable>
 
     <!--  Responsive grids attributes  -->
-    <declare-styleable name="WorkspaceSpec">
+    <declare-styleable name="ResponsiveSpec">
         <attr name="specType" format="integer">
             <enum name="height" value="0" />
             <enum name="width" value="1" />
@@ -260,12 +263,9 @@
         <attr name="maxAvailableSize" format="dimension" />
     </declare-styleable>
 
-    <declare-styleable name="SizeSpec">
-        <attr name="fixedSize" format="dimension" />
-        <attr name="ofAvailableSpace" format="float" />
-        <attr name="ofRemainderSpace" format="float" />
-        <attr name="matchWorkspace" format="boolean" />
-        <attr name="maxSize" format="dimension" />
+    <declare-styleable name="WorkspaceSpec">
+        <attr name="specType" />
+        <attr name="maxAvailableSize" />
     </declare-styleable>
 
     <declare-styleable name="FolderSpec">
@@ -278,6 +278,14 @@
         <attr name="maxAvailableSize" />
     </declare-styleable>
 
+    <declare-styleable name="SizeSpec">
+        <attr name="fixedSize" format="dimension" />
+        <attr name="ofAvailableSpace" format="float" />
+        <attr name="ofRemainderSpace" format="float" />
+        <attr name="matchWorkspace" format="boolean" />
+        <attr name="maxSize" format="dimension" />
+    </declare-styleable>
+
     <declare-styleable name="ProfileDisplayOption">
         <attr name="name" />
         <attr name="minWidthDps" format="float" />
@@ -587,5 +595,6 @@
 
     <declare-styleable name="ArrowTipView">
         <attr name="arrowTipBackground" format="color" />
+        <attr name="arrowTipTextColor" format="color" />
     </declare-styleable>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 6796d4b..1079e00 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -411,6 +411,7 @@
     <dimen name="split_instructions_elevation">1dp</dimen>
     <dimen name="split_instructions_horizontal_padding">24dp</dimen>
     <dimen name="split_instructions_vertical_padding">12dp</dimen>
+    <dimen name="split_instructions_drawable_padding">10dp</dimen>
     <dimen name="split_instructions_bottom_margin_phone_landscape">24dp</dimen>
     <dimen name="split_instructions_bottom_margin_phone_portrait">60dp</dimen>
     
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 14454bd..1695c58 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -425,5 +425,6 @@
 
     <style name="ArrowTipStyle">
         <item name="arrowTipBackground">@color/arrow_tip_view_bg</item>
+        <item name="arrowTipTextColor">@color/arrow_tip_view_content</item>
     </style>
 </resources>
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 31f9bfe..b845c88 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -133,6 +133,8 @@
     public static final int TYPE_TASKBAR_OVERLAYS =
             TYPE_TASKBAR_ALL_APPS | TYPE_TASKBAR_EDUCATION_DIALOG;
 
+    public static final int TYPE_ALL_EXCEPT_ON_BOARD_POPUP = TYPE_ALL & ~TYPE_ON_BOARD_POPUP;
+
     protected boolean mIsOpen;
 
     public AbstractFloatingView(Context context, AttributeSet attrs) {
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 2356bcc..360e060 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -97,7 +97,7 @@
     public static final int DISPLAY_ALL_APPS = 1;
     private static final int DISPLAY_FOLDER = 2;
     protected static final int DISPLAY_TASKBAR = 5;
-    private static final int DISPLAY_SEARCH_RESULT = 6;
+    public static final int DISPLAY_SEARCH_RESULT = 6;
     private static final int DISPLAY_SEARCH_RESULT_SMALL = 7;
     public static final int DISPLAY_PREDICTION_ROW = 8;
 
@@ -632,6 +632,11 @@
         }
     }
 
+    @VisibleForTesting
+    public boolean getForceHideDot() {
+        return mForceHideDot;
+    }
+
     private boolean hasDot() {
         return mDotInfo != null;
     }
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index f3b5155..42372f1 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -56,15 +56,15 @@
 import com.android.launcher3.responsive.AllAppsSpecs;
 import com.android.launcher3.responsive.CalculatedAllAppsSpec;
 import com.android.launcher3.responsive.CalculatedFolderSpec;
+import com.android.launcher3.responsive.CalculatedWorkspaceSpec;
 import com.android.launcher3.responsive.FolderSpecs;
+import com.android.launcher3.responsive.WorkspaceSpecs;
 import com.android.launcher3.uioverrides.ApiWrapper;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.IconSizeSteps;
 import com.android.launcher3.util.ResourceHelper;
 import com.android.launcher3.util.WindowBounds;
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec;
-import com.android.launcher3.workspace.WorkspaceSpecs;
 
 import java.io.PrintWriter;
 import java.util.Locale;
@@ -115,13 +115,10 @@
 
     // Responsive grid
     private final boolean mIsResponsiveGrid;
-    private WorkspaceSpecs mWorkspaceSpecs;
     private CalculatedWorkspaceSpec mResponsiveWidthSpec;
     private CalculatedWorkspaceSpec mResponsiveHeightSpec;
-    private AllAppsSpecs mAllAppsSpecs;
     private CalculatedAllAppsSpec mAllAppsResponsiveWidthSpec;
     private CalculatedAllAppsSpec mAllAppsResponsiveHeightSpec;
-    private FolderSpecs mFolderSpecs;
     private CalculatedFolderSpec mResponsiveFolderWidthSpec;
     private CalculatedFolderSpec mResponsiveFolderHeightSpec;
 
@@ -545,29 +542,35 @@
         // Needs to be calculated after hotseatBarSizePx is correct,
         // for the available height to be correct
         if (mIsResponsiveGrid) {
-            mWorkspaceSpecs = new WorkspaceSpecs(new ResourceHelper(context, inv.workspaceSpecsId));
+            WorkspaceSpecs workspaceSpecs = WorkspaceSpecs.create(
+                    new ResourceHelper(context,
+                            isTwoPanels ? inv.workspaceSpecsTwoPanelId : inv.workspaceSpecsId));
             int availableResponsiveWidth =
                     availableWidthPx - (isVerticalBarLayout() ? hotseatBarSizePx : 0);
+            int numColumns = getPanelCount() * inv.numColumns;
             // don't use availableHeightPx because it subtracts bottom padding,
             // but the workspace go behind it
             int availableResponsiveHeight =
                     heightPx - mInsets.top - (isVerticalBarLayout() ? 0 : hotseatBarSizePx);
-            mResponsiveWidthSpec = mWorkspaceSpecs.getCalculatedWidthSpec(inv.numColumns,
+            mResponsiveWidthSpec = workspaceSpecs.getCalculatedWidthSpec(numColumns,
                     availableResponsiveWidth);
-            mResponsiveHeightSpec = mWorkspaceSpecs.getCalculatedHeightSpec(inv.numRows,
+            mResponsiveHeightSpec = workspaceSpecs.getCalculatedHeightSpec(inv.numRows,
                     availableResponsiveHeight);
 
-            mAllAppsSpecs = new AllAppsSpecs(new ResourceHelper(context, inv.allAppsSpecsId));
-            mAllAppsResponsiveWidthSpec = mAllAppsSpecs.getCalculatedWidthSpec(inv.numColumns,
+            AllAppsSpecs allAppsSpecs = AllAppsSpecs.create(
+                    new ResourceHelper(context,
+                            isTwoPanels ? inv.allAppsSpecsTwoPanelId : inv.allAppsSpecsId));
+            mAllAppsResponsiveWidthSpec = allAppsSpecs.getCalculatedWidthSpec(numColumns,
                     mResponsiveWidthSpec.getAvailableSpace(), mResponsiveWidthSpec);
-            mAllAppsResponsiveHeightSpec = mAllAppsSpecs.getCalculatedHeightSpec(inv.numRows,
-                    mResponsiveHeightSpec.getAvailableSpace(),
-                    mResponsiveHeightSpec);
+            mAllAppsResponsiveHeightSpec = allAppsSpecs.getCalculatedHeightSpec(inv.numRows,
+                    mResponsiveHeightSpec.getAvailableSpace(), mResponsiveHeightSpec);
 
-            mFolderSpecs = new FolderSpecs(new ResourceHelper(context, inv.folderSpecsId));
-            mResponsiveFolderWidthSpec = mFolderSpecs.getWidthSpec(inv.numFolderColumns,
+            FolderSpecs folderSpecs = FolderSpecs.create(
+                    new ResourceHelper(context,
+                            isTwoPanels ? inv.folderSpecsTwoPanelId : inv.folderSpecsId));
+            mResponsiveFolderWidthSpec = folderSpecs.getCalculatedWidthSpec(inv.numFolderColumns,
                     mResponsiveWidthSpec.getAvailableSpace(), mResponsiveWidthSpec);
-            mResponsiveFolderHeightSpec = mFolderSpecs.getHeightSpec(inv.numFolderRows,
+            mResponsiveFolderHeightSpec = folderSpecs.getCalculatedHeightSpec(inv.numFolderRows,
                     mResponsiveHeightSpec.getAvailableSpace(), mResponsiveHeightSpec);
         }
 
@@ -1206,7 +1209,6 @@
         allAppsStyle.recycle();
     }
 
-    // TODO(b/288075868): Resize the icon size to make sure it will fit inside the cell size
     private void updateAvailableFolderCellDimensions(Resources res) {
         updateFolderCellSize(1f, res);
 
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 4eaacdc..dac6120 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -180,9 +180,15 @@
     @XmlRes
     public int workspaceSpecsId = INVALID_RESOURCE_HANDLE;
     @XmlRes
+    public int workspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+    @XmlRes
     public int allAppsSpecsId = INVALID_RESOURCE_HANDLE;
     @XmlRes
+    public int allAppsSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+    @XmlRes
     public int folderSpecsId = INVALID_RESOURCE_HANDLE;
+    @XmlRes
+    public int folderSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
 
     public String dbFile;
     public int defaultLayoutId;
@@ -357,8 +363,11 @@
         isScalable = closestProfile.isScalable;
         devicePaddingId = closestProfile.devicePaddingId;
         workspaceSpecsId = closestProfile.mWorkspaceSpecsId;
+        workspaceSpecsTwoPanelId = closestProfile.mWorkspaceSpecsTwoPanelId;
         allAppsSpecsId = closestProfile.mAllAppsSpecsId;
+        allAppsSpecsTwoPanelId = closestProfile.mAllAppsSpecsTwoPanelId;
         folderSpecsId = closestProfile.mFolderSpecsId;
+        folderSpecsTwoPanelId = closestProfile.mFolderSpecsTwoPanelId;
         this.deviceType = deviceType;
 
         inlineNavButtonsEndSpacing = closestProfile.inlineNavButtonsEndSpacing;
@@ -805,8 +814,11 @@
         private final boolean isScalable;
         private final int devicePaddingId;
         private final int mWorkspaceSpecsId;
+        private final int mWorkspaceSpecsTwoPanelId;
         private final int mAllAppsSpecsId;
+        private final int mAllAppsSpecsTwoPanelId;
         private final int mFolderSpecsId;
+        private final int mFolderSpecsTwoPanelId;
 
         public GridOption(Context context, AttributeSet attrs) {
             TypedArray a = context.obtainStyledAttributes(
@@ -871,14 +883,26 @@
             if (FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE.get()) {
                 mWorkspaceSpecsId = a.getResourceId(
                         R.styleable.GridDisplayOption_workspaceSpecsId, INVALID_RESOURCE_HANDLE);
+                mWorkspaceSpecsTwoPanelId = a.getResourceId(
+                        R.styleable.GridDisplayOption_workspaceSpecsTwoPanelId,
+                        INVALID_RESOURCE_HANDLE);
                 mAllAppsSpecsId = a.getResourceId(
                         R.styleable.GridDisplayOption_allAppsSpecsId, INVALID_RESOURCE_HANDLE);
+                mAllAppsSpecsTwoPanelId = a.getResourceId(
+                        R.styleable.GridDisplayOption_allAppsSpecsTwoPanelId,
+                        INVALID_RESOURCE_HANDLE);
                 mFolderSpecsId = a.getResourceId(
                         R.styleable.GridDisplayOption_folderSpecsId, INVALID_RESOURCE_HANDLE);
+                mFolderSpecsTwoPanelId = a.getResourceId(
+                        R.styleable.GridDisplayOption_folderSpecsTwoPanelId,
+                        INVALID_RESOURCE_HANDLE);
             } else {
                 mWorkspaceSpecsId = INVALID_RESOURCE_HANDLE;
+                mWorkspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
                 mAllAppsSpecsId = INVALID_RESOURCE_HANDLE;
+                mAllAppsSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
                 mFolderSpecsId = INVALID_RESOURCE_HANDLE;
+                mFolderSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
             }
 
             int inlineForRotation = a.getInt(R.styleable.GridDisplayOption_inlineQsb,
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index d85c384..bfbd660 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -424,7 +424,6 @@
     @Override
     @TargetApi(Build.VERSION_CODES.S)
     protected void onCreate(Bundle savedInstanceState) {
-        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onCreate 1");
         mStartupLatencyLogger = createStartupLatencyLogger(
                 sIsNewProcess
                         ? LockedUserState.get(this).isUserUnlockedAtLauncherStartup()
@@ -583,7 +582,6 @@
         }
         setTitle(R.string.home_screen);
         mStartupLatencyLogger.logEnd(LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE);
-        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onCreate 2");
     }
 
     /**
@@ -1058,7 +1056,6 @@
 
     @Override
     protected void onStop() {
-        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onStop 1");
         super.onStop();
         if (mDeferOverlayCallbacks) {
             checkIfOverlayStillDeferred();
@@ -1070,12 +1067,10 @@
         mAppWidgetHolder.setActivityStarted(false);
         NotificationListener.removeNotificationsChangedListener(getPopupDataProvider());
         FloatingIconView.resetIconLoadResult();
-        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onStop 2");
     }
 
     @Override
     protected void onStart() {
-        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onStart 1");
         TraceHelper.INSTANCE.beginSection(ON_START_EVT);
         super.onStart();
         if (!mDeferOverlayCallbacks) {
@@ -1084,7 +1079,6 @@
 
         mAppWidgetHolder.setActivityStarted(true);
         TraceHelper.INSTANCE.endSection();
-        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onStart 2");
     }
 
     @Override
@@ -1255,7 +1249,6 @@
 
     @Override
     protected void onResume() {
-        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onResume 1");
         TraceHelper.INSTANCE.beginSection(ON_RESUME_EVT);
         super.onResume();
 
@@ -1267,12 +1260,10 @@
 
         DragView.removeAllViews(this);
         TraceHelper.INSTANCE.endSection();
-        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onResume 2");
     }
 
     @Override
     protected void onPause() {
-        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onPause 1");
         // Ensure that items added to Launcher are queued until Launcher returns
         ItemInstallQueue.INSTANCE.get(this).pauseModelPush(FLAG_ACTIVITY_PAUSED);
 
@@ -1285,7 +1276,6 @@
             mOverlayManager.onActivityPaused(this);
         }
         mAppWidgetHolder.setActivityResumed(false);
-        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onPause 2");
     }
 
     /**
@@ -1402,9 +1392,10 @@
      * @param info   The data structure describing the shortcut.
      * @return A View inflated from layoutResId.
      */
-    public View createShortcut(ViewGroup parent, WorkspaceItemInfo info) {
-        BubbleTextView favorite = (BubbleTextView) LayoutInflater.from(parent.getContext())
-                .inflate(R.layout.app_icon, parent, false);
+    public View createShortcut(@Nullable ViewGroup parent, WorkspaceItemInfo info) {
+        BubbleTextView favorite =
+                (BubbleTextView) LayoutInflater.from(parent != null ? parent.getContext() : this)
+                        .inflate(R.layout.app_icon, parent, false);
         favorite.applyFromWorkspaceItem(info);
         favorite.setOnClickListener(getItemOnClickListener());
         favorite.setOnFocusChangeListener(mFocusHandler);
@@ -1758,8 +1749,6 @@
 
     @Override
     protected void onSaveInstanceState(Bundle outState) {
-        TestProtocol.testLogD(
-                TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onSaveInstanceState 1");
         outState.putIntArray(RUNTIME_STATE_CURRENT_SCREEN_IDS,
                 mWorkspace.getCurrentPageScreenIds().getArray().toArray());
         outState.putInt(RUNTIME_STATE, mStateManager.getState().ordinal);
@@ -1791,13 +1780,10 @@
 
         super.onSaveInstanceState(outState);
         mOverlayManager.onActivitySaveInstanceState(this, outState);
-        TestProtocol.testLogD(
-                TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onSaveInstanceState 2");
     }
 
     @Override
     public void onDestroy() {
-        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onDestroy 1");
         super.onDestroy();
         ACTIVITY_TRACKER.onActivityDestroyed(this);
 
@@ -1820,7 +1806,6 @@
         LauncherAppState.getIDP(this).removeOnChangeListener(this);
 
         mOverlayManager.onActivityDestroyed(this);
-        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onDestroy 2");
     }
 
     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 5f3d27c..fd8f668 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -102,6 +102,9 @@
     private LoaderTask mLoaderTask;
     private boolean mIsLoaderTaskRunning;
 
+    // only allow this once per reboot to reload work apps
+    private boolean mShouldReloadWorkProfile = true;
+
     // Indicates whether the current model data is valid or not.
     // We start off with everything not loaded. After that, we assume that
     // our monitoring of the package manager provides all updates and we never
@@ -308,17 +311,19 @@
      */
     public void onUserEvent(UserHandle user, String action) {
         if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action)
+                && mShouldReloadWorkProfile) {
+            mShouldReloadWorkProfile = false;
+            forceReload();
+        } else if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action)
                 || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) {
+            mShouldReloadWorkProfile = false;
             enqueueModelUpdateTask(new PackageUpdatedTask(
                     PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user));
-        }
-
-        if (UserCache.ACTION_PROFILE_LOCKED.equals(action)
+        } else if (UserCache.ACTION_PROFILE_LOCKED.equals(action)
                 || UserCache.ACTION_PROFILE_UNLOCKED.equals(action)) {
             enqueueModelUpdateTask(new UserLockStateChangedTask(
                     user, UserCache.ACTION_PROFILE_UNLOCKED.equals(action)));
-        }
-        if (UserCache.ACTION_PROFILE_ADDED.equals(action)
+        } else if (UserCache.ACTION_PROFILE_ADDED.equals(action)
                 || UserCache.ACTION_PROFILE_REMOVED.equals(action)) {
             forceReload();
         }
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 4f5de05..e8c6ff9 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -334,14 +334,12 @@
 
     public static void scaleRectAboutCenter(Rect r, float scale) {
         if (scale != 1.0f) {
-            int cx = r.centerX();
-            int cy = r.centerY();
-            r.offset(-cx, -cy);
-            r.left = (int) (r.left * scale + 0.5f);
-            r.top = (int) (r.top * scale + 0.5f);
-            r.right = (int) (r.right * scale + 0.5f);
-            r.bottom = (int) (r.bottom * scale + 0.5f);
-            r.offset(cx, cy);
+            float cx = r.exactCenterX();
+            float cy = r.exactCenterY();
+            r.left = Math.round(cx + (r.left - cx) * scale);
+            r.top = Math.round(cy + (r.top - cy) * scale);
+            r.right = Math.round(cx + (r.right - cx) * scale);
+            r.bottom = Math.round(cy + (r.bottom - cy) * scale);
         }
     }
 
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index eb4ecaf..4c86bec 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -391,7 +391,10 @@
                         rebindAdapters(false);
                         mRebindAdaptersAfterSearchAnimation = false;
                     }
-                    if (!goingToSearch) {
+
+                    if (goingToSearch) {
+                        mSearchUiDelegate.onAnimateToSearchStateCompleted();
+                    } else {
                         setSearchResults(null);
                         if (mViewPager != null) {
                             mViewPager.setCurrentPage(previousPage);
@@ -585,6 +588,10 @@
         } else {
             mAH.get(AdapterHolder.MAIN).setup(findViewById(R.id.apps_list_view), null);
             mAH.get(AdapterHolder.WORK).mRecyclerView = null;
+            if (ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
+                mAH.get(AdapterHolder.MAIN).mRecyclerView
+                        .setRecycledViewPool(mAllAppsStore.getRecyclerViewPool());
+            }
         }
         setupHeader();
 
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionListener.java b/src/com/android/launcher3/allapps/AllAppsTransitionListener.java
new file mode 100644
index 0000000..4a17e29
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionListener.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+/**
+ * An interface for listening to all-apps open-close transition
+ */
+public interface AllAppsTransitionListener {
+    /**
+     * Called when the transition starts
+     * @param toAllApps {@code true} if this transition is supposed to end in the AppApps UI
+     *
+     * @see ActivityAllAppsContainerView
+     */
+    void onAllAppsTransitionStart(boolean toAllApps);
+
+    /**
+     * Called when the transition ends
+     * @param toAllApps {@code true} if the final state is all-apps
+     *
+     * @see ActivityAllAppsContainerView
+     */
+    void onAllAppsTransitionEnd(boolean toAllApps);
+}
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java b/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java
index 49cecca..2347bfd 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java
@@ -49,6 +49,11 @@
         // Do nothing.
     }
 
+    /** Invoked when transition animations to go to search is completed . */
+    public void onAnimateToSearchStateCompleted() {
+        // Do nothing
+    }
+
     /** Invoked when the search bar has been added to All Apps. */
     public void onInitializeSearchBar() {
         // Do nothing.
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 5426532..e3e1400 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -387,9 +387,6 @@
             "Use local overrides for search request timeout");
 
     // TODO(Block 31): Clean up flags
-    public static final BooleanFlag ENABLE_SPLIT_LAUNCH_DATA_REFACTOR = getDebugFlag(279494325,
-            "ENABLE_SPLIT_LAUNCH_DATA_REFACTOR", ENABLED,
-            "Use refactored split launching code path");
 
     // TODO(Block 32): Clean up flags
     public static final BooleanFlag ENABLE_RESPONSIVE_WORKSPACE = getDebugFlag(241386436,
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index b9bb52c..adfdc89 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -99,7 +99,9 @@
 
     // Whether mAnim has started. Unlike mAnim.isStarted(), this is true even after mAnim ends.
     private boolean mScaleAnimStarted;
-    private Runnable mOnAnimEndCallback = null;
+    private boolean mShiftAnimStarted;
+    private Runnable mOnScaleAnimEndCallback;
+    private Runnable mOnShiftAnimEndCallback;
 
     private int mLastTouchX;
     private int mLastTouchY;
@@ -186,13 +188,26 @@
             @Override
             public void onAnimationEnd(Animator animation) {
                 super.onAnimationEnd(animation);
-                if (mOnAnimEndCallback != null) {
-                    mOnAnimEndCallback.run();
+                if (mOnScaleAnimEndCallback != null) {
+                    mOnScaleAnimEndCallback.run();
                 }
             }
         });
         // Set up the shift animator.
         mShiftAnim = ValueAnimator.ofFloat(0f, 1f);
+        mShiftAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mShiftAnimStarted = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mOnShiftAnimEndCallback != null) {
+                    mOnShiftAnimEndCallback.run();
+                }
+            }
+        });
 
         setDragRegion(new Rect(0, 0, width, height));
 
@@ -211,8 +226,14 @@
         setWillNotDraw(false);
     }
 
-    public void setOnAnimationEndCallback(Runnable callback) {
-        mOnAnimEndCallback = callback;
+    /** Callback invoked when the scale animation ends. */
+    public void setOnScaleAnimEndCallback(Runnable callback) {
+        mOnScaleAnimEndCallback = callback;
+    }
+
+    /** Callback invoked when the shift animation ends. */
+    public void setOnShiftAnimEndCallback(Runnable callback) {
+        mOnShiftAnimEndCallback = callback;
     }
 
     /**
@@ -416,10 +437,16 @@
         }
     }
 
+    /** {@code true} if the scale animation has finished. */
     public boolean isScaleAnimationFinished() {
         return mScaleAnimStarted && !mScaleAnim.isRunning();
     }
 
+    /** {@code true} if the shift animation has finished. */
+    public boolean isShiftAnimationFinished() {
+        return mShiftAnimStarted && !mShiftAnim.isRunning();
+    }
+
     /**
      * Move the window containing this view.
      *
diff --git a/src/com/android/launcher3/folder/LauncherDelegate.java b/src/com/android/launcher3/folder/LauncherDelegate.java
index 7ac2472..c06a0f3 100644
--- a/src/com/android/launcher3/folder/LauncherDelegate.java
+++ b/src/com/android/launcher3/folder/LauncherDelegate.java
@@ -94,9 +94,6 @@
                         // folder
                         CellLayout cellLayout = mLauncher.getCellLayout(info.container,
                                 mLauncher.getCellPosMapper().mapModelToPresenter(info).screenId);
-                        if (cellLayout == null) {
-                            return;
-                        }
                         finalItem =  info.contents.remove(0);
                         newIcon = mLauncher.createShortcut(cellLayout, finalItem);
                         mLauncher.getModelWriter().addOrMoveItemInDatabase(finalItem,
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 3e9731382..2a7cd9a 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -32,12 +32,9 @@
 import com.android.launcher3.logger.LauncherAtom.FromState;
 import com.android.launcher3.logger.LauncherAtom.ToState;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.ResourceBasedOverride;
 import com.android.launcher3.views.ActivityContext;
 
-import java.util.List;
-
 /**
  * Handles the user event logging in R+.
  *
@@ -961,33 +958,32 @@
         }
 
         /**
-         * Sets list of {@link com.android.app.search.ResultType} for the impression event.
+         * Sets {@link com.android.app.search.ResultType} for the impression event.
          */
-        default StatsImpressionLogger withResultType(IntArray resultType) {
+        default StatsImpressionLogger withResultType(int resultType) {
             return this;
         }
 
         /**
-         * Sets list of count for each of {@link com.android.app.search.ResultType} for the
-         * impression event.
-         */
-        default StatsImpressionLogger withResultCount(IntArray resultCount) {
-            return this;
-        }
-
-        /**
-         * Sets list of boolean for each of {@link com.android.app.search.ResultType} that indicates
+         * Sets boolean for each of {@link com.android.app.search.ResultType} that indicates
          * if this result is above keyboard or not for the impression event.
          */
-        default StatsImpressionLogger withAboveKeyboard(List<Boolean> aboveKeyboard) {
+        default StatsImpressionLogger withAboveKeyboard(boolean aboveKeyboard) {
             return this;
         }
 
         /**
-         * Sets list of uid for each of {@link com.android.app.search.ResultType} that indicates
+         * Sets uid for each of {@link com.android.app.search.ResultType} that indicates
          * package name for the impression event.
          */
-        default StatsImpressionLogger withUids(IntArray uid) {
+        default StatsImpressionLogger withUid(int uid) {
+            return this;
+        }
+
+        /**
+         * Sets result source that indicates the origin of the result for the impression event.
+         */
+        default StatsImpressionLogger withResultSource(int resultSource) {
             return this;
         }
 
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index 1b1b38f..e10e72d 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -466,7 +466,7 @@
                 () -> parser, AutoInstallsLayout.TAG_WORKSPACE);
     }
 
-    private static Uri getLayoutUri(String authority, Context ctx) {
+    public static Uri getLayoutUri(String authority, Context ctx) {
         InvariantDeviceProfile grid = LauncherAppState.getIDP(ctx);
         return new Uri.Builder().scheme("content").authority(authority).path("launcher_layout")
                 .appendQueryParameter("version", "1")
diff --git a/src/com/android/launcher3/responsive/AllAppsSpecs.kt b/src/com/android/launcher3/responsive/AllAppsSpecs.kt
index 85e383e..8ed3ffc 100644
--- a/src/com/android/launcher3/responsive/AllAppsSpecs.kt
+++ b/src/com/android/launcher3/responsive/AllAppsSpecs.kt
@@ -16,277 +16,89 @@
 
 package com.android.launcher3.responsive
 
-import android.content.res.XmlResourceParser
-import android.util.AttributeSet
-import android.util.Log
-import android.util.Xml
+import android.content.res.TypedArray
 import com.android.launcher3.R
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
 import com.android.launcher3.util.ResourceHelper
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec
-import java.io.IOException
-import kotlin.math.roundToInt
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
 
-private const val LOG_TAG = "AllAppsSpecs"
+class AllAppsSpecs(widthSpecs: List<AllAppsSpec>, heightSpecs: List<AllAppsSpec>) :
+    ResponsiveSpecs<AllAppsSpec>(widthSpecs, heightSpecs) {
 
-class AllAppsSpecs(resourceHelper: ResourceHelper) {
-    object XmlTags {
-        const val ALL_APPS_SPECS = "allAppsSpecs"
-
-        const val ALL_APPS_SPEC = "allAppsSpec"
-        const val START_PADDING = "startPadding"
-        const val END_PADDING = "endPadding"
-        const val GUTTER = "gutter"
-        const val CELL_SIZE = "cellSize"
-    }
-
-    val allAppsHeightSpecList = mutableListOf<AllAppsSpec>()
-    val allAppsWidthSpecList = mutableListOf<AllAppsSpec>()
-
-    // TODO(b/286538013) Remove this init after a more generic or reusable parser is created
-    init {
-        var parser: XmlResourceParser? = null
-        try {
-            parser = 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.ALL_APPS_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.ALL_APPS_SPEC == parser.name
-                        ) {
-                            val attrs =
-                                resourceHelper.obtainStyledAttributes(
-                                    Xml.asAttributeSet(parser),
-                                    R.styleable.AllAppsSpec
-                                )
-                            val maxAvailableSize =
-                                attrs.getDimensionPixelSize(
-                                    R.styleable.AllAppsSpec_maxAvailableSize,
-                                    0
-                                )
-                            val specType =
-                                AllAppsSpec.SpecType.values()[
-                                        attrs.getInt(
-                                            R.styleable.AllAppsSpec_specType,
-                                            AllAppsSpec.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.create(resourceHelper, attr)
-                                        }
-                                        XmlTags.END_PADDING -> {
-                                            endPadding = SizeSpec.create(resourceHelper, attr)
-                                        }
-                                        XmlTags.GUTTER -> {
-                                            gutter = SizeSpec.create(resourceHelper, attr)
-                                        }
-                                        XmlTags.CELL_SIZE -> {
-                                            cellSize = SizeSpec.create(resourceHelper, attr)
-                                        }
-                                    }
-                                }
-                            }
-
-                            if (
-                                startPadding == null ||
-                                    endPadding == null ||
-                                    gutter == null ||
-                                    cellSize == null
-                            ) {
-                                throw IllegalStateException(
-                                    "All attributes in AllAppsSpec must be defined"
-                                )
-                            }
-
-                            val allAppsSpec =
-                                AllAppsSpec(
-                                    maxAvailableSize,
-                                    specType,
-                                    startPadding,
-                                    endPadding,
-                                    gutter,
-                                    cellSize
-                                )
-                            if (allAppsSpec.isValid()) {
-                                if (allAppsSpec.specType == AllAppsSpec.SpecType.HEIGHT)
-                                    allAppsHeightSpecList.add(allAppsSpec)
-                                else allAppsWidthSpecList.add(allAppsSpec)
-                            } else {
-                                throw IllegalStateException("Invalid AllAppsSpec found.")
-                            }
-                        }
-                    }
-
-                    if (allAppsWidthSpecList.isEmpty() || allAppsHeightSpecList.isEmpty()) {
-                        throw IllegalStateException(
-                            "AllAppsSpecs is incomplete - " +
-                                "height list size = ${allAppsHeightSpecList.size}; " +
-                                "width list size = ${allAppsWidthSpecList.size}."
-                        )
-                    }
-                }
-            }
-        } catch (e: Exception) {
-            when (e) {
-                is IOException,
-                is XmlPullParserException -> {
-                    throw RuntimeException("Failure parsing all apps specs file.", e)
-                }
-                else -> throw e
-            }
-        } finally {
-            parser?.close()
-        }
-    }
-
-    /**
-     * Returns the CalculatedAllAppsSpec for width, based on the available width, the AllAppsSpecs
-     * and the CalculatedWorkspaceSpec.
-     */
     fun getCalculatedWidthSpec(
         columns: Int,
         availableWidth: Int,
         calculatedWorkspaceSpec: CalculatedWorkspaceSpec
     ): CalculatedAllAppsSpec {
-        val widthSpec = allAppsWidthSpecList.first { availableWidth <= it.maxAvailableSize }
+        check(calculatedWorkspaceSpec.spec.specType == SpecType.WIDTH) {
+            "Invalid specType for CalculatedWorkspaceSpec. " +
+                "Expected: ${SpecType.WIDTH} - " +
+                "Found: ${calculatedWorkspaceSpec.spec.specType}}"
+        }
 
-        return CalculatedAllAppsSpec(availableWidth, columns, widthSpec, calculatedWorkspaceSpec)
+        val spec = getWidthSpec(availableWidth)
+        return CalculatedAllAppsSpec(availableWidth, columns, spec, calculatedWorkspaceSpec)
     }
 
-    /**
-     * Returns the CalculatedAllAppsSpec for height, based on the available height, the AllAppsSpecs
-     * and the CalculatedWorkspaceSpec.
-     */
     fun getCalculatedHeightSpec(
         rows: Int,
         availableHeight: Int,
         calculatedWorkspaceSpec: CalculatedWorkspaceSpec
     ): CalculatedAllAppsSpec {
-        val heightSpec = allAppsHeightSpecList.first { availableHeight <= it.maxAvailableSize }
+        check(calculatedWorkspaceSpec.spec.specType == SpecType.HEIGHT) {
+            "Invalid specType for CalculatedWorkspaceSpec. " +
+                "Expected: ${SpecType.HEIGHT} - " +
+                "Found: ${calculatedWorkspaceSpec.spec.specType}}"
+        }
 
-        return CalculatedAllAppsSpec(availableHeight, rows, heightSpec, calculatedWorkspaceSpec)
-    }
-}
-
-class CalculatedAllAppsSpec(
-    val availableSpace: Int,
-    val cells: Int,
-    private val allAppsSpec: AllAppsSpec,
-    calculatedWorkspaceSpec: CalculatedWorkspaceSpec
-) {
-    var startPaddingPx: Int = 0
-        private set
-    var endPaddingPx: Int = 0
-        private set
-    var gutterPx: Int = 0
-        private set
-    var cellSizePx: Int = 0
-        private set
-    init {
-        // Copy values from workspace
-        if (allAppsSpec.startPadding.matchWorkspace)
-            startPaddingPx = calculatedWorkspaceSpec.startPaddingPx
-        if (allAppsSpec.endPadding.matchWorkspace)
-            endPaddingPx = calculatedWorkspaceSpec.endPaddingPx
-        if (allAppsSpec.gutter.matchWorkspace) gutterPx = calculatedWorkspaceSpec.gutterPx
-        if (allAppsSpec.cellSize.matchWorkspace) cellSizePx = calculatedWorkspaceSpec.cellSizePx
-
-        // Calculate all fixed size first
-        if (allAppsSpec.startPadding.fixedSize > 0)
-            startPaddingPx = allAppsSpec.startPadding.fixedSize.roundToInt()
-        if (allAppsSpec.endPadding.fixedSize > 0)
-            endPaddingPx = allAppsSpec.endPadding.fixedSize.roundToInt()
-        if (allAppsSpec.gutter.fixedSize > 0) gutterPx = allAppsSpec.gutter.fixedSize.roundToInt()
-        if (allAppsSpec.cellSize.fixedSize > 0)
-            cellSizePx = allAppsSpec.cellSize.fixedSize.roundToInt()
-
-        // Calculate all available space next
-        if (allAppsSpec.startPadding.ofAvailableSpace > 0)
-            startPaddingPx =
-                (allAppsSpec.startPadding.ofAvailableSpace * availableSpace).roundToInt()
-        if (allAppsSpec.endPadding.ofAvailableSpace > 0)
-            endPaddingPx = (allAppsSpec.endPadding.ofAvailableSpace * availableSpace).roundToInt()
-        if (allAppsSpec.gutter.ofAvailableSpace > 0)
-            gutterPx = (allAppsSpec.gutter.ofAvailableSpace * availableSpace).roundToInt()
-        if (allAppsSpec.cellSize.ofAvailableSpace > 0)
-            cellSizePx = (allAppsSpec.cellSize.ofAvailableSpace * availableSpace).roundToInt()
-
-        // Calculate remainder space last
-        val gutters = cells - 1
-        val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
-        val remainderSpace = availableSpace - usedSpace
-        if (allAppsSpec.startPadding.ofRemainderSpace > 0)
-            startPaddingPx =
-                (allAppsSpec.startPadding.ofRemainderSpace * remainderSpace).roundToInt()
-        if (allAppsSpec.endPadding.ofRemainderSpace > 0)
-            endPaddingPx = (allAppsSpec.endPadding.ofRemainderSpace * remainderSpace).roundToInt()
-        if (allAppsSpec.gutter.ofRemainderSpace > 0)
-            gutterPx = (allAppsSpec.gutter.ofRemainderSpace * remainderSpace).roundToInt()
-        if (allAppsSpec.cellSize.ofRemainderSpace > 0)
-            cellSizePx = (allAppsSpec.cellSize.ofRemainderSpace * remainderSpace).roundToInt()
+        val spec = getHeightSpec(availableHeight)
+        return CalculatedAllAppsSpec(availableHeight, rows, spec, calculatedWorkspaceSpec)
     }
 
-    override fun toString(): String {
-        return "CalculatedAllAppsSpec(availableSpace=$availableSpace, " +
-            "cells=$cells, startPaddingPx=$startPaddingPx, endPaddingPx=$endPaddingPx, " +
-            "gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
-            "AllAppsSpec.maxAvailableSize=${allAppsSpec.maxAvailableSize})"
+    companion object {
+        private const val XML_ALL_APPS_SPEC = "allAppsSpec"
+
+        @JvmStatic
+        fun create(resourceHelper: ResourceHelper): AllAppsSpecs {
+            val parser = ResponsiveSpecsParser(resourceHelper)
+            val specs = parser.parseXML(XML_ALL_APPS_SPEC, ::AllAppsSpec)
+            val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+            return AllAppsSpecs(widthSpecs, heightSpecs)
+        }
     }
 }
 
 data class AllAppsSpec(
-    val maxAvailableSize: Int,
-    val specType: SpecType,
-    val startPadding: SizeSpec,
-    val endPadding: SizeSpec,
-    val gutter: SizeSpec,
-    val cellSize: SizeSpec
-) {
+    override val maxAvailableSize: Int,
+    override val specType: SpecType,
+    override val startPadding: SizeSpec,
+    override val endPadding: SizeSpec,
+    override val gutter: SizeSpec,
+    override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
 
-    enum class SpecType {
-        HEIGHT,
-        WIDTH
+    init {
+        check(isValid()) { "Invalid AllAppsSpec found." }
     }
 
-    fun isValid(): Boolean {
-        if (maxAvailableSize <= 0) {
-            Log.e(LOG_TAG, "AllAppsSpec#isValid - maxAvailableSize <= 0")
-            return false
-        }
-
-        // All specs need to be individually valid
-        if (!allSpecsAreValid()) {
-            Log.e(LOG_TAG, "AllAppsSpec#isValid - !allSpecsAreValid()")
-            return false
-        }
-
-        return true
-    }
-
-    private fun allSpecsAreValid(): Boolean =
-        startPadding.isValid() && endPadding.isValid() && gutter.isValid() && cellSize.isValid()
+    constructor(
+        attrs: TypedArray,
+        specs: Map<String, SizeSpec>
+    ) : this(
+        maxAvailableSize =
+            attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+        specType =
+            SpecType.values()[
+                    attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+        startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+        endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+        gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+        cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
+    )
 }
+
+class CalculatedAllAppsSpec(
+    availableSpace: Int,
+    cells: Int,
+    spec: AllAppsSpec,
+    calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+) : CalculatedResponsiveSpec(availableSpace, cells, spec, calculatedWorkspaceSpec)
diff --git a/src/com/android/launcher3/responsive/FolderSpecs.kt b/src/com/android/launcher3/responsive/FolderSpecs.kt
index f4446bc..bc2db28 100644
--- a/src/com/android/launcher3/responsive/FolderSpecs.kt
+++ b/src/com/android/launcher3/responsive/FolderSpecs.kt
@@ -1,280 +1,105 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.launcher3.responsive
 
-import android.content.res.XmlResourceParser
-import android.util.AttributeSet
-import android.util.Log
-import android.util.Xml
+import android.content.res.TypedArray
 import com.android.launcher3.R
-import com.android.launcher3.responsive.FolderSpec.*
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
 import com.android.launcher3.util.ResourceHelper
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec
-import com.android.launcher3.workspace.WorkspaceSpec
-import java.io.IOException
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
 
-private const val LOG_TAG = "FolderSpecs"
+class FolderSpecs(widthSpecs: List<FolderSpec>, heightSpecs: List<FolderSpec>) :
+    ResponsiveSpecs<FolderSpec>(widthSpecs, heightSpecs) {
 
-class FolderSpecs(resourceHelper: ResourceHelper) {
-
-    object XmlTags {
-        const val FOLDER_SPECS = "folderSpecs"
-
-        const val FOLDER_SPEC = "folderSpec"
-        const val START_PADDING = "startPadding"
-        const val END_PADDING = "endPadding"
-        const val GUTTER = "gutter"
-        const val CELL_SIZE = "cellSize"
-    }
-
-    private val _heightSpecs = mutableListOf<FolderSpec>()
-    val heightSpecs: List<FolderSpec>
-        get() = _heightSpecs
-
-    private val _widthSpecs = mutableListOf<FolderSpec>()
-    val widthSpecs: List<FolderSpec>
-        get() = _widthSpecs
-
-    // TODO(b/286538013) Remove this init after a more generic or reusable parser is created
-    init {
-        var parser: XmlResourceParser? = null
-        try {
-            parser = 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.FOLDER_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.FOLDER_SPEC == parser.name) {
-                            val attrs =
-                                resourceHelper.obtainStyledAttributes(
-                                    Xml.asAttributeSet(parser),
-                                    R.styleable.FolderSpec
-                                )
-                            val maxAvailableSize =
-                                attrs.getDimensionPixelSize(
-                                    R.styleable.FolderSpec_maxAvailableSize,
-                                    0
-                                )
-                            val specType =
-                                SpecType.values()[
-                                        attrs.getInt(
-                                            R.styleable.FolderSpec_specType,
-                                            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) {
-                                    val sizeSpec = SizeSpec.create(resourceHelper, attr)
-                                    when (parser.name) {
-                                        XmlTags.START_PADDING -> startPadding = sizeSpec
-                                        XmlTags.END_PADDING -> endPadding = sizeSpec
-                                        XmlTags.GUTTER -> gutter = sizeSpec
-                                        XmlTags.CELL_SIZE -> cellSize = sizeSpec
-                                    }
-                                }
-                            }
-
-                            checkNotNull(startPadding) {
-                                "Attr 'startPadding' in FolderSpec must be defined."
-                            }
-                            checkNotNull(endPadding) {
-                                "Attr 'endPadding' in FolderSpec must be defined."
-                            }
-                            checkNotNull(gutter) { "Attr 'gutter' in FolderSpec must be defined." }
-                            checkNotNull(cellSize) {
-                                "Attr 'cellSize' in FolderSpec must be defined."
-                            }
-
-                            val folderSpec =
-                                FolderSpec(
-                                    maxAvailableSize,
-                                    specType,
-                                    startPadding,
-                                    endPadding,
-                                    gutter,
-                                    cellSize
-                                )
-
-                            check(folderSpec.isValid()) { "Invalid FolderSpec found." }
-
-                            if (folderSpec.specType == SpecType.HEIGHT) {
-                                _heightSpecs += folderSpec
-                            } else {
-                                _widthSpecs += folderSpec
-                            }
-                        }
-                    }
-
-                    check(_widthSpecs.isNotEmpty() && _heightSpecs.isNotEmpty()) {
-                        "FolderSpecs is incomplete - " +
-                            "height list size = ${_heightSpecs.size}; " +
-                            "width list size = ${_widthSpecs.size}."
-                    }
-                }
-            }
-        } catch (e: Exception) {
-            when (e) {
-                is IOException,
-                is XmlPullParserException -> {
-                    throw RuntimeException("Failure parsing folder specs file.", e)
-                }
-                else -> throw e
-            }
-        } finally {
-            parser?.close()
-        }
-    }
-
-    /**
-     * Returns the [CalculatedFolderSpec] for width, based on the available width, FolderSpecs and
-     * WorkspaceSpecs.
-     */
-    fun getWidthSpec(
+    fun getCalculatedWidthSpec(
         columns: Int,
         availableWidth: Int,
-        workspaceSpec: CalculatedWorkspaceSpec
+        calculatedWorkspaceSpec: CalculatedWorkspaceSpec
     ): CalculatedFolderSpec {
-        check(workspaceSpec.workspaceSpec.specType == WorkspaceSpec.SpecType.WIDTH) {
+        check(calculatedWorkspaceSpec.spec.specType == SpecType.WIDTH) {
             "Invalid specType for CalculatedWorkspaceSpec. " +
-                "Expected: ${WorkspaceSpec.SpecType.WIDTH} - " +
-                "Found: ${workspaceSpec.workspaceSpec.specType}}"
+                "Expected: ${SpecType.WIDTH} - " +
+                "Found: ${calculatedWorkspaceSpec.spec.specType}}"
         }
 
-        val widthSpec = _widthSpecs.firstOrNull { availableWidth <= it.maxAvailableSize }
-        check(widthSpec != null) { "No FolderSpec for width spec found with $availableWidth." }
-
-        return convertToCalculatedFolderSpec(widthSpec, availableWidth, columns, workspaceSpec)
+        val spec = getWidthSpec(availableWidth)
+        return CalculatedFolderSpec(availableWidth, columns, spec, calculatedWorkspaceSpec)
     }
 
-    /**
-     * Returns the [CalculatedFolderSpec] for height, based on the available height, FolderSpecs and
-     * WorkspaceSpecs.
-     */
-    fun getHeightSpec(
+    fun getCalculatedHeightSpec(
         rows: Int,
         availableHeight: Int,
-        workspaceSpec: CalculatedWorkspaceSpec
+        calculatedWorkspaceSpec: CalculatedWorkspaceSpec
     ): CalculatedFolderSpec {
-        check(workspaceSpec.workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT) {
+        check(calculatedWorkspaceSpec.spec.specType == SpecType.HEIGHT) {
             "Invalid specType for CalculatedWorkspaceSpec. " +
-                "Expected: ${WorkspaceSpec.SpecType.HEIGHT} - " +
-                "Found: ${workspaceSpec.workspaceSpec.specType}}"
+                "Expected: ${SpecType.HEIGHT} - " +
+                "Found: ${calculatedWorkspaceSpec.spec.specType}}"
         }
 
-        val heightSpec = _heightSpecs.firstOrNull { availableHeight <= it.maxAvailableSize }
-        check(heightSpec != null) { "No FolderSpec for height spec found with $availableHeight." }
+        val spec = getHeightSpec(availableHeight)
+        return CalculatedFolderSpec(availableHeight, rows, spec, calculatedWorkspaceSpec)
+    }
 
-        return convertToCalculatedFolderSpec(heightSpec, availableHeight, rows, workspaceSpec)
+    companion object {
+
+        private const val XML_FOLDER_SPEC = "folderSpec"
+
+        @JvmStatic
+        fun create(resourceHelper: ResourceHelper): FolderSpecs {
+            val parser = ResponsiveSpecsParser(resourceHelper)
+            val specs = parser.parseXML(XML_FOLDER_SPEC, ::FolderSpec)
+            val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+            return FolderSpecs(widthSpecs, heightSpecs)
+        }
     }
 }
 
-data class CalculatedFolderSpec(
-    val availableSpace: Int,
-    val cells: Int,
-    val startPaddingPx: Int,
-    val endPaddingPx: Int,
-    val gutterPx: Int,
-    val cellSizePx: Int
-)
-
-/**
- * Responsive folder specs to be used to calculate the paddings, gutter and cell size for folders in
- * the workspace.
- *
- * @param maxAvailableSize indicates the breakpoint to use this specification.
- * @param specType indicates whether the paddings and gutters will be applied vertically or
- *   horizontally.
- * @param startPadding padding used at the top or left (right in RTL) in the workspace folder.
- * @param endPadding padding used at the bottom or right (left in RTL) in the workspace folder.
- * @param gutter the space between the cells vertically or horizontally depending on the [specType].
- * @param cellSize height or width of the cell depending on the [specType].
- */
 data class FolderSpec(
-    val maxAvailableSize: Int,
-    val specType: SpecType,
-    val startPadding: SizeSpec,
-    val endPadding: SizeSpec,
-    val gutter: SizeSpec,
-    val cellSize: SizeSpec
-) {
+    override val maxAvailableSize: Int,
+    override val specType: SpecType,
+    override val startPadding: SizeSpec,
+    override val endPadding: SizeSpec,
+    override val gutter: SizeSpec,
+    override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
 
-    enum class SpecType {
-        HEIGHT,
-        WIDTH
+    init {
+        check(isValid()) { "Invalid FolderSpec found." }
     }
 
-    fun isValid(): Boolean {
-        if (maxAvailableSize <= 0) {
-            Log.e(LOG_TAG, "FolderSpec#isValid - maxAvailableSize <= 0")
-            return false
-        }
-
-        // All specs are valid
-        if (
-            !(startPadding.isValid() &&
-                endPadding.isValid() &&
-                gutter.isValid() &&
-                cellSize.isValid())
-        ) {
-            Log.e(LOG_TAG, "FolderSpec#isValid - !allSpecsAreValid()")
-            return false
-        }
-
-        return true
-    }
-}
-
-/** Helper function to convert [FolderSpec] to [CalculatedFolderSpec] */
-private fun convertToCalculatedFolderSpec(
-    folderSpec: FolderSpec,
-    availableSpace: Int,
-    cells: Int,
-    workspaceSpec: CalculatedWorkspaceSpec
-): CalculatedFolderSpec {
-    // Map if is fixedSize, ofAvailableSpace or matchWorkspace
-    var startPaddingPx =
-        folderSpec.startPadding.getCalculatedValue(availableSpace, workspaceSpec.startPaddingPx)
-    var endPaddingPx =
-        folderSpec.endPadding.getCalculatedValue(availableSpace, workspaceSpec.endPaddingPx)
-    var gutterPx = folderSpec.gutter.getCalculatedValue(availableSpace, workspaceSpec.gutterPx)
-    var cellSizePx =
-        folderSpec.cellSize.getCalculatedValue(availableSpace, workspaceSpec.cellSizePx)
-
-    // Remainder space
-    val gutters = cells - 1
-    val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
-    val remainderSpace = availableSpace - usedSpace
-
-    startPaddingPx = folderSpec.startPadding.getRemainderSpaceValue(remainderSpace, startPaddingPx)
-    endPaddingPx = folderSpec.endPadding.getRemainderSpaceValue(remainderSpace, endPaddingPx)
-    gutterPx = folderSpec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx)
-    cellSizePx = folderSpec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx)
-
-    return CalculatedFolderSpec(
-        availableSpace = availableSpace,
-        cells = cells,
-        startPaddingPx = startPaddingPx,
-        endPaddingPx = endPaddingPx,
-        gutterPx = gutterPx,
-        cellSizePx = cellSizePx
+    constructor(
+        attrs: TypedArray,
+        specs: Map<String, SizeSpec>
+    ) : this(
+        maxAvailableSize =
+            attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+        specType =
+            SpecType.values()[
+                    attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+        startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+        endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+        gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+        cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
     )
 }
+
+class CalculatedFolderSpec(
+    availableSpace: Int,
+    cells: Int,
+    spec: FolderSpec,
+    calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+) : CalculatedResponsiveSpec(availableSpace, cells, spec, calculatedWorkspaceSpec)
diff --git a/src/com/android/launcher3/responsive/ResponsiveSpecs.kt b/src/com/android/launcher3/responsive/ResponsiveSpecs.kt
new file mode 100644
index 0000000..72a0ea4
--- /dev/null
+++ b/src/com/android/launcher3/responsive/ResponsiveSpecs.kt
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.util.Log
+
+/**
+ * Base class for responsive specs that holds a list of width and height specs.
+ *
+ * @param widthSpecs List of width responsive specifications
+ * @param heightSpecs List of height responsive specifications
+ */
+abstract class ResponsiveSpecs<T : ResponsiveSpec>(
+    val widthSpecs: List<T>,
+    val heightSpecs: List<T>
+) {
+
+    init {
+        check(widthSpecs.isNotEmpty() && heightSpecs.isNotEmpty()) {
+            "${this::class.simpleName} is incomplete - " +
+                "width list size = ${widthSpecs.size}; " +
+                "height list size = ${heightSpecs.size}."
+        }
+    }
+
+    /**
+     * Get a [ResponsiveSpec] for width within the breakpoint.
+     *
+     * @param availableWidth The width breakpoint for the spec
+     * @return A [ResponsiveSpec] for width.
+     */
+    fun getWidthSpec(availableWidth: Int): T {
+        val spec = widthSpecs.firstOrNull { availableWidth <= it.maxAvailableSize }
+        check(spec != null) { "No available width spec found within $availableWidth." }
+        return spec
+    }
+
+    /**
+     * Get a [ResponsiveSpec] for height within the breakpoint.
+     *
+     * @param availableHeight The height breakpoint for the spec
+     * @return A [ResponsiveSpec] for height.
+     */
+    fun getHeightSpec(availableHeight: Int): T {
+        val spec = heightSpecs.firstOrNull { availableHeight <= it.maxAvailableSize }
+        check(spec != null) { "No available height spec found within $availableHeight." }
+        return spec
+    }
+}
+
+/**
+ * Base class for a responsive specification that is used to calculate the paddings, gutter and cell
+ * size.
+ *
+ * @param maxAvailableSize indicates the breakpoint to use this specification.
+ * @param specType indicates whether the paddings and gutters will be applied vertically or
+ *   horizontally.
+ * @param startPadding padding used at the top or left (right in RTL) in the workspace folder.
+ * @param endPadding padding used at the bottom or right (left in RTL) in the workspace folder.
+ * @param gutter the space between the cells vertically or horizontally depending on the [specType].
+ * @param cellSize height or width of the cell depending on the [specType].
+ */
+abstract class ResponsiveSpec(
+    open val maxAvailableSize: Int,
+    open val specType: SpecType,
+    open val startPadding: SizeSpec,
+    open val endPadding: SizeSpec,
+    open val gutter: SizeSpec,
+    open val cellSize: SizeSpec
+) {
+    open fun isValid(): Boolean {
+        if (maxAvailableSize <= 0) {
+            Log.e(LOG_TAG, "${this::class.simpleName}#isValid - maxAvailableSize <= 0")
+            return false
+        }
+
+        // All specs need to be individually valid
+        if (!allSpecsAreValid()) {
+            Log.e(LOG_TAG, "${this::class.simpleName}#isValid - !allSpecsAreValid()")
+            return false
+        }
+
+        return true
+    }
+
+    private fun allSpecsAreValid(): Boolean {
+        return startPadding.isValid() &&
+            endPadding.isValid() &&
+            gutter.isValid() &&
+            cellSize.isValid()
+    }
+
+    enum class SpecType {
+        HEIGHT,
+        WIDTH
+    }
+
+    companion object {
+        private const val LOG_TAG = "ResponsiveSpec"
+    }
+}
+
+/**
+ * Calculated responsive specs contains the final paddings, gutter and cell size in pixels after
+ * they are calculated from the available space, cells and workspace specs.
+ */
+sealed class CalculatedResponsiveSpec {
+    var availableSpace: Int = 0
+        private set
+
+    var cells: Int = 0
+        private set
+
+    var startPaddingPx: Int = 0
+        private set
+
+    var endPaddingPx: Int = 0
+        private set
+
+    var gutterPx: Int = 0
+        private set
+
+    var cellSizePx: Int = 0
+        private set
+
+    var spec: ResponsiveSpec
+        private set
+
+    constructor(
+        availableSpace: Int,
+        cells: Int,
+        spec: ResponsiveSpec,
+        calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+    ) {
+        this.availableSpace = availableSpace
+        this.cells = cells
+        this.spec = spec
+
+        // Map if is fixedSize, ofAvailableSpace or matchWorkspace
+        startPaddingPx =
+            spec.startPadding.getCalculatedValue(
+                availableSpace,
+                calculatedWorkspaceSpec.startPaddingPx
+            )
+        endPaddingPx =
+            spec.endPadding.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.endPaddingPx)
+        gutterPx = spec.gutter.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.gutterPx)
+        cellSizePx =
+            spec.cellSize.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.cellSizePx)
+
+        updateRemainderSpaces(availableSpace, cells, spec)
+    }
+
+    constructor(availableSpace: Int, cells: Int, spec: ResponsiveSpec) {
+        this.availableSpace = availableSpace
+        this.cells = cells
+        this.spec = spec
+
+        // Map if is fixedSize or ofAvailableSpace
+        startPaddingPx = spec.startPadding.getCalculatedValue(availableSpace)
+        endPaddingPx = spec.endPadding.getCalculatedValue(availableSpace)
+        gutterPx = spec.gutter.getCalculatedValue(availableSpace)
+        cellSizePx = spec.cellSize.getCalculatedValue(availableSpace)
+
+        updateRemainderSpaces(availableSpace, cells, spec)
+    }
+
+    private fun updateRemainderSpaces(availableSpace: Int, cells: Int, spec: ResponsiveSpec) {
+        val gutters = cells - 1
+        val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
+        val remainderSpace = availableSpace - usedSpace
+
+        startPaddingPx = spec.startPadding.getRemainderSpaceValue(remainderSpace, startPaddingPx)
+        endPaddingPx = spec.endPadding.getRemainderSpaceValue(remainderSpace, endPaddingPx)
+        gutterPx = spec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx)
+        cellSizePx = spec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx)
+    }
+
+    override fun hashCode(): Int {
+        var result = availableSpace.hashCode()
+        result = 31 * result + cells.hashCode()
+        result = 31 * result + startPaddingPx.hashCode()
+        result = 31 * result + endPaddingPx.hashCode()
+        result = 31 * result + gutterPx.hashCode()
+        result = 31 * result + cellSizePx.hashCode()
+        result = 31 * result + spec.hashCode()
+        return result
+    }
+
+    override fun equals(other: Any?): Boolean {
+        return other is CalculatedResponsiveSpec &&
+            availableSpace == other.availableSpace &&
+            cells == other.cells &&
+            startPaddingPx == other.startPaddingPx &&
+            endPaddingPx == other.endPaddingPx &&
+            gutterPx == other.gutterPx &&
+            cellSizePx == other.cellSizePx &&
+            spec == other.spec
+    }
+
+    override fun toString(): String {
+        return "${this::class.simpleName}(" +
+            "availableSpace=$availableSpace, cells=$cells, startPaddingPx=$startPaddingPx, " +
+            "endPaddingPx=$endPaddingPx, gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
+            "${spec::class.simpleName}.maxAvailableSize=${spec.maxAvailableSize}" +
+            ")"
+    }
+}
diff --git a/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt b/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt
new file mode 100644
index 0000000..a89b619
--- /dev/null
+++ b/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.res.TypedArray
+import android.content.res.XmlResourceParser
+import android.util.Xml
+import com.android.launcher3.R
+import com.android.launcher3.util.ResourceHelper
+import java.io.IOException
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserException
+
+class ResponsiveSpecsParser(private val resourceHelper: ResourceHelper) {
+
+    private fun parseSizeSpecs(parser: XmlResourceParser): Map<String, SizeSpec> {
+        val parentName = parser.name
+        parser.next()
+
+        val result = mutableMapOf<String, SizeSpec>()
+        while (parser.eventType != XmlPullParser.END_DOCUMENT && parser.name != parentName) {
+            if (parser.eventType == XmlResourceParser.START_TAG) {
+                result[parser.name] = SizeSpec.create(resourceHelper, Xml.asAttributeSet(parser))
+            }
+            parser.next()
+        }
+
+        return result
+    }
+
+    fun <T> parseXML(
+        tagName: String,
+        map: (attributes: TypedArray, sizeSpecs: Map<String, SizeSpec>) -> T
+    ): List<T> {
+        val parser: XmlResourceParser = resourceHelper.getXml()
+
+        try {
+            val list = mutableListOf<T>()
+
+            var eventType = parser.eventType
+            while (eventType != XmlPullParser.END_DOCUMENT) {
+                if (eventType == XmlResourceParser.START_TAG && parser.name == tagName) {
+                    val attrs =
+                        resourceHelper.obtainStyledAttributes(
+                            Xml.asAttributeSet(parser),
+                            R.styleable.ResponsiveSpec
+                        )
+
+                    val sizeSpecs = parseSizeSpecs(parser)
+                    list += map(attrs, sizeSpecs)
+                    attrs.recycle()
+                }
+
+                eventType = parser.next()
+            }
+
+            parser.close()
+
+            return list
+        } catch (e: Exception) {
+            when (e) {
+                is NoSuchFieldException,
+                is IOException,
+                is XmlPullParserException ->
+                    throw RuntimeException("Failure parsing specs file.", e)
+                else -> throw e
+            }
+        } finally {
+            parser.close()
+        }
+    }
+}
+
+fun Map<String, SizeSpec>.getOrError(key: String): SizeSpec {
+    return this.getOrElse(key) { error("Attr '$key' must be defined.") }
+}
diff --git a/src/com/android/launcher3/responsive/SizeSpec.kt b/src/com/android/launcher3/responsive/SizeSpec.kt
index 3d618f9..d3868f0 100644
--- a/src/com/android/launcher3/responsive/SizeSpec.kt
+++ b/src/com/android/launcher3/responsive/SizeSpec.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.launcher3.responsive
 
 import android.content.res.TypedArray
@@ -26,7 +42,7 @@
 ) {
 
     /** Retrieves the correct value for [SizeSpec]. */
-    fun getCalculatedValue(availableSpace: Int, workspaceValue: Int): Int {
+    fun getCalculatedValue(availableSpace: Int, workspaceValue: Int = 0): Int {
         val calculatedValue =
             when {
                 fixedSize > 0 -> fixedSize.roundToInt()
@@ -91,6 +107,13 @@
         return true
     }
 
+    object XmlTags {
+        const val START_PADDING = "startPadding"
+        const val END_PADDING = "endPadding"
+        const val GUTTER = "gutter"
+        const val CELL_SIZE = "cellSize"
+    }
+
     companion object {
         private const val TAG = "SizeSpec"
 
diff --git a/src/com/android/launcher3/responsive/WorkspaceSpecs.kt b/src/com/android/launcher3/responsive/WorkspaceSpecs.kt
new file mode 100644
index 0000000..0da7026
--- /dev/null
+++ b/src/com/android/launcher3/responsive/WorkspaceSpecs.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.res.TypedArray
+import android.util.Log
+import com.android.launcher3.R
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
+import com.android.launcher3.util.ResourceHelper
+
+private const val TAG = "WorkspaceSpecs"
+
+class WorkspaceSpecs(widthSpecs: List<WorkspaceSpec>, heightSpecs: List<WorkspaceSpec>) :
+    ResponsiveSpecs<WorkspaceSpec>(widthSpecs, heightSpecs) {
+
+    fun getCalculatedWidthSpec(columns: Int, availableWidth: Int): CalculatedWorkspaceSpec {
+        val spec = getWidthSpec(availableWidth)
+        return CalculatedWorkspaceSpec(availableWidth, columns, spec)
+    }
+
+    fun getCalculatedHeightSpec(rows: Int, availableHeight: Int): CalculatedWorkspaceSpec {
+        val spec = getHeightSpec(availableHeight)
+        return CalculatedWorkspaceSpec(availableHeight, rows, spec)
+    }
+
+    companion object {
+        private const val XML_WORKSPACE_SPEC = "workspaceSpec"
+
+        @JvmStatic
+        fun create(resourceHelper: ResourceHelper): WorkspaceSpecs {
+            val parser = ResponsiveSpecsParser(resourceHelper)
+            val specs = parser.parseXML(XML_WORKSPACE_SPEC, ::WorkspaceSpec)
+            val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+            return WorkspaceSpecs(widthSpecs, heightSpecs)
+        }
+    }
+}
+
+data class WorkspaceSpec(
+    override val maxAvailableSize: Int,
+    override val specType: SpecType,
+    override val startPadding: SizeSpec,
+    override val endPadding: SizeSpec,
+    override val gutter: SizeSpec,
+    override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
+
+    init {
+        check(isValid()) { "Invalid WorkspaceSpec found." }
+    }
+
+    constructor(
+        attrs: TypedArray,
+        specs: Map<String, SizeSpec>
+    ) : this(
+        maxAvailableSize =
+            attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+        specType =
+            SpecType.values()[
+                    attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+        startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+        endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+        gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+        cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
+    )
+
+    override fun isValid(): Boolean {
+        // Workspace spec should not match workspace
+        if (
+            startPadding.matchWorkspace ||
+                endPadding.matchWorkspace ||
+                gutter.matchWorkspace ||
+                cellSize.matchWorkspace
+        ) {
+            Log.e(TAG, "WorkspaceSpec#isValid - workspace shouldn't contain matchWorkspace!")
+            return false
+        }
+
+        return super.isValid()
+    }
+}
+
+class CalculatedWorkspaceSpec(availableSpace: Int, cells: Int, spec: WorkspaceSpec) :
+    CalculatedResponsiveSpec(availableSpace, cells, spec)
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
index 717164e..e8be12c 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
@@ -240,9 +240,8 @@
                 public void onPreDragStart(DropTarget.DragObject dragObject) {
                     mDragView = dragObject.dragView;
                     if (!shouldStartDrag(0)) {
-                        mDragView.setOnAnimationEndCallback(() -> {
-                            mActivity.beginDragShared(v, mActivity.getAppsView(), options);
-                        });
+                        mDragView.setOnScaleAnimEndCallback(() ->
+                                mActivity.beginDragShared(v, mActivity.getAppsView(), options));
                     }
                 }
 
diff --git a/src/com/android/launcher3/util/EventLogArray.kt b/src/com/android/launcher3/util/EventLogArray.kt
new file mode 100644
index 0000000..a17d650
--- /dev/null
+++ b/src/com/android/launcher3/util/EventLogArray.kt
@@ -0,0 +1,117 @@
+/*
+ * 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 java.io.PrintWriter
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+/**
+ * A utility class to record and log events. Events are stored in a fixed size array and old logs
+ * are purged as new events come.
+ */
+class EventLogArray(private val name: String, size: Int) {
+
+    companion object {
+        private const val TYPE_ONE_OFF = 0
+        private const val TYPE_FLOAT = 1
+        private const val TYPE_INTEGER = 2
+        private const val TYPE_BOOL_TRUE = 3
+        private const val TYPE_BOOL_FALSE = 4
+        private fun isEntrySame(entry: EventEntry?, type: Int, event: String): Boolean {
+            return entry != null && entry.type == type && entry.event == event
+        }
+    }
+
+    private val logs: Array<EventEntry?>
+    private var nextIndex = 0
+
+    init {
+        logs = arrayOfNulls(size)
+    }
+
+    fun addLog(event: String) {
+        addLog(TYPE_ONE_OFF, event, 0f)
+    }
+
+    fun addLog(event: String, extras: Int) {
+        addLog(TYPE_INTEGER, event, extras.toFloat())
+    }
+
+    fun addLog(event: String, extras: Float) {
+        addLog(TYPE_FLOAT, event, extras)
+    }
+
+    fun addLog(event: String, extras: Boolean) {
+        addLog(if (extras) TYPE_BOOL_TRUE else TYPE_BOOL_FALSE, event, 0f)
+    }
+
+    private fun addLog(type: Int, event: String, extras: Float) {
+        // Merge the logs if it's a duplicate
+        val last = (nextIndex + logs.size - 1) % logs.size
+        val secondLast = (nextIndex + logs.size - 2) % logs.size
+        if (isEntrySame(logs[last], type, event) && isEntrySame(logs[secondLast], type, event)) {
+            logs[last]!!.update(type, event, extras)
+            logs[secondLast]!!.duplicateCount++
+            return
+        }
+        if (logs[nextIndex] == null) {
+            logs[nextIndex] = EventEntry()
+        }
+        logs[nextIndex]!!.update(type, event, extras)
+        nextIndex = (nextIndex + 1) % logs.size
+    }
+
+    fun dump(prefix: String, writer: PrintWriter) {
+        writer.println("$prefix$name event history:")
+        val sdf = SimpleDateFormat("  HH:mm:ss.SSSZ  ", Locale.US)
+        val date = Date()
+        for (i in logs.indices) {
+            val log = logs[(nextIndex + logs.size - i - 1) % logs.size] ?: continue
+            date.time = log.time
+            val msg = StringBuilder(prefix).append(sdf.format(date)).append(log.event)
+            when (log.type) {
+                TYPE_BOOL_FALSE -> msg.append(": false")
+                TYPE_BOOL_TRUE -> msg.append(": true")
+                TYPE_FLOAT -> msg.append(": ").append(log.extras)
+                TYPE_INTEGER -> msg.append(": ").append(log.extras.toInt())
+                else -> {}
+            }
+            if (log.duplicateCount > 0) {
+                msg.append(" & ").append(log.duplicateCount).append(" similar events")
+            }
+            writer.println(msg)
+        }
+    }
+
+    /** A single event entry. */
+    private class EventEntry {
+        var type = 0
+        var event: String? = null
+        var extras = 0f
+        var time: Long = 0
+        var duplicateCount = 0
+        fun update(type: Int, event: String, extras: Float) {
+            this.type = type
+            this.event = event
+            this.extras = extras
+            time = System.currentTimeMillis()
+            duplicateCount = 0
+        }
+    }
+}
diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index 91eb109..de10fc5 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -94,6 +94,8 @@
 
     // range [0, 1], 0=> completely open, 1=> completely closed
     protected float mTranslationShift = TRANSLATION_SHIFT_CLOSED;
+    /** {@link #mTranslationShift} at the invocation of {@link #onDragStart(boolean, float)}. */
+    protected float mDragStartTranslationShift;
 
     protected boolean mNoIntercept;
     protected @Nullable OnCloseListener mOnCloseBeginListener;
@@ -285,18 +287,23 @@
     /* SingleAxisSwipeDetector.Listener */
 
     @Override
-    public void onDragStart(boolean start, float startDisplacement) { }
+    public void onDragStart(boolean start, float startDisplacement) {
+        mOpenCloseAnimator.cancel();
+        mDragStartTranslationShift = mTranslationShift;
+    }
 
     @Override
     public boolean onDrag(float displacement) {
-        float range = getShiftRange();
-        displacement = Utilities.boundToRange(displacement, 0, range);
-        setTranslationShift(displacement / range);
+        setTranslationShift(Utilities.boundToRange(
+                mDragStartTranslationShift + displacement / getShiftRange(),
+                TRANSLATION_SHIFT_OPENED,
+                TRANSLATION_SHIFT_CLOSED));
         return true;
     }
 
     @Override
     public void onDragEnd(float velocity) {
+        mDragStartTranslationShift = 0;
         float successfulShiftThreshold = mActivityContext.getDeviceProfile().isTablet
                 ? TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS : SUCCESS_TRANSITION_PROGRESS;
         if ((mSwipeDetector.isFling(velocity) && velocity > 0)
diff --git a/src/com/android/launcher3/views/ArrowTipView.java b/src/com/android/launcher3/views/ArrowTipView.java
index 8e05650..b44dbeb 100644
--- a/src/com/android/launcher3/views/ArrowTipView.java
+++ b/src/com/android/launcher3/views/ArrowTipView.java
@@ -28,7 +28,9 @@
 import android.graphics.Rect;
 import android.graphics.drawable.ShapeDrawable;
 import android.os.Handler;
+import android.util.IntProperty;
 import android.util.Log;
+import android.view.ContextThemeWrapper;
 import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.View;
@@ -43,6 +45,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.graphics.TriangleShape;
 
@@ -57,6 +60,19 @@
     private static final long SHOW_DURATION_MS = 300;
     private static final long HIDE_DURATION_MS = 100;
 
+    public static final IntProperty<ArrowTipView> TEXT_ALPHA =
+            new IntProperty<>("textAlpha") {
+                @Override
+                public void setValue(ArrowTipView view, int v) {
+                    view.setTextAlpha(v);
+                }
+
+                @Override
+                public Integer get(ArrowTipView view) {
+                    return view.getTextAlpha();
+                }
+            };
+
     private final ActivityContext mActivityContext;
     private final Handler mHandler = new Handler();
     private boolean mIsPointingUp;
@@ -69,6 +85,8 @@
     private AnimatorSet mOpenAnimator = new AnimatorSet();
     private AnimatorSet mCloseAnimator = new AnimatorSet();
 
+    private int mTextAlpha;
+
     public ArrowTipView(Context context) {
         this(context, false);
     }
@@ -86,6 +104,11 @@
         mArrowMinOffset = context.getResources().getDimensionPixelSize(
                 R.dimen.dynamic_grid_cell_border_spacing);
         TypedArray ta = context.obtainStyledAttributes(R.styleable.ArrowTipView);
+        // Set style to default to avoid inflation issues with missing attributes.
+        if (!ta.hasValue(R.styleable.ArrowTipView_arrowTipBackground)
+                || !ta.hasValue(R.styleable.ArrowTipView_arrowTipTextColor)) {
+            context = new ContextThemeWrapper(context, R.style.ArrowTipStyle);
+        }
         mArrowViewPaintColor = ta.getColor(R.styleable.ArrowTipView_arrowTipBackground,
                 context.getColor(R.color.arrow_tip_view_bg));
         ta.recycle();
@@ -110,6 +133,8 @@
         }
         if (mIsOpen) {
             if (animate) {
+                mCloseAnimator.addListener(AnimatorListeners.forSuccessCallback(
+                        () -> mActivityContext.getDragLayer().removeView(this)));
                 mCloseAnimator.start();
             } else {
                 mCloseAnimator.cancel();
@@ -414,4 +439,16 @@
     public void setCustomCloseAnimation(AnimatorSet animator) {
         mCloseAnimator = animator;
     }
+
+    private void setTextAlpha(int textAlpha) {
+        if (mTextAlpha != textAlpha) {
+            mTextAlpha = textAlpha;
+            TextView textView = findViewById(R.id.text);
+            textView.setTextColor(textView.getTextColors().withAlpha(mTextAlpha));
+        }
+    }
+
+    private int getTextAlpha() {
+        return mTextAlpha;
+    }
 }
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 55febc7..b62f60d 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -62,8 +62,10 @@
 
 /**
  * Popup shown on long pressing an empty space in launcher
+ *
+ * @param <T> The context showing this popup.
  */
-public class OptionsPopupView extends ArrowPopup<Launcher>
+public class OptionsPopupView<T extends Context & ActivityContext> extends ArrowPopup<T>
         implements OnClickListener, OnLongClickListener {
 
     // An intent extra to indicate the horizontal scroll of the wallpaper.
@@ -155,21 +157,27 @@
         }
     }
 
-    public static OptionsPopupView show(ActivityContext launcher, RectF targetRect,
-            List<OptionItem> items, boolean shouldAddArrow) {
-        return show(launcher, targetRect, items, shouldAddArrow, 0 /* width */);
+    public static <T extends Context & ActivityContext> OptionsPopupView<T> show(
+            ActivityContext activityContext,
+            RectF targetRect,
+            List<OptionItem> items,
+            boolean shouldAddArrow) {
+        return show(activityContext, targetRect, items, shouldAddArrow, 0 /* width */);
     }
 
-    public static OptionsPopupView show(ActivityContext launcher, RectF targetRect,
-            List<OptionItem> items, boolean shouldAddArrow, int width) {
-        OptionsPopupView popup = (OptionsPopupView) launcher.getLayoutInflater()
-                .inflate(R.layout.longpress_options_menu, launcher.getDragLayer(), false);
+    public static <T extends Context & ActivityContext> OptionsPopupView<T> show(
+            ActivityContext activityContext,
+            RectF targetRect,
+            List<OptionItem> items,
+            boolean shouldAddArrow,
+            int width) {
+        OptionsPopupView<T> popup = (OptionsPopupView<T>) activityContext.getLayoutInflater()
+                .inflate(R.layout.longpress_options_menu, activityContext.getDragLayer(), false);
         popup.mTargetRect = targetRect;
         popup.setShouldAddArrow(shouldAddArrow);
 
         for (OptionItem item : items) {
-            DeepShortcutView view =
-                    (DeepShortcutView) popup.inflateAndAdd(R.layout.system_shortcut, popup);
+            DeepShortcutView view = popup.inflateAndAdd(R.layout.system_shortcut, popup);
             if (width > 0) {
                 view.getLayoutParams().width = width;
             }
diff --git a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
deleted file mode 100644
index 8cc0c59..0000000
--- a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.workspace
-
-import android.content.res.XmlResourceParser
-import android.util.AttributeSet
-import android.util.Log
-import android.util.Xml
-import com.android.launcher3.R
-import com.android.launcher3.responsive.SizeSpec
-import com.android.launcher3.util.ResourceHelper
-import java.io.IOException
-import kotlin.math.roundToInt
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
-
-private const val TAG = "WorkspaceSpecs"
-
-class WorkspaceSpecs(resourceHelper: ResourceHelper) {
-    object XmlTags {
-        const val WORKSPACE_SPECS = "workspaceSpecs"
-
-        const val WORKSPACE_SPEC = "workspaceSpec"
-        const val START_PADDING = "startPadding"
-        const val END_PADDING = "endPadding"
-        const val GUTTER = "gutter"
-        const val CELL_SIZE = "cellSize"
-    }
-
-    val workspaceHeightSpecList = mutableListOf<WorkspaceSpec>()
-    val workspaceWidthSpecList = mutableListOf<WorkspaceSpec>()
-
-    // TODO(b/286538013) Remove this init after a more generic or reusable parser is created
-    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.create(resourceHelper, attr)
-                                        }
-                                        XmlTags.END_PADDING -> {
-                                            endPadding = SizeSpec.create(resourceHelper, attr)
-                                        }
-                                        XmlTags.GUTTER -> {
-                                            gutter = SizeSpec.create(resourceHelper, attr)
-                                        }
-                                        XmlTags.CELL_SIZE -> {
-                                            cellSize = SizeSpec.create(resourceHelper, attr)
-                                        }
-                                    }
-                                }
-                            }
-
-                            if (
-                                startPadding == null ||
-                                    endPadding == null ||
-                                    gutter == null ||
-                                    cellSize == null
-                            ) {
-                                throw IllegalStateException(
-                                    "All attributes in workspaceSpec must be defined"
-                                )
-                            }
-
-                            val workspaceSpec =
-                                WorkspaceSpec(
-                                    maxAvailableSize,
-                                    specType,
-                                    startPadding,
-                                    endPadding,
-                                    gutter,
-                                    cellSize
-                                )
-                            if (workspaceSpec.isValid()) {
-                                if (workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT)
-                                    workspaceHeightSpecList.add(workspaceSpec)
-                                else workspaceWidthSpecList.add(workspaceSpec)
-                            } else {
-                                throw IllegalStateException("Invalid workspaceSpec found.")
-                            }
-                        }
-                    }
-
-                    if (workspaceWidthSpecList.isEmpty() || workspaceHeightSpecList.isEmpty()) {
-                        throw IllegalStateException(
-                            "WorkspaceSpecs is incomplete - " +
-                                "height list size = ${workspaceHeightSpecList.size}; " +
-                                "width list size = ${workspaceWidthSpecList.size}."
-                        )
-                    }
-                }
-            }
-            parser.close()
-        } catch (e: Exception) {
-            when (e) {
-                is IOException,
-                is XmlPullParserException -> {
-                    throw RuntimeException("Failure parsing workspaces specs file.", e)
-                }
-                else -> throw e
-            }
-        }
-    }
-
-    /**
-     * Returns the CalculatedWorkspaceSpec for width, based on the available width and the
-     * WorkspaceSpecs.
-     */
-    fun getCalculatedWidthSpec(columns: Int, availableWidth: Int): CalculatedWorkspaceSpec {
-        val widthSpec = workspaceWidthSpecList.first { availableWidth <= it.maxAvailableSize }
-
-        return CalculatedWorkspaceSpec(availableWidth, columns, widthSpec)
-    }
-
-    /**
-     * Returns the CalculatedWorkspaceSpec for height, based on the available height and the
-     * WorkspaceSpecs.
-     */
-    fun getCalculatedHeightSpec(rows: Int, availableHeight: Int): CalculatedWorkspaceSpec {
-        val heightSpec = workspaceHeightSpecList.first { availableHeight <= it.maxAvailableSize }
-
-        return CalculatedWorkspaceSpec(availableHeight, rows, heightSpec)
-    }
-}
-
-class CalculatedWorkspaceSpec(
-    val availableSpace: Int,
-    val cells: Int,
-    val workspaceSpec: WorkspaceSpec
-) {
-    var startPaddingPx: Int = 0
-        private set
-    var endPaddingPx: Int = 0
-        private set
-    var gutterPx: Int = 0
-        private set
-    var cellSizePx: Int = 0
-        private set
-    init {
-        // Calculate all fixed size first
-        if (workspaceSpec.startPadding.fixedSize > 0)
-            startPaddingPx = workspaceSpec.startPadding.fixedSize.roundToInt()
-        if (workspaceSpec.endPadding.fixedSize > 0)
-            endPaddingPx = workspaceSpec.endPadding.fixedSize.roundToInt()
-        if (workspaceSpec.gutter.fixedSize > 0)
-            gutterPx = workspaceSpec.gutter.fixedSize.roundToInt()
-        if (workspaceSpec.cellSize.fixedSize > 0)
-            cellSizePx = workspaceSpec.cellSize.fixedSize.roundToInt()
-
-        // Calculate all available space next
-        if (workspaceSpec.startPadding.ofAvailableSpace > 0)
-            startPaddingPx =
-                (workspaceSpec.startPadding.ofAvailableSpace * availableSpace).roundToInt()
-        if (workspaceSpec.endPadding.ofAvailableSpace > 0)
-            endPaddingPx = (workspaceSpec.endPadding.ofAvailableSpace * availableSpace).roundToInt()
-        if (workspaceSpec.gutter.ofAvailableSpace > 0)
-            gutterPx = (workspaceSpec.gutter.ofAvailableSpace * availableSpace).roundToInt()
-        if (workspaceSpec.cellSize.ofAvailableSpace > 0)
-            cellSizePx = (workspaceSpec.cellSize.ofAvailableSpace * availableSpace).roundToInt()
-
-        // Calculate remainder space last
-        val gutters = cells - 1
-        val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
-        val remainderSpace = availableSpace - usedSpace
-        if (workspaceSpec.startPadding.ofRemainderSpace > 0)
-            startPaddingPx =
-                (workspaceSpec.startPadding.ofRemainderSpace * remainderSpace).roundToInt()
-        if (workspaceSpec.endPadding.ofRemainderSpace > 0)
-            endPaddingPx = (workspaceSpec.endPadding.ofRemainderSpace * remainderSpace).roundToInt()
-        if (workspaceSpec.gutter.ofRemainderSpace > 0)
-            gutterPx = (workspaceSpec.gutter.ofRemainderSpace * remainderSpace).roundToInt()
-        if (workspaceSpec.cellSize.ofRemainderSpace > 0)
-            cellSizePx = (workspaceSpec.cellSize.ofRemainderSpace * remainderSpace).roundToInt()
-    }
-
-    override fun toString(): String {
-        return "CalculatedWorkspaceSpec(availableSpace=$availableSpace, " +
-            "cells=$cells, startPaddingPx=$startPaddingPx, endPaddingPx=$endPaddingPx, " +
-            "gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
-            "workspaceSpec.maxAvailableSize=${workspaceSpec.maxAvailableSize})"
-    }
-}
-
-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() &&
-            !startPadding.matchWorkspace &&
-            !endPadding.matchWorkspace &&
-            !gutter.matchWorkspace &&
-            !cellSize.matchWorkspace
-}
diff --git a/tests/Android.bp b/tests/Android.bp
index 135a6e0..5a52440 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -50,12 +50,10 @@
       "src/com/android/launcher3/util/Wait.java",
       "src/com/android/launcher3/util/WidgetUtils.java",
       "src/com/android/launcher3/util/rule/FailureWatcher.java",
-      "src/com/android/launcher3/util/rule/LauncherActivityRule.java",
       "src/com/android/launcher3/util/rule/ViewCaptureRule.kt",
       "src/com/android/launcher3/util/rule/SamplerRule.java",
       "src/com/android/launcher3/util/rule/ScreenRecordRule.java",
       "src/com/android/launcher3/util/rule/ShellCommandRule.java",
-      "src/com/android/launcher3/util/rule/SimpleActivityRule.java",
       "src/com/android/launcher3/util/rule/TestStabilityRule.java",
       "src/com/android/launcher3/util/rule/TISBindRule.java",
       "src/com/android/launcher3/util/viewcapture_analysis/*.java",
@@ -88,6 +86,7 @@
         "launcher_log_protos_lite",
         "truth-prebuilt",
         "platform-test-rules",
+        "testables",
     ],
     manifest: "AndroidManifest-common.xml",
     platform_apis: true,
diff --git a/tests/res/values/attrs.xml b/tests/res/values/attrs.xml
index 0d586c2..e5ee064 100644
--- a/tests/res/values/attrs.xml
+++ b/tests/res/values/attrs.xml
@@ -18,7 +18,7 @@
 <!-- Attributes have to be copied to test for correct parsing of files -->
 <resources>
     <!--  Responsive grids attributes  -->
-    <declare-styleable name="WorkspaceSpec">
+    <declare-styleable name="ResponsiveSpec">
         <attr name="specType" format="integer">
             <enum name="height" value="0" />
             <enum name="width" value="1" />
@@ -26,12 +26,9 @@
         <attr name="maxAvailableSize" format="dimension" />
     </declare-styleable>
 
-    <declare-styleable name="SizeSpec">
-        <attr name="fixedSize" format="dimension" />
-        <attr name="ofAvailableSpace" format="float" />
-        <attr name="ofRemainderSpace" format="float" />
-        <attr name="matchWorkspace" format="boolean" />
-        <attr name="maxSize" format="dimension" />
+    <declare-styleable name="WorkspaceSpec">
+        <attr name="specType" />
+        <attr name="maxAvailableSize" />
     </declare-styleable>
 
     <declare-styleable name="FolderSpec">
@@ -43,4 +40,12 @@
         <attr name="specType" />
         <attr name="maxAvailableSize" />
     </declare-styleable>
+
+    <declare-styleable name="SizeSpec">
+        <attr name="fixedSize" format="dimension" />
+        <attr name="ofAvailableSpace" format="float" />
+        <attr name="ofRemainderSpace" format="float" />
+        <attr name="matchWorkspace" format="boolean" />
+        <attr name="maxSize" format="dimension" />
+    </declare-styleable>
 </resources>
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 75cee2f..fcc2fc1 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -156,8 +156,9 @@
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
     public static final String TWO_TASKBAR_LONG_CLICKS = "b/262282528";
     public static final String FLAKY_ACTIVITY_COUNT = "b/260260325";
+    public static final String FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP = "b/286084688";
     public static final String ICON_MISSING = "b/282963545";
-    public static final String ACTIVITY_LIFECYCLE_RULE = "b/289161193";
+    public static final String LAUNCH_SPLIT_PAIR = "b/288939273";
 
     public static final String REQUEST_EMULATE_DISPLAY = "emulate-display";
     public static final String REQUEST_STOP_EMULATE_DISPLAY = "stop-emulate-display";
diff --git a/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt b/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
index 77ea5ba..cd95e99 100644
--- a/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
+++ b/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
@@ -41,9 +41,9 @@
     @Test
     fun parseValidFile() {
         val allAppsSpecs =
-            AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.valid_all_apps_file))
-        assertThat(allAppsSpecs.allAppsHeightSpecList.size).isEqualTo(1)
-        assertThat(allAppsSpecs.allAppsHeightSpecList[0].toString())
+            AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_all_apps_file))
+        assertThat(allAppsSpecs.heightSpecs.size).isEqualTo(1)
+        assertThat(allAppsSpecs.heightSpecs[0].toString())
             .isEqualTo(
                 "AllAppsSpec(" +
                     "maxAvailableSize=26247, " +
@@ -71,8 +71,8 @@
                     ")"
             )
 
-        assertThat(allAppsSpecs.allAppsWidthSpecList.size).isEqualTo(1)
-        assertThat(allAppsSpecs.allAppsWidthSpecList[0].toString())
+        assertThat(allAppsSpecs.widthSpecs.size).isEqualTo(1)
+        assertThat(allAppsSpecs.widthSpecs[0].toString())
             .isEqualTo(
                 "AllAppsSpec(" +
                     "maxAvailableSize=26247, " +
@@ -103,16 +103,16 @@
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_missingTag_throwsError() {
-        AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_1))
+        AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_1))
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
-        AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_2))
+        AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_2))
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_valueBiggerThan1_throwsError() {
-        AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_3))
+        AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_3))
     }
 }
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
index 9f981fa..0f12e58 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
+++ b/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
@@ -23,7 +23,6 @@
 import com.android.launcher3.AbstractDeviceProfileTest
 import com.android.launcher3.tests.R as TestR
 import com.android.launcher3.util.TestResourceHelper
-import com.android.launcher3.workspace.WorkspaceSpecs
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -49,12 +48,12 @@
         val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 495
 
         val workspaceSpecs =
-            WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+            WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
         val widthSpec = workspaceSpecs.getCalculatedWidthSpec(4, availableWidth)
         val heightSpec = workspaceSpecs.getCalculatedHeightSpec(5, availableHeight)
 
         val allAppsSpecs =
-            AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.valid_all_apps_file))
+            AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_all_apps_file))
 
         with(allAppsSpecs.getCalculatedWidthSpec(4, availableWidth, widthSpec)) {
             assertThat(availableSpace).isEqualTo(availableWidth)
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt
index c14722c..863cf76 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt
+++ b/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt
@@ -24,7 +24,6 @@
 import com.android.launcher3.testing.shared.ResourceUtils
 import com.android.launcher3.tests.R
 import com.android.launcher3.util.TestResourceHelper
-import com.android.launcher3.workspace.WorkspaceSpecs
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -48,11 +47,11 @@
 
         // Loading workspace specs
         val resourceHelperWorkspace = TestResourceHelper(context!!, R.xml.valid_workspace_file)
-        val workspaceSpecs = WorkspaceSpecs(resourceHelperWorkspace)
+        val workspaceSpecs = WorkspaceSpecs.create(resourceHelperWorkspace)
 
         // Loading folders specs
         val resourceHelperFolder = TestResourceHelper(context!!, R.xml.valid_folders_specs)
-        val folderSpecs = FolderSpecs(resourceHelperFolder)
+        val folderSpecs = FolderSpecs.create(resourceHelperFolder)
 
         assertThat(folderSpecs.widthSpecs.size).isEqualTo(2)
         assertThat(folderSpecs.widthSpecs[0].cellSize.matchWorkspace).isEqualTo(true)
@@ -62,7 +61,7 @@
         var availableWidth = deviceSpec.naturalSize.first
         var calculatedWorkspace = workspaceSpecs.getCalculatedWidthSpec(columns, availableWidth)
         var calculatedWidthFolderSpec =
-            folderSpecs.getWidthSpec(columns, availableWidth, calculatedWorkspace)
+            folderSpecs.getCalculatedWidthSpec(columns, availableWidth, calculatedWorkspace)
         with(calculatedWidthFolderSpec) {
             assertThat(availableSpace).isEqualTo(availableWidth)
             assertThat(cells).isEqualTo(columns)
@@ -76,7 +75,7 @@
         availableWidth = 2000.dpToPx()
         calculatedWorkspace = workspaceSpecs.getCalculatedWidthSpec(columns, availableWidth)
         calculatedWidthFolderSpec =
-            folderSpecs.getWidthSpec(columns, availableWidth, calculatedWorkspace)
+            folderSpecs.getCalculatedWidthSpec(columns, availableWidth, calculatedWorkspace)
         with(calculatedWidthFolderSpec) {
             assertThat(availableSpace).isEqualTo(availableWidth)
             assertThat(cells).isEqualTo(columns)
@@ -97,11 +96,11 @@
 
         // Loading workspace specs
         val resourceHelperWorkspace = TestResourceHelper(context!!, R.xml.valid_workspace_file)
-        val workspaceSpecs = WorkspaceSpecs(resourceHelperWorkspace)
+        val workspaceSpecs = WorkspaceSpecs.create(resourceHelperWorkspace)
 
         // Loading folders specs
         val resourceHelperFolder = TestResourceHelper(context!!, R.xml.valid_folders_specs)
-        val folderSpecs = FolderSpecs(resourceHelperFolder)
+        val folderSpecs = FolderSpecs.create(resourceHelperFolder)
 
         assertThat(folderSpecs.heightSpecs.size).isEqualTo(1)
         assertThat(folderSpecs.heightSpecs[0].cellSize.matchWorkspace).isEqualTo(true)
@@ -109,7 +108,7 @@
         // Validate height spec
         val calculatedWorkspace = workspaceSpecs.getCalculatedHeightSpec(rows, availableHeight)
         val calculatedFolderSpec =
-            folderSpecs.getHeightSpec(rows, availableHeight, calculatedWorkspace)
+            folderSpecs.getCalculatedHeightSpec(rows, availableHeight, calculatedWorkspace)
         with(calculatedFolderSpec) {
             assertThat(availableSpace).isEqualTo(availableHeight)
             assertThat(cells).isEqualTo(rows)
diff --git a/tests/src/com/android/launcher3/workspace/CalculatedWorkspaceSpecTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
similarity index 94%
rename from tests/src/com/android/launcher3/workspace/CalculatedWorkspaceSpecTest.kt
rename to tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
index 7f03ba2..0af694e 100644
--- a/tests/src/com/android/launcher3/workspace/CalculatedWorkspaceSpecTest.kt
+++ b/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.workspace
+package com.android.launcher3.responsive
 
 import android.content.Context
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -49,7 +49,7 @@
         val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 495
 
         val workspaceSpecs =
-            WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+            WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
         val widthSpec = workspaceSpecs.getCalculatedWidthSpec(4, availableWidth)
         val heightSpec = workspaceSpecs.getCalculatedHeightSpec(5, availableHeight)
 
@@ -86,7 +86,7 @@
         val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 640
 
         val workspaceSpecs =
-            WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+            WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
         val widthSpec = workspaceSpecs.getCalculatedWidthSpec(4, availableWidth)
         val heightSpec = workspaceSpecs.getCalculatedHeightSpec(5, availableHeight)
 
diff --git a/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt b/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt
index 796bf9a..e21af57 100644
--- a/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt
+++ b/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt
@@ -21,11 +21,10 @@
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.AbstractDeviceProfileTest
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
 import com.android.launcher3.testing.shared.ResourceUtils
 import com.android.launcher3.tests.R
 import com.android.launcher3.util.TestResourceHelper
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec
-import com.android.launcher3.workspace.WorkspaceSpec
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -44,14 +43,14 @@
     @Test
     fun parseValidFile() {
         val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
-        val folderSpecs = FolderSpecs(resourceHelper)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
 
         val sizeSpec16 = SizeSpec(16f.dpToPx())
         val widthSpecsExpected =
             listOf(
                 FolderSpec(
                     maxAvailableSize = 800.dpToPx(),
-                    specType = FolderSpec.SpecType.WIDTH,
+                    specType = SpecType.WIDTH,
                     startPadding = sizeSpec16,
                     endPadding = sizeSpec16,
                     gutter = sizeSpec16,
@@ -59,7 +58,7 @@
                 ),
                 FolderSpec(
                     maxAvailableSize = 9999.dpToPx(),
-                    specType = FolderSpec.SpecType.WIDTH,
+                    specType = SpecType.WIDTH,
                     startPadding = sizeSpec16,
                     endPadding = sizeSpec16,
                     gutter = sizeSpec16,
@@ -70,7 +69,7 @@
         val heightSpecsExpected =
             FolderSpec(
                 maxAvailableSize = 9999.dpToPx(),
-                specType = FolderSpec.SpecType.HEIGHT,
+                specType = SpecType.HEIGHT,
                 startPadding = SizeSpec(24f.dpToPx()),
                 endPadding = SizeSpec(64f.dpToPx()),
                 gutter = sizeSpec16,
@@ -88,25 +87,25 @@
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_missingTag_throwsError() {
         val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_1)
-        FolderSpecs(resourceHelper)
+        FolderSpecs.create(resourceHelper)
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
         val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_2)
-        FolderSpecs(resourceHelper)
+        FolderSpecs.create(resourceHelper)
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_valueBiggerThan1_throwsError() {
         val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_3)
-        FolderSpecs(resourceHelper)
+        FolderSpecs.create(resourceHelper)
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_missingSpecs_throwsError() {
         val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_4)
-        FolderSpecs(resourceHelper)
+        FolderSpecs.create(resourceHelper)
     }
 
     @Test(expected = IllegalStateException::class)
@@ -117,7 +116,7 @@
         val workspaceSpec =
             WorkspaceSpec(
                 maxAvailableSize = availableSpace,
-                specType = WorkspaceSpec.SpecType.WIDTH,
+                specType = SpecType.WIDTH,
                 startPadding = SizeSpec(fixedSize = 10f),
                 endPadding = SizeSpec(fixedSize = 10f),
                 gutter = SizeSpec(fixedSize = 10f),
@@ -126,8 +125,8 @@
         val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
 
         val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_5)
-        val folderSpecs = FolderSpecs(resourceHelper)
-        folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+        folderSpecs.getCalculatedWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
     }
 
     @Test(expected = IllegalStateException::class)
@@ -138,7 +137,7 @@
         val workspaceSpec =
             WorkspaceSpec(
                 maxAvailableSize = availableSpace,
-                specType = WorkspaceSpec.SpecType.HEIGHT,
+                specType = SpecType.HEIGHT,
                 startPadding = SizeSpec(fixedSize = 10f),
                 endPadding = SizeSpec(fixedSize = 10f),
                 gutter = SizeSpec(fixedSize = 10f),
@@ -147,8 +146,8 @@
         val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
 
         val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_5)
-        val folderSpecs = FolderSpecs(resourceHelper)
-        folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+        folderSpecs.getCalculatedHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
     }
 
     @Test
@@ -159,7 +158,7 @@
         val workspaceSpec =
             WorkspaceSpec(
                 maxAvailableSize = availableSpace,
-                specType = WorkspaceSpec.SpecType.WIDTH,
+                specType = SpecType.WIDTH,
                 startPadding = SizeSpec(fixedSize = 10f),
                 endPadding = SizeSpec(fixedSize = 10f),
                 gutter = SizeSpec(fixedSize = 10f),
@@ -167,21 +166,17 @@
             )
         val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
 
-        val expectedResult =
-            CalculatedFolderSpec(
-                startPaddingPx = 16.dpToPx(),
-                endPaddingPx = 16.dpToPx(),
-                gutterPx = 16.dpToPx(),
-                cellSizePx = calculatedWorkspaceSpec.cellSizePx,
-                availableSpace = availableSpace,
-                cells = cells
-            )
-
         val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
-        val folderSpecs = FolderSpecs(resourceHelper)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
         val calculatedWidthSpec =
-            folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
-        assertThat(calculatedWidthSpec).isEqualTo(expectedResult)
+            folderSpecs.getCalculatedWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
+
+        assertThat(calculatedWidthSpec.cells).isEqualTo(cells)
+        assertThat(calculatedWidthSpec.availableSpace).isEqualTo(availableSpace)
+        assertThat(calculatedWidthSpec.startPaddingPx).isEqualTo(16.dpToPx())
+        assertThat(calculatedWidthSpec.endPaddingPx).isEqualTo(16.dpToPx())
+        assertThat(calculatedWidthSpec.gutterPx).isEqualTo(16.dpToPx())
+        assertThat(calculatedWidthSpec.cellSizePx).isEqualTo(calculatedWorkspaceSpec.cellSizePx)
     }
 
     @Test(expected = IllegalStateException::class)
@@ -192,7 +187,7 @@
         val workspaceSpec =
             WorkspaceSpec(
                 maxAvailableSize = availableSpace,
-                specType = WorkspaceSpec.SpecType.HEIGHT,
+                specType = SpecType.HEIGHT,
                 startPadding = SizeSpec(fixedSize = 10f),
                 endPadding = SizeSpec(fixedSize = 10f),
                 gutter = SizeSpec(fixedSize = 10f),
@@ -201,8 +196,8 @@
         val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
 
         val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
-        val folderSpecs = FolderSpecs(resourceHelper)
-        folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+        folderSpecs.getCalculatedWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
     }
 
     @Test
@@ -213,7 +208,7 @@
         val workspaceSpec =
             WorkspaceSpec(
                 maxAvailableSize = availableSpace,
-                specType = WorkspaceSpec.SpecType.HEIGHT,
+                specType = SpecType.HEIGHT,
                 startPadding = SizeSpec(fixedSize = 10f),
                 endPadding = SizeSpec(fixedSize = 10f),
                 gutter = SizeSpec(fixedSize = 10f),
@@ -221,21 +216,17 @@
             )
         val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
 
-        val expectedResult =
-            CalculatedFolderSpec(
-                startPaddingPx = 24.dpToPx(),
-                endPaddingPx = 64.dpToPx(),
-                gutterPx = 16.dpToPx(),
-                cellSizePx = calculatedWorkspaceSpec.cellSizePx,
-                availableSpace = availableSpace,
-                cells = cells
-            )
-
         val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
-        val folderSpecs = FolderSpecs(resourceHelper)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
         val calculatedHeightSpec =
-            folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
-        assertThat(calculatedHeightSpec).isEqualTo(expectedResult)
+            folderSpecs.getCalculatedHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
+
+        assertThat(calculatedHeightSpec.cells).isEqualTo(cells)
+        assertThat(calculatedHeightSpec.availableSpace).isEqualTo(availableSpace)
+        assertThat(calculatedHeightSpec.startPaddingPx).isEqualTo(24.dpToPx())
+        assertThat(calculatedHeightSpec.endPaddingPx).isEqualTo(64.dpToPx())
+        assertThat(calculatedHeightSpec.gutterPx).isEqualTo(16.dpToPx())
+        assertThat(calculatedHeightSpec.cellSizePx).isEqualTo(calculatedWorkspaceSpec.cellSizePx)
     }
 
     @Test(expected = IllegalStateException::class)
@@ -246,7 +237,7 @@
         val workspaceSpec =
             WorkspaceSpec(
                 maxAvailableSize = availableSpace,
-                specType = WorkspaceSpec.SpecType.WIDTH,
+                specType = SpecType.WIDTH,
                 startPadding = SizeSpec(fixedSize = 10f),
                 endPadding = SizeSpec(fixedSize = 10f),
                 gutter = SizeSpec(fixedSize = 10f),
@@ -255,8 +246,8 @@
         val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
 
         val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
-        val folderSpecs = FolderSpecs(resourceHelper)
-        folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+        folderSpecs.getCalculatedHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
     }
 
     private fun Float.dpToPx(): Float {
diff --git a/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt b/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
similarity index 86%
rename from tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
rename to tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
index 8b99a3a..0364069 100644
--- a/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
+++ b/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.workspace
+package com.android.launcher3.responsive
 
 import android.content.Context
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -41,9 +41,9 @@
     @Test
     fun parseValidFile() {
         val workspaceSpecs =
-            WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
-        assertThat(workspaceSpecs.workspaceHeightSpecList.size).isEqualTo(3)
-        assertThat(workspaceSpecs.workspaceHeightSpecList[0].toString())
+            WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+        assertThat(workspaceSpecs.heightSpecs.size).isEqualTo(3)
+        assertThat(workspaceSpecs.heightSpecs[0].toString())
             .isEqualTo(
                 "WorkspaceSpec(" +
                     "maxAvailableSize=1533, " +
@@ -70,7 +70,7 @@
                     "maxSize=2147483647)" +
                     ")"
             )
-        assertThat(workspaceSpecs.workspaceHeightSpecList[1].toString())
+        assertThat(workspaceSpecs.heightSpecs[1].toString())
             .isEqualTo(
                 "WorkspaceSpec(" +
                     "maxAvailableSize=1607, " +
@@ -97,7 +97,7 @@
                     "maxSize=2147483647)" +
                     ")"
             )
-        assertThat(workspaceSpecs.workspaceHeightSpecList[2].toString())
+        assertThat(workspaceSpecs.heightSpecs[2].toString())
             .isEqualTo(
                 "WorkspaceSpec(" +
                     "maxAvailableSize=26247, " +
@@ -124,8 +124,8 @@
                     "maxSize=2147483647)" +
                     ")"
             )
-        assertThat(workspaceSpecs.workspaceWidthSpecList.size).isEqualTo(1)
-        assertThat(workspaceSpecs.workspaceWidthSpecList[0].toString())
+        assertThat(workspaceSpecs.widthSpecs.size).isEqualTo(1)
+        assertThat(workspaceSpecs.widthSpecs[0].toString())
             .isEqualTo(
                 "WorkspaceSpec(" +
                     "maxAvailableSize=26247, " +
@@ -156,21 +156,29 @@
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_missingTag_throwsError() {
-        WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_1))
+        WorkspaceSpecs.create(
+            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))
+        WorkspaceSpecs.create(
+            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))
+        WorkspaceSpecs.create(
+            TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_3)
+        )
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_matchWorkspace_true_throwsError() {
-        WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_4))
+        WorkspaceSpecs.create(
+            TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_4)
+        )
     }
 }
diff --git a/tests/src/com/android/launcher3/tapl/TaplUtilityTests.java b/tests/src/com/android/launcher3/tapl/TaplUtilityTests.java
new file mode 100644
index 0000000..15db1d8
--- /dev/null
+++ b/tests/src/com/android/launcher3/tapl/TaplUtilityTests.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class TaplUtilityTests {
+
+    @Test
+    public void testNewStringWithRegex() {
+        assertTrue(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("Play Store has 7 notifications").matches());
+        assertTrue(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("Play  Store!").matches());
+        assertFalse(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("play  store").matches());
+        assertFalse(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("").matches());
+        assertTrue(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("Play \n Store").matches());
+    }
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java b/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java
index d3ce67c..81a59b9 100644
--- a/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java
+++ b/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java
@@ -69,7 +69,10 @@
         mView.setBackgroundColor(Color.BLUE);
         setContentView(mView);
 
-        registerReceiver(mCommandReceiver, new IntentFilter(mAction + SUFFIX_COMMAND));
+        registerReceiver(
+                mCommandReceiver,
+                new IntentFilter(mAction + SUFFIX_COMMAND),
+                RECEIVER_EXPORTED);
     }
 
     protected void addButton(String title, String method) {
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index a82b005..b1b3baa 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -65,7 +65,6 @@
 import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.FailureWatcher;
-import com.android.launcher3.util.rule.LauncherActivityRule;
 import com.android.launcher3.util.rule.SamplerRule;
 import com.android.launcher3.util.rule.ScreenRecordRule;
 import com.android.launcher3.util.rule.ShellCommandRule;
@@ -101,7 +100,7 @@
 
     private static boolean sDumpWasGenerated = false;
     private static boolean sActivityLeakReported = false;
-    private static boolean sSeenKeygard = false;
+    private static boolean sSeenKeyguard = false;
 
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
 
@@ -182,8 +181,6 @@
         mLauncher.setOnLauncherCrashed(() -> mLauncherPid = 0);
     }
 
-    protected final LauncherActivityRule mActivityMonitor = new LauncherActivityRule();
-
     @Rule
     public ShellCommandRule mDisableHeadsUpNotification =
             ShellCommandRule.disableHeadsUpNotification();
@@ -204,7 +201,7 @@
     }
 
     protected TestRule getRulesInsideActivityMonitor() {
-        final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(mActivityMonitor::getActivity);
+        final ViewCaptureRule viewCaptureRule = new ViewCaptureRule();
         final RuleChain inner = RuleChain
                 .outerRule(new PortraitLandscapeRunner(this))
                 .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
@@ -219,7 +216,6 @@
     public TestRule mOrderSensitiveRules = RuleChain
             .outerRule(new SamplerRule())
             .around(new TestStabilityRule())
-            .around(mActivityMonitor)
             .around(getRulesInsideActivityMonitor());
 
     public UiDevice getDevice() {
@@ -264,9 +260,9 @@
     }
 
     private static void verifyKeyguardInvisible() {
-        final boolean keyguardAlreadyVisible = sSeenKeygard;
+        final boolean keyguardAlreadyVisible = sSeenKeyguard;
 
-        sSeenKeygard = sSeenKeygard
+        sSeenKeyguard = sSeenKeyguard
                 || !TestHelpers.wait(
                 Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000);
 
@@ -274,7 +270,7 @@
                 "Keyguard is visible, which is likely caused by a crash in SysUI, seeing keyguard"
                         + " for the first time = "
                         + !keyguardAlreadyVisible,
-                sSeenKeygard);
+                sSeenKeyguard);
     }
 
     @After
@@ -322,7 +318,7 @@
 
     protected <T> T getFromLauncher(Function<Launcher, T> f) {
         if (!TestHelpers.isInLauncherProcess()) return null;
-        return getOnUiThread(() -> f.apply(mActivityMonitor.getActivity()));
+        return getOnUiThread(() -> f.apply(Launcher.ACTIVITY_TRACKER.getCreatedActivity()));
     }
 
     protected void executeOnLauncher(Consumer<Launcher> f) {
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 0d63a68..b67478f 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -683,8 +683,8 @@
         HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
         allApps.freeze();
         try {
-            HomeAppIcon icon = allApps.getAppIcon(APP_NAME);
-            assertEquals("Wrong app icon name.", icon.getIconName(), APP_NAME);
+            // getAppIcon() already verifies that the icon is not null and is the correct icon name.
+            allApps.getAppIcon(APP_NAME);
         } finally {
             allApps.unfreeze();
         }
diff --git a/tests/src/com/android/launcher3/ui/WorkProfileTest.java b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
index 7237387..5b9adcd 100644
--- a/tests/src/com/android/launcher3/ui/WorkProfileTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
@@ -85,7 +85,7 @@
         waitForStateTransitionToEnd("Launcher internal state didn't switch to Normal",
                 () -> NORMAL);
         waitForResumed("Launcher internal state is still Background");
-        executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
+        mLauncher.getWorkspace().switchToAllApps();
         waitForStateTransitionToEnd("Launcher internal state didn't switch to All Apps",
                 () -> ALL_APPS);
     }
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index 82bf644..b2ce400 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -30,6 +30,7 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.launcher3.Launcher;
 import com.android.launcher3.celllayout.FavoriteItemsTransaction;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -124,7 +125,10 @@
 
         @Override
         public boolean isTrue() throws Throwable {
-            return mMainThreadExecutor.submit(mActivityMonitor.itemExists(this)).get();
+            return mMainThreadExecutor.submit(() -> {
+                Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+                return l != null && l.getWorkspace().getFirstMatch(this) != null;
+            }).get();
         }
 
         @Override
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index b05ebf8..9dca24b 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -18,7 +18,6 @@
 import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
 
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
 
 import android.platform.test.annotations.PlatinumTest;
 import android.platform.test.rule.ScreenRecordRule;
@@ -27,7 +26,6 @@
 import androidx.test.filters.LargeTest;
 
 import com.android.launcher3.celllayout.FavoriteItemsTransaction;
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.tapl.Widget;
 import com.android.launcher3.tapl.WidgetResizeFrame;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
@@ -70,11 +68,6 @@
                 .getWidget(widgetInfo.getLabel(mTargetContext.getPackageManager()))
                 .dragWidgetToWorkspace();
 
-        assertTrue(mActivityMonitor.itemExists(
-                (info, view) -> info instanceof LauncherAppWidgetInfo &&
-                        ((LauncherAppWidgetInfo) info).providerName.getClassName().equals(
-                                widgetInfo.provider.getClassName())).call());
-
         assertNotNull("Widget resize frame not shown after widget add", resizeFrame);
         resizeFrame.dismiss();
 
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 384fa46..a6b5369 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -32,6 +32,7 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.celllayout.FavoriteItemsTransaction;
 import com.android.launcher3.model.data.ItemInfo;
@@ -193,7 +194,10 @@
 
         @Override
         public boolean isTrue() throws Throwable {
-            return mMainThreadExecutor.submit(mActivityMonitor.itemExists(mOp)).get();
+            return mMainThreadExecutor.submit(() -> {
+                Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+                return l != null && l.getWorkspace().getFirstMatch(mOp) != null;
+            }).get();
         }
     }
 }
diff --git a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
index 7ba0b53..8e5e9cc 100644
--- a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
@@ -147,9 +147,8 @@
                 for (int i = parent.getChildCount() - 1; i >= 0; i--) {
                     viewQueue.add(parent.getChildAt(i));
                 }
-            } else if (view instanceof BubbleTextView) {
-                BubbleTextView btv = (BubbleTextView) view;
-                if (title.equals(btv.getText())) {
+            } else if (view instanceof BubbleTextView btv) {
+                if (title.equals(btv.getContentDescription().toString())) {
                     icon = btv;
                     break;
                 }
diff --git a/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java b/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java
deleted file mode 100644
index e9a52f8..0000000
--- a/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.android.launcher3.util.rule;
-
-import android.app.Activity;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
-
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.util.concurrent.Callable;
-
-/**
- * Test rule to get the current Launcher activity.
- */
-public class LauncherActivityRule extends SimpleActivityRule<Launcher> {
-
-    public LauncherActivityRule() {
-        super(Launcher.class);
-    }
-
-    @Override
-    public Statement apply(Statement base, Description description) {
-
-        return new MyStatement(base) {
-            @Override
-            public void onActivityStarted(Activity activity) {
-                if (activity instanceof Launcher) {
-                    ((Launcher) activity).getRotationHelper().forceAllowRotationForTesting(true);
-                }
-            }
-        };
-    }
-
-    public Callable<Boolean> itemExists(final ItemOperator op) {
-        return () -> {
-            Launcher launcher = getActivity();
-            if (launcher == null) {
-                return false;
-            }
-            return launcher.getWorkspace().getFirstMatch(op) != null;
-        };
-    }
-}
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/util/rule/LazyActivityRule.java b/tests/src/com/android/launcher3/util/rule/LazyActivityRule.java
deleted file mode 100644
index 6c300bb..0000000
--- a/tests/src/com/android/launcher3/util/rule/LazyActivityRule.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.util.rule;
-
-import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
-import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
-import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
-import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ResolveInfo;
-
-import androidx.annotation.Nullable;
-import androidx.test.core.app.ActivityScenario;
-
-import com.android.launcher3.Launcher;
-
-import org.junit.rules.ExternalResource;
-
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.Supplier;
-
-/**
- * Similar to {@code ActivityScenarioRule} but it creates the activity lazily when needed
- */
-public class LazyActivityRule<A extends Activity> extends ExternalResource {
-
-    private final Supplier<ActivityScenario<A>> mScenarioSupplier;
-
-    @Nullable private ActivityScenario<A> mScenario;
-
-    /**
-     * Constructs LazyActivityScenarioRule for a given scenario provider.
-     */
-    public LazyActivityRule(Supplier<ActivityScenario<A>> supplier) {
-        mScenarioSupplier = supplier;
-    }
-
-    /**
-     * Resets the rule, such that the activity is in closed state
-     */
-    public synchronized void reset() {
-        if (mScenario != null) {
-            try {
-                mScenario.close();
-            } catch (AssertionError e) {
-                // Ignore errors during close
-            }
-        }
-        mScenario = null;
-    }
-
-    @Override
-    protected synchronized void after() {
-        reset();
-    }
-
-    /**
-     * Returns the scenario, creating one if it doesn't exist
-     */
-    public synchronized ActivityScenario<A> getScenario() {
-        if (mScenario == null) {
-            mScenario = mScenarioSupplier.get();
-        }
-        return mScenario;
-    }
-
-    /**
-     * Executes the function {@code f} on the activities main thread and returns the result
-     */
-    public <T> T getFromActivity(Function<A, T> f) {
-        AtomicReference<T> result = new AtomicReference<>();
-        getScenario().onActivity(a -> result.set(f.apply(a)));
-        return result.get();
-    }
-
-    /**
-     * Runs the provided function {@code f} on the activity if the scenario is already created
-     */
-    public synchronized void runOnActivity(Consumer<A> f) {
-        if (mScenario != null) {
-            mScenario.onActivity(f::accept);
-        }
-    }
-
-    /**
-     * Returns a {@link LazyActivityRule} for the Launcher activity
-     */
-    public static <T extends Launcher> LazyActivityRule<T> forLauncher() {
-        Context context = getInstrumentation().getTargetContext();
-        // Create the activity after the model setup is done.
-        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
-                .addCategory(Intent.CATEGORY_HOME)
-                .addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK
-                        | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | FLAG_ACTIVITY_NO_ANIMATION);
-        ResolveInfo ri = context.getPackageManager().resolveActivity(
-                new Intent(homeIntent).setPackage(context.getPackageName()), 0);
-        homeIntent.setComponent(ri.getComponentInfo().getComponentName());
-        return new LazyActivityRule<>(() -> ActivityScenario.launch(homeIntent));
-    }
-}
diff --git a/tests/src/com/android/launcher3/util/rule/SimpleActivityRule.java b/tests/src/com/android/launcher3/util/rule/SimpleActivityRule.java
deleted file mode 100644
index b5d8193..0000000
--- a/tests/src/com/android/launcher3/util/rule/SimpleActivityRule.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.android.launcher3.util.rule;
-
-import android.app.Activity;
-import android.app.Application;
-import android.app.Application.ActivityLifecycleCallbacks;
-import android.os.Bundle;
-
-import androidx.test.InstrumentationRegistry;
-
-import com.android.launcher3.testing.shared.TestProtocol;
-
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-/**
- * Test rule to get the current activity.
- */
-public class SimpleActivityRule<T extends Activity> implements TestRule {
-
-    private final Class<T> mClass;
-    private T mActivity;
-
-    public SimpleActivityRule(Class<T> clazz) {
-        mClass = clazz;
-    }
-
-    @Override
-    public Statement apply(Statement base, Description description) {
-        return new MyStatement(base);
-    }
-
-    public T getActivity() {
-        return mActivity;
-    }
-
-    protected class MyStatement extends Statement implements ActivityLifecycleCallbacks {
-
-        private final Statement mBase;
-
-        public MyStatement(Statement base) {
-            mBase = base;
-        }
-
-        @Override
-        public void evaluate() throws Throwable {
-            Application app = (Application)
-                    InstrumentationRegistry.getTargetContext().getApplicationContext();
-            app.registerActivityLifecycleCallbacks(this);
-            try {
-                mBase.evaluate();
-            } finally {
-                app.unregisterActivityLifecycleCallbacks(this);
-                mActivity = null;
-            }
-        }
-
-        @Override
-        public void onActivityCreated(Activity activity, Bundle bundle) {
-            if (activity != null && mClass.isInstance(activity)) {
-                TestProtocol.testLogD(
-                        TestProtocol.ACTIVITY_LIFECYCLE_RULE, "MyStatement.onActivityCreated");
-                mActivity = (T) activity;
-            }
-        }
-
-        @Override
-        public void onActivityStarted(Activity activity) {
-            if (activity == mActivity) {
-                TestProtocol.testLogD(
-                        TestProtocol.ACTIVITY_LIFECYCLE_RULE, "MyStatement.onActivityStarted");
-            }
-        }
-
-        @Override
-        public void onActivityResumed(Activity activity) {
-            if (activity == mActivity) {
-                TestProtocol.testLogD(
-                        TestProtocol.ACTIVITY_LIFECYCLE_RULE, "MyStatement.onActivityResumed");
-            }
-        }
-
-        @Override
-        public void onActivityPaused(Activity activity) {
-            if (activity == mActivity) {
-                TestProtocol.testLogD(
-                        TestProtocol.ACTIVITY_LIFECYCLE_RULE, "MyStatement.onActivityPaused");
-            }
-        }
-
-        @Override
-        public void onActivityStopped(Activity activity) {
-            if (activity == mActivity) {
-                TestProtocol.testLogD(
-                        TestProtocol.ACTIVITY_LIFECYCLE_RULE, "MyStatement.onAcgtivityStopped");
-            }
-        }
-
-        @Override
-        public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
-            if (activity == mActivity) {
-                TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE,
-                        "MyStatement.onActivitySaveInstanceState");
-            }
-        }
-
-        @Override
-        public void onActivityDestroyed(Activity activity) {
-            if (activity == mActivity) {
-                TestProtocol.testLogD(
-                        TestProtocol.ACTIVITY_LIFECYCLE_RULE, "MyStatement.onActivityDestroyed");
-                mActivity = null;
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
index b2a2f7f..1ca4434 100644
--- a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
+++ b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
@@ -25,7 +25,6 @@
 import com.android.app.viewcapture.data.ExportedData
 import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter
 import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer
-import java.util.function.Supplier
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
@@ -36,7 +35,7 @@
  *
  * This rule will not work in OOP tests that don't have access to the activity under test.
  */
-class ViewCaptureRule(var alreadyOpenActivitySupplier: Supplier<Activity?>) : TestRule {
+class ViewCaptureRule : TestRule {
     private val viewCapture = SimpleViewCapture("test-view-capture")
     var viewCaptureData: ExportedData? = null
         private set
@@ -47,20 +46,13 @@
                 viewCaptureData = null
                 val windowListenerCloseables = mutableListOf<SafeCloseable>()
 
-                val alreadyOpenActivity = alreadyOpenActivitySupplier.get()
-                if (alreadyOpenActivity != null) {
-                    startCapture(windowListenerCloseables, alreadyOpenActivity)
-                }
-
                 val lifecycleCallbacks =
                     object : ActivityLifecycleCallbacksAdapter {
                         override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
-                            super.onActivityCreated(activity, bundle)
                             startCapture(windowListenerCloseables, activity)
                         }
 
                         override fun onActivityDestroyed(activity: Activity) {
-                            super.onActivityDestroyed(activity)
                             viewCapture.stopCapture(activity.window.decorView)
                         }
                     }
diff --git a/tests/src/com/android/launcher3/util/rule/WrapperRule.kt b/tests/src/com/android/launcher3/util/rule/WrapperRule.kt
deleted file mode 100644
index 290cc45..0000000
--- a/tests/src/com/android/launcher3/util/rule/WrapperRule.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.android.launcher3.util.rule
-
-import com.android.launcher3.config.FeatureFlags.BooleanFlag
-import com.android.launcher3.config.FeatureFlags.IntFlag
-import com.android.launcher3.util.SafeCloseable
-import com.android.launcher3.util.TestUtil
-import java.util.function.Supplier
-import org.junit.rules.ExternalResource
-
-/** Simple rule which wraps any SafeCloseable object */
-class WrapperRule(private val overrideProvider: Supplier<SafeCloseable>) : ExternalResource() {
-
-    private lateinit var overrideClosable: SafeCloseable
-
-    override fun before() {
-        overrideClosable = overrideProvider.get()
-    }
-
-    override fun after() {
-        overrideClosable.close()
-    }
-
-    companion object {
-
-        fun BooleanFlag.overrideFlag(value: Boolean) = WrapperRule {
-            TestUtil.overrideFlag(this, value)
-        }
-
-        fun IntFlag.overrideFlag(value: Int) = WrapperRule { TestUtil.overrideFlag(this, value) }
-    }
-}
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 399abc7..23d09d4 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -210,6 +210,9 @@
     public AppIcon getAppIcon(String appName) {
         AppIcon appIcon = tryGetAppIcon(appName);
         mLauncher.assertNotNull("Unable to scroll to a clickable icon: " + appName, appIcon);
+        // appIcon.getAppName() checks for content description, so it is possible that it can have
+        // trailing words. So check if the content description contains the appName.
+        mLauncher.assertTrue("Wrong app icon name.", appIcon.getAppName().contains(appName));
         return appIcon;
     }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 0a0cf07..85098c8 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -37,10 +37,14 @@
     }
 
     static BySelector getAppIconSelector(String appName, LauncherInstrumentation launcher) {
-        return By.clazz(TextView.class).textContains(appName)
+        return By.clazz(TextView.class).desc(makeMultilinePattern(appName))
                 .pkg(launcher.getLauncherPackageName());
     }
 
+    static BySelector getMenuItemSelector(String text, LauncherInstrumentation launcher) {
+        return By.clazz(TextView.class).text(text).pkg(launcher.getLauncherPackageName());
+    }
+
     static BySelector getAnyAppIconSelector() {
         return By.clazz(TextView.class);
     }
@@ -94,4 +98,24 @@
     public String getIconName() {
         return getObject().getText();
     }
+
+    /**
+     * Return the app name of a icon by the content description. This should be used when trying to
+     * get the name of an app where the text of it is multiline.
+     */
+    @NonNull
+    String getAppName() {
+        return getObject().getContentDescription();
+    }
+
+    /**
+     * Create a regular expression pattern that matches strings starting with the app name, where
+     * spaces in the app name are replaced with zero or more occurrences of the "\s" character
+     * (which represents a whitespace character in regular expressions), followed by any characters
+     * after the app name.
+     */
+    static Pattern makeMultilinePattern(String appName) {
+        return Pattern.compile(appName.replaceAll("\\s+", "\\\\s*") + ".*",
+                Pattern.DOTALL);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java b/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java
index 667290f..bbcc6a8 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java
@@ -50,7 +50,7 @@
      */
     public AppIconMenuItem getMenuItem(String shortcutText) {
         final UiObject2 menuItem = mLauncher.waitForObjectInContainer(mDeepShortcutsContainer,
-                AppIcon.getAppIconSelector(shortcutText, mLauncher));
+                AppIcon.getMenuItemSelector(shortcutText, mLauncher));
         return createMenuItem(menuItem);
     }
 
@@ -59,7 +59,7 @@
      */
     public SplitScreenMenuItem getSplitScreenMenuItem() {
         final UiObject2 menuItem = mLauncher.waitForObjectInContainer(mDeepShortcutsContainer,
-                AppIcon.getAppIconSelector("Split screen", mLauncher));
+                AppIcon.getMenuItemSelector("Split screen", mLauncher));
         return new SplitScreenMenuItem(mLauncher, menuItem);
     }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index a59eff7..9b4d273 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -37,6 +37,7 @@
 import androidx.test.uiautomator.Condition;
 import androidx.test.uiautomator.UiDevice;
 
+import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.testing.shared.TestProtocol;
 
 /**
@@ -50,6 +51,8 @@
     // UNSTASHED_TASKBAR_HANDLE_HINT_SCALE value from TaskbarStashController.
     private static final float UNSTASHED_TASKBAR_HANDLE_HINT_SCALE = 1.1f;
 
+    private static final int STASHED_TASKBAR_BOTTOM_EDGE_DP = 1;
+
     private final Condition<UiDevice, Boolean> mStashedTaskbarHintScaleCondition =
             device -> mLauncher.getTestInfo(REQUEST_STASHED_TASKBAR_SCALE).getFloat(
                     TestProtocol.TEST_INFO_RESPONSE_FIELD) - UNSTASHED_TASKBAR_HANDLE_HINT_SCALE
@@ -209,7 +212,7 @@
      *
      * <p>This unstashing occurs when not actively hovering the taskbar.
      */
-    public void hoverScreenBottomEdgeToUnstashTaskbar() {
+    public Taskbar hoverScreenBottomEdgeToUnstashTaskbar() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "cursor hover entering screen edge to unstash taskbar")) {
@@ -226,13 +229,15 @@
 
             mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
                     new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null);
+
+            return new Taskbar(mLauncher);
         }
     }
 
     /**
      * Emulate the cursor hovering the taskbar to get unstash hint, then hovering below to unstash.
      */
-    public void hoverBelowHintedTaskbarToUnstash() {
+    public Taskbar hoverBelowHintedTaskbarToUnstash() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "cursor hover entering stashed taskbar")) {
@@ -254,6 +259,7 @@
                         new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null);
 
                 mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
+                return new Taskbar(mLauncher);
             }
         }
     }
@@ -288,4 +294,45 @@
             }
         }
     }
+
+    /**
+     * Emulate the cursor clicking the stashed taskbar to go home.
+     */
+    public Workspace clickStashedTaskbarToGoHome() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "cursor hover entering stashed taskbar")) {
+            long downTime = SystemClock.uptimeMillis();
+            int stashedTaskbarBottomEdge = ResourceUtils.pxFromDp(STASHED_TASKBAR_BOTTOM_EDGE_DP,
+                    mLauncher.getResources().getDisplayMetrics());
+            Point stashedTaskbarHintArea = new Point(mLauncher.getRealDisplaySize().x / 2,
+                    mLauncher.getRealDisplaySize().y - stashedTaskbarBottomEdge - 1);
+            mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_ENTER,
+                    new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y), null);
+
+            mLauncher.getDevice().wait(mStashedTaskbarHintScaleCondition,
+                    LauncherInstrumentation.WAIT_TIME_MS);
+
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "cursor clicking stashed taskbar to go home")) {
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        null);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        LauncherInstrumentation.GestureScope.OUTSIDE_WITHOUT_PILFER);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_BUTTON_PRESS,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        null);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_BUTTON_RELEASE,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        null);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        LauncherInstrumentation.GestureScope.OUTSIDE_WITHOUT_PILFER);
+
+                return mLauncher.getWorkspace();
+            }
+        }
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 89f141f..b929293 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -99,17 +99,22 @@
     private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 15;
     private static final int GESTURE_STEP_MS = 16;
 
-    static final Pattern EVENT_TOUCH_DOWN = getTouchEventPattern("ACTION_DOWN");
-    static final Pattern EVENT_TOUCH_UP = getTouchEventPattern("ACTION_UP");
-    private static final Pattern EVENT_TOUCH_CANCEL = getTouchEventPattern("ACTION_CANCEL");
+    static final Pattern EVENT_TOUCH_DOWN = getTouchEventPatternWithPointerCount("ACTION_DOWN");
+    static final Pattern EVENT_TOUCH_UP = getTouchEventPatternWithPointerCount("ACTION_UP");
+    private static final Pattern EVENT_TOUCH_CANCEL = getTouchEventPatternWithPointerCount(
+            "ACTION_CANCEL");
     static final Pattern EVENT_PILFER_POINTERS = Pattern.compile("pilferPointers");
     static final Pattern EVENT_START = Pattern.compile("start:");
 
     static final Pattern EVENT_TOUCH_DOWN_TIS = getTouchEventPatternTIS("ACTION_DOWN");
     static final Pattern EVENT_TOUCH_UP_TIS = getTouchEventPatternTIS("ACTION_UP");
-    static final Pattern EVENT_TOUCH_CANCEL_TIS = getTouchEventPatternTIS("ACTION_CANCEL");
+    static final Pattern EVENT_TOUCH_CANCEL_TIS = getTouchEventPattern(
+            "TouchInteractionService.onInputEvent", "ACTION_CANCEL");
     static final Pattern EVENT_HOVER_ENTER_TIS = getTouchEventPatternTIS("ACTION_HOVER_ENTER");
     static final Pattern EVENT_HOVER_EXIT_TIS = getTouchEventPatternTIS("ACTION_HOVER_EXIT");
+    static final Pattern EVENT_BUTTON_PRESS_TIS = getTouchEventPatternTIS("ACTION_BUTTON_PRESS");
+    static final Pattern EVENT_BUTTON_RELEASE_TIS =
+            getTouchEventPatternTIS("ACTION_BUTTON_RELEASE");
 
     private static final Pattern EVENT_KEY_BACK_DOWN =
             getKeyEventPattern("ACTION_DOWN", "KEYCODE_BACK");
@@ -173,7 +178,7 @@
         void close();
     }
 
-    private static final String WORKSPACE_RES_ID = "workspace";
+    static final String WORKSPACE_RES_ID = "workspace";
     private static final String APPS_RES_ID = "apps_view";
     private static final String OVERVIEW_RES_ID = "overview_panel";
     private static final String WIDGETS_RES_ID = "primary_widgets_list_view";
@@ -209,30 +214,35 @@
     private int mPointerCount = 0;
 
     private static Pattern getTouchEventPattern(String prefix, String action) {
-        return getTouchEventPattern(prefix, action, 1);
+        return Pattern.compile(
+                prefix + ": MotionEvent.*?action=" + action + ".*?id\\[0\\]=0"
+                        + ".*?toolType\\[0\\]=TOOL_TYPE_FINGER.*?buttonState=0.*?");
     }
 
-    private static Pattern getTouchEventPattern(String prefix, String action, int pointerCount) {
+    private static Pattern getTouchEventPatternWithPointerCount(String prefix, String action,
+            int pointerCount) {
         return Pattern.compile(
                 prefix + ": MotionEvent.*?action=" + action + ".*?id\\[0\\]=0"
                         + ".*?toolType\\[0\\]=TOOL_TYPE_FINGER.*?buttonState=0.*?pointerCount="
                         + pointerCount);
     }
 
-    private static Pattern getTouchEventPattern(String action) {
-        return getTouchEventPattern("Touch event", action);
+    private static Pattern getTouchEventPatternWithPointerCount(String action) {
+        return getTouchEventPatternWithPointerCount("Touch event", action, 1);
     }
 
-    private static Pattern getTouchEventPattern(String action, int pointerCount) {
-        return getTouchEventPattern("Touch event", action, pointerCount);
+    private static Pattern getTouchEventPatternWithPointerCount(String action, int pointerCount) {
+        return getTouchEventPatternWithPointerCount("Touch event", action, pointerCount);
     }
 
     private static Pattern getTouchEventPatternTIS(String action) {
-        return getTouchEventPattern("TouchInteractionService.onInputEvent", action);
+        return getTouchEventPatternWithPointerCount("TouchInteractionService.onInputEvent", action,
+                1);
     }
 
     private static Pattern getTouchEventPatternTIS(String action, int pointerCount) {
-        return getTouchEventPattern("TouchInteractionService.onInputEvent", action, pointerCount);
+        return getTouchEventPatternWithPointerCount("TouchInteractionService.onInputEvent", action,
+                pointerCount);
     }
 
     private static Pattern getKeyEventPattern(String action, String keyCode) {
@@ -1811,11 +1821,14 @@
                         && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER
                         && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE
                         && (!isTrackpadGesture || isTwoFingerTrackpadGesture)) {
-                    expectEvent(TestProtocol.SEQUENCE_MAIN,
-                            getTouchEventPattern("ACTION_POINTER_DOWN", mPointerCount));
+                    expectEvent(TestProtocol.SEQUENCE_MAIN, getTouchEventPatternWithPointerCount(
+                            "ACTION_POINTER_DOWN", mPointerCount));
                 }
-                expectEvent(TestProtocol.SEQUENCE_TIS, getTouchEventPatternTIS(
-                        "ACTION_POINTER_DOWN", mPointerCount));
+                if (hasTIS && (isTrackpadGestureEnabled()
+                        || getNavigationModel() != NavigationModel.THREE_BUTTON)) {
+                    expectEvent(TestProtocol.SEQUENCE_TIS, getTouchEventPatternTIS(
+                            "ACTION_POINTER_DOWN", mPointerCount));
+                }
                 pointerCount = mPointerCount;
                 break;
             case MotionEvent.ACTION_POINTER_UP:
@@ -1823,17 +1836,26 @@
                         && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER
                         && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE
                         && (!isTrackpadGesture || isTwoFingerTrackpadGesture)) {
-                    expectEvent(TestProtocol.SEQUENCE_MAIN,
-                            getTouchEventPattern("ACTION_POINTER_UP", mPointerCount));
-                }
-                // When the gesture is handled outside, it's cancelled within launcher.
-                if (gestureScope != GestureScope.INSIDE_TO_OUTSIDE_WITH_KEYCODE
-                        && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE) {
-                    expectEvent(TestProtocol.SEQUENCE_TIS, getTouchEventPatternTIS(
+                    expectEvent(TestProtocol.SEQUENCE_MAIN, getTouchEventPatternWithPointerCount(
                             "ACTION_POINTER_UP", mPointerCount));
                 }
+                // When the gesture is handled outside, it's cancelled within launcher.
+                if (hasTIS && (isTrackpadGestureEnabled()
+                        || getNavigationModel() != NavigationModel.THREE_BUTTON)) {
+                    if (gestureScope != GestureScope.INSIDE_TO_OUTSIDE_WITH_KEYCODE
+                            && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE) {
+                        expectEvent(TestProtocol.SEQUENCE_TIS, getTouchEventPatternTIS(
+                                "ACTION_POINTER_UP", mPointerCount));
+                    }
+                }
                 mPointerCount--;
                 break;
+            case MotionEvent.ACTION_BUTTON_PRESS:
+                expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_BUTTON_PRESS_TIS);
+                break;
+            case MotionEvent.ACTION_BUTTON_RELEASE:
+                expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_BUTTON_RELEASE_TIS);
+                break;
         }
 
         final MotionEvent event = isTrackpadGesture
@@ -1841,6 +1863,10 @@
                         downTime, currentTime, action, point.x, point.y, pointerCount,
                         mTrackpadGestureType)
                 : getMotionEvent(downTime, currentTime, action, point.x, point.y);
+        if (action == MotionEvent.ACTION_BUTTON_PRESS
+                || action == MotionEvent.ACTION_BUTTON_RELEASE) {
+            event.setActionButton(MotionEvent.BUTTON_PRIMARY);
+        }
         assertTrue("injectInputEvent failed",
                 mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
         event.recycle();