Merge "Unit Testing for TaskbarEduTooltipController" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 21b9863..a779641 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -316,3 +316,16 @@
     description: "Archived apps will use new icon in app title"
     bug: "350758155"
 }
+
+flag {
+    name: "enable_multi_instance_menu_taskbar"
+    namespace: "launcher"
+    description: "Menu in Taskbar with options to launch and manage multiple instances of the same app"
+    bug: "355237285"
+}
+flag {
+    name: "navigate_to_child_preference"
+    namespace: "launcher"
+    description: "Settings screen supports navigating to child preference if the key is not on the screen"
+    bug: "293390881"
+}
diff --git a/go/quickstep/res/values/styles.xml b/go/quickstep/res/values/styles.xml
index c659331..2524c76 100644
--- a/go/quickstep/res/values/styles.xml
+++ b/go/quickstep/res/values/styles.xml
@@ -16,7 +16,7 @@
 -->
 <resources>
     <!-- App themes -->
-    <style name="LauncherTheme" parent="@style/BaseLauncherTheme">
+    <style name="LauncherTheme" parent="@style/DynamicColorsBaseLauncherTheme">
         <item name="overviewButtonTextColor">@color/go_overview_text_color</item>
         <item name="overviewButtonIconColor">@color/go_overview_text_color</item>
         <item name="overviewButtonBackgroundColor">@color/go_overview_button_color</item>
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index 80d8154..c6e2d8c 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -48,7 +48,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 4abf6e1..bf198b6 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -80,7 +80,7 @@
              android:stateNotNeeded="true"
              android:theme="@style/LauncherTheme"
              android:screenOrientation="behind"
-             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
+             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
              android:resizeableActivity="true"
              android:resumeWhilePausing="true"
              android:enableOnBackInvokedCallback="false"
diff --git a/quickstep/res/color/all_set_bg_primary.xml b/quickstep/res/color/all_set_bg_primary.xml
index 4cf857d..013de7a 100644
--- a/quickstep/res/color/all_set_bg_primary.xml
+++ b/quickstep/res/color/all_set_bg_primary.xml
@@ -15,5 +15,5 @@
 -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <item android:color="?androidprv:attr/materialColorPrimaryContainer"/>
+    <item android:color="?attr/materialColorPrimaryContainer"/>
 </selector>
diff --git a/quickstep/res/color/all_set_bg_tertiary.xml b/quickstep/res/color/all_set_bg_tertiary.xml
index e62b094..b58d61c 100644
--- a/quickstep/res/color/all_set_bg_tertiary.xml
+++ b/quickstep/res/color/all_set_bg_tertiary.xml
@@ -15,5 +15,5 @@
 -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <item android:color="?androidprv:attr/materialColorTertiary"/>
+    <item android:color="?attr/materialColorTertiary"/>
 </selector>
diff --git a/quickstep/res/color/bubblebar_drop_target_bg_color.xml b/quickstep/res/color/bubblebar_drop_target_bg_color.xml
index ca37c7f..bae8c4e 100644
--- a/quickstep/res/color/bubblebar_drop_target_bg_color.xml
+++ b/quickstep/res/color/bubblebar_drop_target_bg_color.xml
@@ -15,5 +15,5 @@
   -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <item android:alpha="0.35" android:color="?androidprv:attr/materialColorPrimaryContainer" />
+    <item android:alpha="0.35" android:color="?attr/materialColorPrimaryContainer" />
 </selector>
\ No newline at end of file
diff --git a/quickstep/res/color/menu_item_hover_state_color.xml b/quickstep/res/color/menu_item_hover_state_color.xml
index 1dd7654..3c68789 100644
--- a/quickstep/res/color/menu_item_hover_state_color.xml
+++ b/quickstep/res/color/menu_item_hover_state_color.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <selector xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <item android:state_hovered="false" android:color="?androidprv:attr/materialColorSurfaceBright" />
-    <item android:state_hovered="true" android:color="?androidprv:attr/materialColorSurfaceVariant" />
+    <item android:state_hovered="false" android:color="?attr/materialColorSurfaceBright" />
+    <item android:state_hovered="true" android:color="?attr/materialColorSurfaceVariant" />
 </selector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_bubble_bar_drop_target.xml b/quickstep/res/drawable/bg_bubble_bar_drop_target.xml
index 79e4318..f597cb5 100644
--- a/quickstep/res/drawable/bg_bubble_bar_drop_target.xml
+++ b/quickstep/res/drawable/bg_bubble_bar_drop_target.xml
@@ -20,5 +20,5 @@
     <solid android:color="@color/bubblebar_drop_target_bg_color" />
     <stroke
         android:width="1dp"
-        android:color="?androidprv:attr/materialColorPrimaryContainer" />
+        android:color="?attr/materialColorPrimaryContainer" />
 </shape>
diff --git a/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml b/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml
index d722dd7..169e396 100644
--- a/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml
+++ b/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml
@@ -22,6 +22,6 @@
         <solid android:color="@color/bubblebar_drop_target_bg_color" />
         <stroke
             android:width="1dp"
-            android:color="?androidprv:attr/materialColorPrimaryContainer" />
+            android:color="?attr/materialColorPrimaryContainer" />
     </shape>
 </inset>
diff --git a/quickstep/res/drawable/bg_floating_desktop_select.xml b/quickstep/res/drawable/bg_floating_desktop_select.xml
index d7df338..6481be4 100644
--- a/quickstep/res/drawable/bg_floating_desktop_select.xml
+++ b/quickstep/res/drawable/bg_floating_desktop_select.xml
@@ -19,5 +19,5 @@
     android:shape="rectangle">
 
     <corners android:radius="@dimen/rounded_button_radius" />
-    <solid android:color="?androidprv:attr/materialColorPrimaryContainer" />
+    <solid android:color="?attr/materialColorPrimaryContainer" />
 </shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_overview_clear_all_button.xml b/quickstep/res/drawable/bg_overview_clear_all_button.xml
index 143761f..0d12274 100644
--- a/quickstep/res/drawable/bg_overview_clear_all_button.xml
+++ b/quickstep/res/drawable/bg_overview_clear_all_button.xml
@@ -21,7 +21,7 @@
         <shape android:shape="rectangle"
             android:tint="?colorButtonNormal">
             <corners android:radius="@dimen/recents_clear_all_outline_radius" />
-            <solid android:color="?androidprv:attr/materialColorSurfaceBright"/>
+            <solid android:color="?attr/materialColorSurfaceBright"/>
         </shape>
     </item>
 </ripple>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml b/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml
index 20c524d..9e9bb2b 100644
--- a/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml
+++ b/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml
@@ -18,5 +18,5 @@
     android:shape="rectangle">
 
     <corners android:radius="@dimen/dialogCornerRadius" />
-    <solid android:color="?androidprv:attr/materialColorSurfaceBright"/>
+    <solid android:color="?attr/materialColorSurfaceBright"/>
 </shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_wellbeing_toast.xml b/quickstep/res/drawable/bg_wellbeing_toast.xml
index 0b66849..418caae 100644
--- a/quickstep/res/drawable/bg_wellbeing_toast.xml
+++ b/quickstep/res/drawable/bg_wellbeing_toast.xml
@@ -16,6 +16,6 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:shape="rectangle">
-    <solid android:color="?androidprv:attr/materialColorSecondaryFixed" />
+    <solid android:color="?attr/materialColorSecondaryFixed" />
     <corners android:radius="?android:attr/dialogCornerRadius" />
 </shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_chevron_down.xml b/quickstep/res/drawable/ic_chevron_down.xml
index f246cbc..b586e50 100644
--- a/quickstep/res/drawable/ic_chevron_down.xml
+++ b/quickstep/res/drawable/ic_chevron_down.xml
@@ -19,7 +19,7 @@
     android:width="48dp"
     android:height="48dp"
     android:autoMirrored="true"
-    android:tint="?androidprv:attr/materialColorOnSurface"
+    android:tint="?attr/materialColorOnSurface"
     android:viewportHeight="48"
     android:viewportWidth="48">
     <group
diff --git a/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml b/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml
index 2a4f087..8180293 100644
--- a/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml
+++ b/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml
@@ -17,6 +17,6 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:shape="rectangle">
-    <solid android:color="?androidprv:attr/materialColorSurfaceBright" />
+    <solid android:color="?attr/materialColorSurfaceBright" />
     <corners android:radius="@dimen/keyboard_quick_switch_task_view_radius" />
 </shape>
diff --git a/quickstep/res/drawable/rotate_tutorial_warning.xml b/quickstep/res/drawable/rotate_tutorial_warning.xml
index d1117a4..90b7d64 100644
--- a/quickstep/res/drawable/rotate_tutorial_warning.xml
+++ b/quickstep/res/drawable/rotate_tutorial_warning.xml
@@ -21,6 +21,6 @@
     android:viewportHeight="960"
     android:viewportWidth="960">
     <path
-        android:fillColor="?androidprv:attr/materialColorOnSurface"
+        android:fillColor="?attr/materialColorOnSurface"
         android:pathData="M40,840L480,80L920,840L40,840ZM178,760L782,760L480,240L178,760ZM480,720Q497,720 508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720ZM440,600L520,600L520,400L440,400L440,600ZM480,500L480,500L480,500L480,500Z" />
 </vector>
diff --git a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
index 38df756..613edac 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:focusBorderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?attr/materialColorOutline">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content"
diff --git a/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml b/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml
index e4942ae..40d2322 100644
--- a/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml
+++ b/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml
@@ -20,7 +20,7 @@
     android:theme="@style/GestureTutorialActivity"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:background="?androidprv:attr/materialColorSurfaceContainer"
+    android:background="?attr/materialColorSurfaceContainer"
     android:fitsSystemWindows="true">
 
     <androidx.constraintlayout.widget.ConstraintLayout
@@ -163,7 +163,7 @@
             android:layout_marginVertical="16dp"
             android:text="@string/gesture_tutorial_action_button_label"
             android:background="@drawable/gesture_tutorial_action_button_background"
-            android:backgroundTint="?androidprv:attr/materialColorPrimary"
+            android:backgroundTint="?attr/materialColorPrimary"
             android:stateListAnimator="@null"
 
             app:layout_constraintTop_toBottomOf="@id/guideline"
diff --git a/quickstep/res/layout/digital_wellbeing_toast.xml b/quickstep/res/layout/digital_wellbeing_toast.xml
index 9144c7f..6a99a3b 100644
--- a/quickstep/res/layout/digital_wellbeing_toast.xml
+++ b/quickstep/res/layout/digital_wellbeing_toast.xml
@@ -24,7 +24,7 @@
     android:forceHasOverlappingRendering="false"
     android:gravity="center"
     android:importantForAccessibility="noHideDescendants"
-    android:textColor="?androidprv:attr/materialColorOnSecondaryFixed"
+    android:textColor="?attr/materialColorOnSecondaryFixed"
     android:textSize="14sp"
     android:autoSizeTextType="uniform"
     android:autoSizeMaxTextSize="14sp"/>
\ No newline at end of file
diff --git a/quickstep/res/layout/gesture_tutorial_step_menu.xml b/quickstep/res/layout/gesture_tutorial_step_menu.xml
index 668a2e1..7feb882 100644
--- a/quickstep/res/layout/gesture_tutorial_step_menu.xml
+++ b/quickstep/res/layout/gesture_tutorial_step_menu.xml
@@ -20,7 +20,7 @@
     android:theme="@style/GestureTutorialActivity"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:background="?androidprv:attr/materialColorSurfaceContainer"
+    android:background="?attr/materialColorSurfaceContainer"
     android:fitsSystemWindows="true">
 
     <androidx.constraintlayout.widget.ConstraintLayout
@@ -161,7 +161,7 @@
             android:layout_marginVertical="16dp"
             android:text="@string/gesture_tutorial_action_button_label"
             android:background="@drawable/gesture_tutorial_action_button_background"
-            android:backgroundTint="?androidprv:attr/materialColorPrimary"
+            android:backgroundTint="?attr/materialColorPrimary"
             android:stateListAnimator="@null"
 
             app:layout_constraintTop_toBottomOf="@id/guideline"
diff --git a/quickstep/res/layout/icon_app_chip_view.xml b/quickstep/res/layout/icon_app_chip_view.xml
index fb9bf99..36ece2a 100644
--- a/quickstep/res/layout/icon_app_chip_view.xml
+++ b/quickstep/res/layout/icon_app_chip_view.xml
@@ -26,7 +26,7 @@
     android:importantForAccessibility="no"
     android:autoMirrored="true"
     android:elevation="@dimen/task_thumbnail_icon_menu_elevation"
-    android:background="?androidprv:attr/materialColorSurfaceBright">
+    android:background="?attr/materialColorSurfaceBright">
 
     <!-- ignoring warning because the user of the anchor is a Rect where RTL is not needed -->
     <!-- This anchor's bounds is in the expected location after rotations and translations are
diff --git a/quickstep/res/layout/keyboard_quick_switch_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_taskview.xml
index c0ace9a..8f09176 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:focusBorderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?attr/materialColorOutline">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content"
diff --git a/quickstep/res/layout/keyboard_quick_switch_textonly_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_textonly_taskview.xml
index e48794e..c76a2e3 100644
--- a/quickstep/res/layout/keyboard_quick_switch_textonly_taskview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_textonly_taskview.xml
@@ -22,7 +22,7 @@
     android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
     android:clipToOutline="true"
     android:importantForAccessibility="yes"
-    launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
+    launcher:focusBorderColor="?attr/materialColorOutline">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content"
@@ -40,7 +40,7 @@
             android:layout_width="@dimen/keyboard_quick_switch_recents_icon_size"
             android:layout_height="@dimen/keyboard_quick_switch_recents_icon_size"
             android:layout_marginBottom="8dp"
-            android:tint="?androidprv:attr/materialColorOnSurface"
+            android:tint="?attr/materialColorOnSurface"
 
             app:layout_constraintVertical_chainStyle="packed"
             app:layout_constraintTop_toTopOf="parent"
diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml
index 2bba788..2420a46 100644
--- a/quickstep/res/layout/keyboard_quick_switch_view.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_view.xml
@@ -43,7 +43,7 @@
             android:layout_height="@dimen/keyboard_quick_switch_no_recent_items_icon_size"
             android:layout_marginBottom="@dimen/keyboard_quick_switch_no_recent_items_icon_margin"
             android:src="@drawable/view_carousel"
-            android:tint="?androidprv:attr/materialColorOnSurface"
+            android:tint="?attr/materialColorOnSurface"
             android:importantForAccessibility="no"
 
             app:layout_constraintVertical_chainStyle="packed"
diff --git a/quickstep/res/layout/overview_clear_all_button.xml b/quickstep/res/layout/overview_clear_all_button.xml
index 3380ea4..40f38f4 100644
--- a/quickstep/res/layout/overview_clear_all_button.xml
+++ b/quickstep/res/layout/overview_clear_all_button.xml
@@ -23,6 +23,6 @@
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:text="@string/recents_clear_all"
-    android:textColor="?androidprv:attr/materialColorOnSurface"
-    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
+    android:textColor="?attr/materialColorOnSurface"
+    launcher:focusBorderColor="?attr/materialColorOutline"
     android:textSize="14sp" />
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 46c1332..ac1a50a 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -25,8 +25,8 @@
     android:clipChildren="false"
     android:defaultFocusHighlightEnabled="false"
     android:focusable="true"
-    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
-    launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
+    launcher:focusBorderColor="?attr/materialColorOutline"
+    launcher:hoverBorderColor="?attr/materialColorPrimary">
 
     <include layout="@layout/task_thumbnail_deprecated" />
 
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index 453057c..64aa7e1 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -25,8 +25,8 @@
     android:clipChildren="true"
     android:defaultFocusHighlightEnabled="false"
     android:focusable="true"
-    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
-    launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary"
+    launcher:focusBorderColor="?attr/materialColorOutline"
+    launcher:hoverBorderColor="?attr/materialColorPrimary"
     android:clipToPadding="true"
     android:padding="0.1dp">
     <!-- Setting a padding of 0.1 dp since android:clipToPadding needs a non-zero value for
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index 708aa3c..da2b29f 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -30,8 +30,8 @@
     android:clipChildren="false"
     android:defaultFocusHighlightEnabled="false"
     android:focusable="true"
-    launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
-    launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
+    launcher:focusBorderColor="?attr/materialColorOutline"
+    launcher:hoverBorderColor="?attr/materialColorPrimary">
 
     <include layout="@layout/task_thumbnail_deprecated"/>
 
diff --git a/quickstep/res/layout/task_thumbnail.xml b/quickstep/res/layout/task_thumbnail.xml
index 34640e6..784a094 100644
--- a/quickstep/res/layout/task_thumbnail.xml
+++ b/quickstep/res/layout/task_thumbnail.xml
@@ -23,6 +23,7 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:importantForAccessibility="no"
+        android:scaleType="matrix"
         android:visibility="gone"/>
 
     <com.android.quickstep.task.thumbnail.LiveTileView
@@ -38,4 +39,16 @@
         android:background="@color/overview_foreground_scrim_color"
         android:alpha="0" />
 
+    <FrameLayout
+        android:id="@+id/splash_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="gone">
+        <ImageView
+            android:id="@+id/splash_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:importantForAccessibility="no" />
+    </FrameLayout>
 </com.android.quickstep.task.thumbnail.TaskThumbnailView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task_view_menu_option.xml b/quickstep/res/layout/task_view_menu_option.xml
index ffe2401..5218de0 100644
--- a/quickstep/res/layout/task_view_menu_option.xml
+++ b/quickstep/res/layout/task_view_menu_option.xml
@@ -31,7 +31,7 @@
       android:layout_height="@dimen/system_shortcut_icon_size"
       android:layout_marginStart="@dimen/task_menu_option_start_margin"
       android:layout_gravity="center_horizontal"
-      android:backgroundTint="?androidprv:attr/materialColorOnSurface"/>
+      android:backgroundTint="?attr/materialColorOnSurface"/>
 
     <TextView
         style="@style/BaseIcon"
@@ -40,7 +40,7 @@
         android:layout_height="wrap_content"
         android:layout_marginStart="@dimen/task_menu_option_text_start_margin"
         android:textSize="14sp"
-        android:textColor="?androidprv:attr/materialColorOnSurface"
+        android:textColor="?attr/materialColorOnSurface"
         android:focusable="false"
         android:gravity="start"
         android:ellipsize="end" />
diff --git a/quickstep/res/values-af/strings.xml b/quickstep/res/values-af/strings.xml
index 73c8129..8c3b953 100644
--- a/quickstep/res/values-af/strings.xml
+++ b/quickstep/res/values-af/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Oorvloei"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> vanaf <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> en nog <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-am/strings.xml b/quickstep/res/values-am/strings.xml
index f9eed39..fd8b206 100644
--- a/quickstep/res/values-am/strings.xml
+++ b/quickstep/res/values-am/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ትርፍ ፍሰት"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ከ<xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> እና <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> ተጨማሪ"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-ar/strings.xml b/quickstep/res/values-ar/strings.xml
index 7a0be9b..c53e427 100644
--- a/quickstep/res/values-ar/strings.xml
+++ b/quickstep/res/values-ar/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"القائمة الكاملة"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"‫\"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\" من \"<xliff:g id="APP_NAME">%2$s</xliff:g>\""</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"‫\"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>\" و<xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> غيرها"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-as/strings.xml b/quickstep/res/values-as/strings.xml
index d440400..e82cad2 100644
--- a/quickstep/res/values-as/strings.xml
+++ b/quickstep/res/values-as/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"অ’ভাৰফ্ল’"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g>ৰ পৰা <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> আৰু <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> টা"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-az/strings.xml b/quickstep/res/values-az/strings.xml
index 9cdcc01..1da7f55 100644
--- a/quickstep/res/values-az/strings.xml
+++ b/quickstep/res/values-az/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Kənara çıxma"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>, <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> və daha <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> yumrucuq"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-b+sr+Latn/strings.xml b/quickstep/res/values-b+sr+Latn/strings.xml
index b6271a9..303f0d8 100644
--- a/quickstep/res/values-b+sr+Latn/strings.xml
+++ b/quickstep/res/values-b+sr+Latn/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Preklopni"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> – <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> i još <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-be/strings.xml b/quickstep/res/values-be/strings.xml
index bb6c764..2be8e5c 100644
--- a/quickstep/res/values-be/strings.xml
+++ b/quickstep/res/values-be/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Меню з пашырэннем"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>, крыніца: <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> і яшчэ <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-bg/strings.xml b/quickstep/res/values-bg/strings.xml
index d674dbc..b71d3cf 100644
--- a/quickstep/res/values-bg/strings.xml
+++ b/quickstep/res/values-bg/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Препълване"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> от <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> и още <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-bn/strings.xml b/quickstep/res/values-bn/strings.xml
index 3e974f5..59f9d7a 100644
--- a/quickstep/res/values-bn/strings.xml
+++ b/quickstep/res/values-bn/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ওভারফ্লো"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> থেকে <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> এবং আরও <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>টি"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-bs/strings.xml b/quickstep/res/values-bs/strings.xml
index 19268be..946c08c 100644
--- a/quickstep/res/values-bs/strings.xml
+++ b/quickstep/res/values-bs/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Preklopni meni"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> iz aplikacije <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> i još <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-ca/strings.xml b/quickstep/res/values-ca/strings.xml
index 6a6f131..6850656 100644
--- a/quickstep/res/values-ca/strings.xml
+++ b/quickstep/res/values-ca/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Desbordament"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> i <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> més"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-cs/strings.xml b/quickstep/res/values-cs/strings.xml
index e134850..9fd843a 100644
--- a/quickstep/res/values-cs/strings.xml
+++ b/quickstep/res/values-cs/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Rozbalovací nabídka"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> z aplikace <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> a ještě <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-da/strings.xml b/quickstep/res/values-da/strings.xml
index e9cdbce..823c071 100644
--- a/quickstep/res/values-da/strings.xml
+++ b/quickstep/res/values-da/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overløb"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> fra <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> og <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> mere"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-de/strings.xml b/quickstep/res/values-de/strings.xml
index 9e5cb12..c9b288c 100644
--- a/quickstep/res/values-de/strings.xml
+++ b/quickstep/res/values-de/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Weitere Optionen"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"„<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>“ aus <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> und <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> weitere"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-el/strings.xml b/quickstep/res/values-el/strings.xml
index bbb1282..9f0480c 100644
--- a/quickstep/res/values-el/strings.xml
+++ b/quickstep/res/values-el/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Υπερχείλιση"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> από <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> και <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> ακόμα"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-en-rAU/strings.xml b/quickstep/res/values-en-rAU/strings.xml
index b84f646..ae915ed 100644
--- a/quickstep/res/values-en-rAU/strings.xml
+++ b/quickstep/res/values-en-rAU/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overflow"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> and <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> more"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-en-rCA/strings.xml b/quickstep/res/values-en-rCA/strings.xml
index 88cd0dd..3302a5a 100644
--- a/quickstep/res/values-en-rCA/strings.xml
+++ b/quickstep/res/values-en-rCA/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overflow"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> and <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> more"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-en-rGB/strings.xml b/quickstep/res/values-en-rGB/strings.xml
index b84f646..ae915ed 100644
--- a/quickstep/res/values-en-rGB/strings.xml
+++ b/quickstep/res/values-en-rGB/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overflow"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> and <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> more"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-en-rIN/strings.xml b/quickstep/res/values-en-rIN/strings.xml
index b84f646..ae915ed 100644
--- a/quickstep/res/values-en-rIN/strings.xml
+++ b/quickstep/res/values-en-rIN/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overflow"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> and <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> more"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-en-rXC/strings.xml b/quickstep/res/values-en-rXC/strings.xml
index 76dab0d..24a7919 100644
--- a/quickstep/res/values-en-rXC/strings.xml
+++ b/quickstep/res/values-en-rXC/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‏‎‎‏‎‏‏‏‏‏‏‏‎‏‏‏‎‎‏‎‎‎‎‎‏‏‏‎‏‎‎‏‏‏‏‏‏‎‎‎‏‏‏‎‏‎‎‏‏‏‎‏‏‏‎‎‎Overflow‎‏‎‎‏‎"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‎‏‎‎‎‎‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‏‏‏‎‏‏‏‏‎‏‏‏‎‎‏‏‎‎‎‏‎‏‏‎‎‏‏‏‏‏‏‏‎‏‏‏‎‎‎‏‎‎‏‏‎<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>‎‏‎‎‏‏‏‎ from ‎‏‎‎‏‏‎<xliff:g id="APP_NAME">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‏‏‎‎‏‏‎‏‏‏‎‎‏‏‏‏‎‏‏‎‎‏‎‎‎‏‎‏‏‏‎‏‎‎‏‎‎‏‎‏‎‎‎‏‎‏‏‎‎‏‎‎‏‏‎‎‎‎‎‎‎‏‎‎‏‏‎<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>‎‏‎‎‏‏‏‎ and ‎‏‎‎‏‏‎<xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>‎‏‎‎‏‏‏‎ more‎‏‎‎‏‎"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-es-rUS/strings.xml b/quickstep/res/values-es-rUS/strings.xml
index 58d540f..b8d50d0 100644
--- a/quickstep/res/values-es-rUS/strings.xml
+++ b/quickstep/res/values-es-rUS/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Ampliada"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> y <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> más"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-es/strings.xml b/quickstep/res/values-es/strings.xml
index 2a956f0..ad9f731 100644
--- a/quickstep/res/values-es/strings.xml
+++ b/quickstep/res/values-es/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Menú adicional"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> y <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> más"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-et/strings.xml b/quickstep/res/values-et/strings.xml
index 0ecc0c0..da16bdc 100644
--- a/quickstep/res/values-et/strings.xml
+++ b/quickstep/res/values-et/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Ületäide"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> – <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ja veel <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> mulli"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-eu/strings.xml b/quickstep/res/values-eu/strings.xml
index e83ee28..bc8b285 100644
--- a/quickstep/res/values-eu/strings.xml
+++ b/quickstep/res/values-eu/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Luzapena"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> (<xliff:g id="APP_NAME">%2$s</xliff:g>)"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> eta beste <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-fa/strings.xml b/quickstep/res/values-fa/strings.xml
index bafc2d5..2d82376 100644
--- a/quickstep/res/values-fa/strings.xml
+++ b/quickstep/res/values-fa/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"سرریز"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> از <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> و <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> حبابک دیگر"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-fi/strings.xml b/quickstep/res/values-fi/strings.xml
index 5ac124a..fe54f09 100644
--- a/quickstep/res/values-fi/strings.xml
+++ b/quickstep/res/values-fi/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Ylivuoto"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>: <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ja <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> muuta"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-fr-rCA/strings.xml b/quickstep/res/values-fr-rCA/strings.xml
index 9510494..8385f50 100644
--- a/quickstep/res/values-fr-rCA/strings.xml
+++ b/quickstep/res/values-fr-rCA/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Bulle à développer"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> et <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> autres"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-fr/strings.xml b/quickstep/res/values-fr/strings.xml
index 60f8944..519fcc1 100644
--- a/quickstep/res/values-fr/strings.xml
+++ b/quickstep/res/values-fr/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Dépassement"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> (<xliff:g id="APP_NAME">%2$s</xliff:g>)"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> et <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> autre(s)"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-gl/strings.xml b/quickstep/res/values-gl/strings.xml
index bf081d4..0f09368 100644
--- a/quickstep/res/values-gl/strings.xml
+++ b/quickstep/res/values-gl/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Menú adicional"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> e <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> máis"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-gu/strings.xml b/quickstep/res/values-gu/strings.xml
index b20f771..1db3dcb 100644
--- a/quickstep/res/values-gu/strings.xml
+++ b/quickstep/res/values-gu/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ઓવરફ્લો"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g>થી <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> અને વધુ <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-hi/strings.xml b/quickstep/res/values-hi/strings.xml
index a645186..40bb511 100644
--- a/quickstep/res/values-hi/strings.xml
+++ b/quickstep/res/values-hi/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ओवरफ़्लो"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> की <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> वाली सूचना"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> और <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> अन्य"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-hr/strings.xml b/quickstep/res/values-hr/strings.xml
index c96381d..f8412a8 100644
--- a/quickstep/res/values-hr/strings.xml
+++ b/quickstep/res/values-hr/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Dodatni izbornik"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>, <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> i još <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-hu/strings.xml b/quickstep/res/values-hu/strings.xml
index 03235aa..3958bc5 100644
--- a/quickstep/res/values-hu/strings.xml
+++ b/quickstep/res/values-hu/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Túlcsordulás"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>, forrás: <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> és <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> további"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-hy/strings.xml b/quickstep/res/values-hy/strings.xml
index 3cb7990..d82c93e 100644
--- a/quickstep/res/values-hy/strings.xml
+++ b/quickstep/res/values-hy/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Լրացուցիչ ընտրացանկ"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>՝ <xliff:g id="APP_NAME">%2$s</xliff:g> հավելվածից"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ու ևս <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> ամպիկ"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-in/strings.xml b/quickstep/res/values-in/strings.xml
index 015b09e..b35eade 100644
--- a/quickstep/res/values-in/strings.xml
+++ b/quickstep/res/values-in/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Tambahan"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> dari <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> dan <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> lainnya"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-is/strings.xml b/quickstep/res/values-is/strings.xml
index 883fe82..2794ddc 100644
--- a/quickstep/res/values-is/strings.xml
+++ b/quickstep/res/values-is/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Yfirflæði"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> frá <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> og <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> í viðbót"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-it/strings.xml b/quickstep/res/values-it/strings.xml
index 59b195a..04b4489 100644
--- a/quickstep/res/values-it/strings.xml
+++ b/quickstep/res/values-it/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Extra"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> da <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> e altri <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-iw/strings.xml b/quickstep/res/values-iw/strings.xml
index 9f0ed14..2ac80e0 100644
--- a/quickstep/res/values-iw/strings.xml
+++ b/quickstep/res/values-iw/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"אפשרויות נוספות"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"‫<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> מתוך <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"‫<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ועוד <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-ja/strings.xml b/quickstep/res/values-ja/strings.xml
index 4f1a162..4ad992b 100644
--- a/quickstep/res/values-ja/strings.xml
+++ b/quickstep/res/values-ja/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"オーバーフロー"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>(<xliff:g id="APP_NAME">%2$s</xliff:g>)"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>、他 <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> 件"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-ka/strings.xml b/quickstep/res/values-ka/strings.xml
index 1fb6077..4ade09e 100644
--- a/quickstep/res/values-ka/strings.xml
+++ b/quickstep/res/values-ka/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"გადავსება"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>: <xliff:g id="APP_NAME">%2$s</xliff:g>-იდან"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> და <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> სხვა"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-kk/strings.xml b/quickstep/res/values-kk/strings.xml
index d83e2d3..e10ad21 100644
--- a/quickstep/res/values-kk/strings.xml
+++ b/quickstep/res/values-kk/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Қосымша мәзір"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> ұсынатын <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> және тағы <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-km/strings.xml b/quickstep/res/values-km/strings.xml
index 5448433..33c0738 100644
--- a/quickstep/res/values-km/strings.xml
+++ b/quickstep/res/values-km/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ម៉ឺនុយបន្ថែម"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ពី <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> និង <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> នាក់ទៀត"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-kn/strings.xml b/quickstep/res/values-kn/strings.xml
index 74c7750..18ef7e1 100644
--- a/quickstep/res/values-kn/strings.xml
+++ b/quickstep/res/values-kn/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ಓವರ್‌ಫ್ಲೋ"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> ನಿಂದ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ಮತ್ತು ಇನ್ನೂ <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-ko/strings.xml b/quickstep/res/values-ko/strings.xml
index c27b7f8..3b48d06 100644
--- a/quickstep/res/values-ko/strings.xml
+++ b/quickstep/res/values-ko/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"더보기"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g>의 <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> 외 <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>개"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-ky/strings.xml b/quickstep/res/values-ky/strings.xml
index f0d2af8..413d135 100644
--- a/quickstep/res/values-ky/strings.xml
+++ b/quickstep/res/values-ky/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Кошумча меню"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> колдонмосунан <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> жана дагы <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-lo/strings.xml b/quickstep/res/values-lo/strings.xml
index f54c712..c2418b6 100644
--- a/quickstep/res/values-lo/strings.xml
+++ b/quickstep/res/values-lo/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ລາຍການເພີ່ມເຕີມ"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ຈາກ <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ແລະ ອີກ <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> ລາຍການ"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-lt/strings.xml b/quickstep/res/values-lt/strings.xml
index 554745e..59bda55 100644
--- a/quickstep/res/values-lt/strings.xml
+++ b/quickstep/res/values-lt/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Perpildymas"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"„<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>“ iš „<xliff:g id="APP_NAME">%2$s</xliff:g>“"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"„<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>“ ir dar <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-lv/strings.xml b/quickstep/res/values-lv/strings.xml
index a6a0dab..ede41c3 100644
--- a/quickstep/res/values-lv/strings.xml
+++ b/quickstep/res/values-lv/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Pārpilde"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> no lietotnes <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> un vēl <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-mk/strings.xml b/quickstep/res/values-mk/strings.xml
index 4859055..5787da3 100644
--- a/quickstep/res/values-mk/strings.xml
+++ b/quickstep/res/values-mk/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Проширено балонче"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> од <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> и уште <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-ml/strings.xml b/quickstep/res/values-ml/strings.xml
index 85b093d..61dcce0 100644
--- a/quickstep/res/values-ml/strings.xml
+++ b/quickstep/res/values-ml/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ഓവർഫ്ലോ"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> എന്നതിൽ നിന്നുള്ള <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> എന്നതും മറ്റ് <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> എണ്ണവും"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-mn/strings.xml b/quickstep/res/values-mn/strings.xml
index fe2e4a4..22f4390 100644
--- a/quickstep/res/values-mn/strings.xml
+++ b/quickstep/res/values-mn/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Илүү хэсэг"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g>-с ирсэн <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> болон бусад <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-mr/strings.xml b/quickstep/res/values-mr/strings.xml
index b053a21..d7143a1 100644
--- a/quickstep/res/values-mr/strings.xml
+++ b/quickstep/res/values-mr/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ओव्हरफ्लो"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> वरील <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> आणि आणखी <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-ms/strings.xml b/quickstep/res/values-ms/strings.xml
index c0219e0..ce2367d 100644
--- a/quickstep/res/values-ms/strings.xml
+++ b/quickstep/res/values-ms/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Limpahan"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> daripada <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> dan <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> lagi"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-my/strings.xml b/quickstep/res/values-my/strings.xml
index 7c7ff82..357f2f8 100644
--- a/quickstep/res/values-my/strings.xml
+++ b/quickstep/res/values-my/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"မီနူးအပို"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> မှ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> နှင့် နောက်ထပ် <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> ခု"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-nb/strings.xml b/quickstep/res/values-nb/strings.xml
index 6aa755a..e075be9 100644
--- a/quickstep/res/values-nb/strings.xml
+++ b/quickstep/res/values-nb/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overflyt"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> fra <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> og <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> andre"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-ne/strings.xml b/quickstep/res/values-ne/strings.xml
index d49fd2d..456c1c3 100644
--- a/quickstep/res/values-ne/strings.xml
+++ b/quickstep/res/values-ne/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ओभरफ्लो"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> मा देखाइएका <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> र थप <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-night/colors.xml b/quickstep/res/values-night/colors.xml
index 94100ba..2052446 100644
--- a/quickstep/res/values-night/colors.xml
+++ b/quickstep/res/values-night/colors.xml
@@ -26,6 +26,6 @@
 
     <!-- Turn on work apps button -->
     <color name="work_turn_on_stroke">?androidprv:attr/colorAccentSecondaryVariant</color>
-    <color name="work_fab_bg_color">?androidprv:attr/materialColorPrimaryFixedDim</color>
-    <color name="work_fab_icon_color">?androidprv:attr/materialColorOnPrimaryFixed</color>
+    <color name="work_fab_bg_color">?attr/materialColorPrimaryFixedDim</color>
+    <color name="work_fab_icon_color">?attr/materialColorOnPrimaryFixed</color>
 </resources>
\ No newline at end of file
diff --git a/quickstep/res/values-night/styles.xml b/quickstep/res/values-night/styles.xml
index 401351f..2cb633a 100644
--- a/quickstep/res/values-night/styles.xml
+++ b/quickstep/res/values-night/styles.xml
@@ -82,16 +82,16 @@
     <style name="GestureTutorialActivity" parent="@style/AppTheme">
         <item name="background">@android:color/transparent</item>
         <item name="tutorialSubtitle">@android:color/white</item>
-        <item name="surfaceContainer">?androidprv:attr/materialColorSurfaceContainer</item>
-        <item name="onSurfaceHome">?androidprv:attr/materialColorPrimaryFixedDim</item>
-        <item name="surfaceHome">?androidprv:attr/materialColorOnPrimaryFixedVariant</item>
-        <item name="secondaryHome">?androidprv:attr/materialColorOnPrimaryFixed</item>
-        <item name="onSurfaceBack">?androidprv:attr/materialColorTertiaryFixedDim</item>
-        <item name="surfaceBack">?androidprv:attr/materialColorOnTertiaryFixedVariant</item>
-        <item name="secondaryBack">?androidprv:attr/materialColorOnTertiaryFixed</item>
-        <item name="onSurfaceOverview">?androidprv:attr/materialColorPrimaryFixed</item>
-        <item name="surfaceOverview">?androidprv:attr/materialColorOnSecondaryFixedVariant</item>
-        <item name="secondaryOverview">?androidprv:attr/materialColorOnSecondaryFixed</item>
+        <item name="surfaceContainer">?attr/materialColorSurfaceContainer</item>
+        <item name="onSurfaceHome">?attr/materialColorPrimaryFixedDim</item>
+        <item name="surfaceHome">?attr/materialColorOnPrimaryFixedVariant</item>
+        <item name="secondaryHome">?attr/materialColorOnPrimaryFixed</item>
+        <item name="onSurfaceBack">?attr/materialColorTertiaryFixedDim</item>
+        <item name="surfaceBack">?attr/materialColorOnTertiaryFixedVariant</item>
+        <item name="secondaryBack">?attr/materialColorOnTertiaryFixed</item>
+        <item name="onSurfaceOverview">?attr/materialColorPrimaryFixed</item>
+        <item name="surfaceOverview">?attr/materialColorOnSecondaryFixedVariant</item>
+        <item name="secondaryOverview">?attr/materialColorOnSecondaryFixed</item>
     </style>
 
 </resources>
\ No newline at end of file
diff --git a/quickstep/res/values-nl/strings.xml b/quickstep/res/values-nl/strings.xml
index ca44a69..b6321de 100644
--- a/quickstep/res/values-nl/strings.xml
+++ b/quickstep/res/values-nl/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overloop"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> van <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> en nog <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-or/strings.xml b/quickstep/res/values-or/strings.xml
index bf0bdc8..b7d42ef 100644
--- a/quickstep/res/values-or/strings.xml
+++ b/quickstep/res/values-or/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ଓଭରଫ୍ଲୋ"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g>ରୁ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ଏବଂ ଅଧିକ <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-pa/strings.xml b/quickstep/res/values-pa/strings.xml
index fc60396..d83d744 100644
--- a/quickstep/res/values-pa/strings.xml
+++ b/quickstep/res/values-pa/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ਓਵਰਫ਼ਲੋ"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> ਤੋਂ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ਅਤੇ <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> ਹੋਰ"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-pl/strings.xml b/quickstep/res/values-pl/strings.xml
index d88e28a..064ba19 100644
--- a/quickstep/res/values-pl/strings.xml
+++ b/quickstep/res/values-pl/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Rozwijany"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> z aplikacji <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> i jeszcze <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-pt-rPT/strings.xml b/quickstep/res/values-pt-rPT/strings.xml
index e4d07bd..dc8bceb 100644
--- a/quickstep/res/values-pt-rPT/strings.xml
+++ b/quickstep/res/values-pt-rPT/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Menu adicional"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> da app <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> e mais <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> pessoas"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-pt/strings.xml b/quickstep/res/values-pt/strings.xml
index 4fec4f8..3a5ee73 100644
--- a/quickstep/res/values-pt/strings.xml
+++ b/quickstep/res/values-pt/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Balão flutuante"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> do app <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> e mais <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-ro/strings.xml b/quickstep/res/values-ro/strings.xml
index c839602..46322ed 100644
--- a/quickstep/res/values-ro/strings.xml
+++ b/quickstep/res/values-ro/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Suplimentar"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de la <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> și încă <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-ru/strings.xml b/quickstep/res/values-ru/strings.xml
index da49ad3..ec03374 100644
--- a/quickstep/res/values-ru/strings.xml
+++ b/quickstep/res/values-ru/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Дополнительное меню"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"\"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\" из приложения \"<xliff:g id="APP_NAME">%2$s</xliff:g>\""</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> и ещё <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-si/strings.xml b/quickstep/res/values-si/strings.xml
index 9cbe837..7f4754f 100644
--- a/quickstep/res/values-si/strings.xml
+++ b/quickstep/res/values-si/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"පිටාර යාම"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> සිට <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> හා තව <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>ක්"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-sk/strings.xml b/quickstep/res/values-sk/strings.xml
index 3eca787..42faef3 100644
--- a/quickstep/res/values-sk/strings.xml
+++ b/quickstep/res/values-sk/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Rozbaľovacia ponuka"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> z aplikácie <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> a ešte <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-sl/strings.xml b/quickstep/res/values-sl/strings.xml
index 52faeb7..c824fc0 100644
--- a/quickstep/res/values-sl/strings.xml
+++ b/quickstep/res/values-sl/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Oblaček z dodatnimi elementi"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> iz aplikacije <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> in še <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-sq/strings.xml b/quickstep/res/values-sq/strings.xml
index cdb9cf9..cb793af 100644
--- a/quickstep/res/values-sq/strings.xml
+++ b/quickstep/res/values-sq/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Tejkalimi"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"\"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\" nga <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"\"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>\" dhe <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> të tjera"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-sr/strings.xml b/quickstep/res/values-sr/strings.xml
index 7456a36..c8819c8 100644
--- a/quickstep/res/values-sr/strings.xml
+++ b/quickstep/res/values-sr/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Преклопни"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> – <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> и још <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-sv/strings.xml b/quickstep/res/values-sv/strings.xml
index f369dae..6cc637b 100644
--- a/quickstep/res/values-sv/strings.xml
+++ b/quickstep/res/values-sv/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Fler alternativ"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> från <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> och <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> till"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-sw/strings.xml b/quickstep/res/values-sw/strings.xml
index 3d8277b..aac394a 100644
--- a/quickstep/res/values-sw/strings.xml
+++ b/quickstep/res/values-sw/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Kiputo cha vipengee vya ziada"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> kutoka <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> na vingine <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-ta/strings.xml b/quickstep/res/values-ta/strings.xml
index 47d8055..a20d23c 100644
--- a/quickstep/res/values-ta/strings.xml
+++ b/quickstep/res/values-ta/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"கூடுதல் விருப்பங்களைக் காட்டும்"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> வழங்கும் <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> மற்றும் <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-te/strings.xml b/quickstep/res/values-te/strings.xml
index a4e1cbf..c96412e 100644
--- a/quickstep/res/values-te/strings.xml
+++ b/quickstep/res/values-te/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"ఓవర్‌ఫ్లో"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> నుండి <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>, మరో <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-th/strings.xml b/quickstep/res/values-th/strings.xml
index 1bbb137..080e032 100644
--- a/quickstep/res/values-th/strings.xml
+++ b/quickstep/res/values-th/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"การดำเนินการเพิ่มเติม"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> จาก <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> และอีก <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> รายการ"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-tl/strings.xml b/quickstep/res/values-tl/strings.xml
index 978a5a3..194a81f 100644
--- a/quickstep/res/values-tl/strings.xml
+++ b/quickstep/res/values-tl/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Overflow"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> mula sa <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> at <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> pa"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-tr/strings.xml b/quickstep/res/values-tr/strings.xml
index 0cc5d7f..0c64537 100644
--- a/quickstep/res/values-tr/strings.xml
+++ b/quickstep/res/values-tr/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Taşma"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> uygulamasından <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> ve <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> tane daha"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-uk/strings.xml b/quickstep/res/values-uk/strings.xml
index 9c706a8..9bb0064 100644
--- a/quickstep/res/values-uk/strings.xml
+++ b/quickstep/res/values-uk/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Додаткове повідомлення"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> з додатка <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> і ще <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-ur/strings.xml b/quickstep/res/values-ur/strings.xml
index e125248..63fe0cb 100644
--- a/quickstep/res/values-ur/strings.xml
+++ b/quickstep/res/values-ur/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"اوورفلو"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> سے <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> اور <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> مزید"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-uz/strings.xml b/quickstep/res/values-uz/strings.xml
index 3f4f981..94a23c0 100644
--- a/quickstep/res/values-uz/strings.xml
+++ b/quickstep/res/values-uz/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Kengaytirish"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> (<xliff:g id="APP_NAME">%2$s</xliff:g>)"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> va yana <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> kishi"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-vi/strings.xml b/quickstep/res/values-vi/strings.xml
index 9bc526f..2fe1d14 100644
--- a/quickstep/res/values-vi/strings.xml
+++ b/quickstep/res/values-vi/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Bong bóng bổ sung"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> từ <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> và <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> bong bóng khác"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-zh-rCN/strings.xml b/quickstep/res/values-zh-rCN/strings.xml
index 79ea299..e649e39 100644
--- a/quickstep/res/values-zh-rCN/strings.xml
+++ b/quickstep/res/values-zh-rCN/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"溢出式气泡框"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"来自“<xliff:g id="APP_NAME">%2$s</xliff:g>”的<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>以及另外 <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> 个"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-zh-rHK/strings.xml b/quickstep/res/values-zh-rHK/strings.xml
index b9d8eb7..e554148 100644
--- a/quickstep/res/values-zh-rHK/strings.xml
+++ b/quickstep/res/values-zh-rHK/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"展開式"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g> 的「<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>」通知"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>和其他 <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> 則通知"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-zh-rTW/strings.xml b/quickstep/res/values-zh-rTW/strings.xml
index 90140cb..e598332 100644
--- a/quickstep/res/values-zh-rTW/strings.xml
+++ b/quickstep/res/values-zh-rTW/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"溢位"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"「<xliff:g id="APP_NAME">%2$s</xliff:g>」的「<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>」通知"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>和另外 <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> 則通知"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values-zu/strings.xml b/quickstep/res/values-zu/strings.xml
index 73be445..eb2e9b3 100644
--- a/quickstep/res/values-zu/strings.xml
+++ b/quickstep/res/values-zu/strings.xml
@@ -144,4 +144,10 @@
     <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Ukugcwala kakhulu"</string>
     <string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> kusuka ku-<xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
     <string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> nokunye okungu-<xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
+    <!-- no translation found for bubble_bar_action_move_left (3306922475737714758) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_move_right (3455099638571411251) -->
+    <skip />
+    <!-- no translation found for bubble_bar_action_dismiss_all (3290722022983403060) -->
+    <skip />
 </resources>
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
index 0f997f9..0bb971e 100644
--- a/quickstep/res/values/colors.xml
+++ b/quickstep/res/values/colors.xml
@@ -95,6 +95,6 @@
 
     <!-- Turn on work apps button -->
     <color name="work_turn_on_stroke">?androidprv:attr/colorAccentPrimaryVariant</color>
-    <color name="work_fab_bg_color">?androidprv:attr/materialColorPrimaryFixedDim</color>
-    <color name="work_fab_icon_color">?androidprv:attr/materialColorOnPrimaryFixed</color>
+    <color name="work_fab_bg_color">?attr/materialColorPrimaryFixedDim</color>
+    <color name="work_fab_icon_color">?attr/materialColorOnPrimaryFixed</color>
 </resources>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index d4f66e2..867ce17 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -449,11 +449,13 @@
 
     <dimen name="bubblebar_icon_size_small">32dp</dimen>
     <dimen name="bubblebar_icon_size">36dp</dimen>
+    <dimen name="bubblebar_icon_size_persistent_taskbar">28dp</dimen>
     <dimen name="bubblebar_badge_size">24dp</dimen>
     <dimen name="bubblebar_icon_overlap">12dp</dimen>
     <dimen name="bubblebar_overflow_inset">16dp</dimen>
     <dimen name="bubblebar_icon_spacing">6dp</dimen>
     <dimen name="bubblebar_icon_spacing_large">8dp</dimen>
+    <dimen name="bubblebar_icon_spacing_persistent_taskbar">@dimen/bubblebar_icon_spacing</dimen>
     <dimen name="bubblebar_expanded_icon_spacing">12dp</dimen>
     <dimen name="bubblebar_icon_elevation">1dp</dimen>
 
diff --git a/quickstep/res/values/ids.xml b/quickstep/res/values/ids.xml
new file mode 100644
index 0000000..3091d9e
--- /dev/null
+++ b/quickstep/res/values/ids.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<resources>
+    <!-- Used for A11y actions for bubble bar -->
+    <item type="id" name="action_move_left" />
+    <item type="id" name="action_move_right" />
+    <item type="id" name="action_dismiss_all" />
+</resources>
\ No newline at end of file
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 340d25b..8bcbb33 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -299,10 +299,16 @@
     <string name="taskbar_button_quick_settings">Quick Settings</string>
     <!-- Accessibility title for the Taskbar window. [CHAR_LIMIT=NONE] -->
     <string name="taskbar_a11y_title">Taskbar</string>
-    <!-- Accessibility title for the Taskbar window appeared. [CHAR_LIMIT=NONE] -->
+    <!-- Accessibility title for the Taskbar window appeared. [CHAR_LIMIT=30] -->
     <string name="taskbar_a11y_shown_title">Taskbar shown</string>
-    <!-- Accessibility title for the Taskbar window being close. [CHAR_LIMIT=NONE] -->
+    <!-- Accessibility title for the Taskbar window appearing together with bubble bar on left. [CHAR_LIMIT=30] -->
+    <string name="taskbar_a11y_shown_with_bubbles_left_title">Taskbar &#38; bubbles left shown</string>
+    <!-- Accessibility title for the Taskbar window appearing together with bubble bar on right. [CHAR_LIMIT=30] -->
+    <string name="taskbar_a11y_shown_with_bubbles_right_title">Taskbar &#38; bubbles right shown</string>
+    <!-- Accessibility title for the Taskbar window being closed. [CHAR_LIMIT=30] -->
     <string name="taskbar_a11y_hidden_title">Taskbar hidden</string>
+    <!-- Accessibility title for the Taskbar window being closed together with bubble bar. [CHAR_LIMIT=30] -->
+    <string name="taskbar_a11y_hidden_with_bubbles_title">Taskbar &#38; bubbles hidden</string>
     <!-- Accessibility title for the Taskbar window on phones. [CHAR_LIMIT=NONE] -->
     <string name="taskbar_phone_a11y_title">Navigation bar</string>
     <!-- Text in popup dialog for user to switch between always showing Taskbar or not. [CHAR LIMIT=30] -->
@@ -342,4 +348,14 @@
     <string name="bubble_bar_bubble_description"><xliff:g id="notification_title" example="some title">%1$s</xliff:g> from <xliff:g id="app_name" example="YouTube">%2$s</xliff:g></string>
     <!-- Content description for bubble bar when it has multiple bubbles. [CHAR_LIMIT=NONE] -->
     <string name="bubble_bar_description_multiple_bubbles"><xliff:g id="bubble_bar_bubble_description" example="some title from YouTube">%1$s</xliff:g> and <xliff:g id="bubble_count" example="4">%2$d</xliff:g> more</string>
+    <!-- Action in accessibility menu to move the bubble bar to the left side of the screen. [CHAR_LIMIT=30] -->
+    <string name="bubble_bar_action_move_left">Move left</string>
+    <!-- Action in accessibility menu to move the bubble bar to the right side of the screen. [CHAR_LIMIT=30] -->
+    <string name="bubble_bar_action_move_right">Move right</string>
+    <!-- Action in accessibility menu to dismiss all bubbles. [CHAR_LIMIT=30] -->
+    <string name="bubble_bar_action_dismiss_all">Dismiss all</string>
+    <!-- Accessibility announcement when the bubble bar expands. [CHAR LIMIT=NONE]-->
+    <string name="bubble_bar_accessibility_announce_expand">expand <xliff:g id="bubble_description" example="some title from Messages">%1$s</xliff:g></string>
+    <!-- Accessibility announcement when the bubble bar collapses. [CHAR LIMIT=NONE]-->
+    <string name="bubble_bar_accessibility_announce_collapse">collapse <xliff:g id="bubble_description" example="some title from Messages">%1$s</xliff:g></string>
 </resources>
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index 952505a..1f4720c 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -124,7 +124,7 @@
     <style name="TextAppearance.GestureTutorial.ButtonLabel"
         parent="TextAppearance.GestureTutorial.CallToAction">
         <item name="android:gravity">center</item>
-        <item name="android:textColor">?androidprv:attr/materialColorOnPrimary</item>
+        <item name="android:textColor">?attr/materialColorOnPrimary</item>
         <item name="android:letterSpacing">0.02</item>
         <item name="android:textSize">16sp</item>
         <item name="android:textAllCaps">false</item>
@@ -268,53 +268,59 @@
     <style name="KeyboardQuickSwitchText">
         <item name="fontFamily">google-sans-text</item>
         <item name="android:textSize">14sp</item>
-        <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+        <item name="android:textColor">?attr/materialColorOnSurface</item>
         <item name="lineHeight">20sp</item>
     </style>
 
     <style name="KeyboardQuickSwitchText.OnBackground" parent="KeyboardQuickSwitchText">
-        <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+        <item name="android:textColor">?attr/materialColorOnSurface</item>
     </style>
 
     <style name="GestureTutorialActivity" parent="@style/AppTheme">
         <item name="background">@android:color/transparent</item>
         <item name="tutorialSubtitle">@android:color/black</item>
-        <item name="surfaceContainer">?androidprv:attr/materialColorSurfaceContainer</item>
-        <item name="onSurfaceHome">?androidprv:attr/materialColorPrimaryFixed</item>
+        <item name="surfaceContainer">?attr/materialColorSurfaceContainer</item>
+        <item name="onSurfaceHome">?attr/materialColorPrimaryFixed</item>
         <item name="surfaceHome">@android:color/system_accent1_300</item>
-        <item name="secondaryHome">?androidprv:attr/materialColorOnPrimaryFixedVariant</item>
-        <item name="onSurfaceBack">?androidprv:attr/materialColorTertiaryFixed</item>
+        <item name="secondaryHome">?attr/materialColorOnPrimaryFixedVariant</item>
+        <item name="onSurfaceBack">?attr/materialColorTertiaryFixed</item>
         <item name="surfaceBack">@android:color/system_accent3_300</item>
-        <item name="secondaryBack">?androidprv:attr/materialColorOnTertiaryFixedVariant</item>
-        <item name="onSurfaceOverview">?androidprv:attr/materialColorPrimaryFixed</item>
+        <item name="secondaryBack">?attr/materialColorOnTertiaryFixedVariant</item>
+        <item name="onSurfaceOverview">?attr/materialColorPrimaryFixed</item>
         <item name="surfaceOverview">@android:color/system_accent2_300</item>
-        <item name="secondaryOverview">?androidprv:attr/materialColorOnSecondaryFixedVariant</item>
+        <item name="secondaryOverview">?attr/materialColorOnSecondaryFixedVariant</item>
     </style>
 
     <style name="rotate_prompt_title" parent="TextAppearance.GestureTutorial.Dialog.Title">
-        <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+        <item name="android:textColor">?attr/materialColorOnSurface</item>
     </style>
 
     <style name="rotate_prompt_subtitle" parent="TextAppearance.GestureTutorial.Dialog.Subtitle">
-        <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
+        <item name="android:textColor">?attr/materialColorOnSurfaceVariant</item>
     </style>
 
     <style name="ArrowTipTaskbarStyle">
-        <item name="arrowTipBackground">?androidprv:attr/materialColorSurfaceContainer</item>
-        <item name="arrowTipTextColor">?androidprv:attr/materialColorOnSurface</item>
+        <item name="arrowTipBackground">?attr/materialColorSurfaceContainer</item>
+        <item name="arrowTipTextColor">?attr/materialColorOnSurface</item>
     </style>
 
     <style name="IconAppChipMenuTextStyle">
         <item name="android:fontFamily">google-sans-text-medium</item>
         <item name="android:textSize">@dimen/task_thumbnail_icon_menu_text_size</item>
-        <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+        <item name="android:textColor">?attr/materialColorOnSurface</item>
         <item name="android:letterSpacing">0.025</item>
         <item name="android:lineHeight">20sp</item>
     </style>
 
-    <style name="WidgetPickerActivityTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
-        <item name="widgetsTheme">@style/WidgetContainerTheme</item>
+    <style name="WidgetPickerActivityTheme" parent="@android:style/Theme.DeviceDefault.DayNight">
         <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:colorBackgroundCacheHint">@null</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowAnimationStyle">@android:style/Animation</item>
+
+        <item name="widgetsTheme">@style/WidgetContainerTheme</item>
         <item name="pageIndicatorDotColor">@color/page_indicator_dot_color_light</item>
     </style>
 </resources>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 5a74f4a..fe1b403 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -356,6 +356,14 @@
         options.setOnAnimationAbortListener(endCallback);
         options.setOnAnimationFinishedListener(endCallback);
 
+        // Prepare taskbar for animation synchronization. This needs to happen here before any
+        // app transition is created.
+        LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController();
+        if (enableScalingRevealHomeAnimation() && taskbarController != null) {
+            taskbarController.setIgnoreInAppFlagForSync(true);
+            onEndCallback.add(() -> taskbarController.setIgnoreInAppFlagForSync(false));
+        }
+
         IBinder cookie = mAppLaunchRunner.supportsReturnTransition()
                 ? ((ContainerAnimationRunner) mAppLaunchRunner).getCookie() : null;
         addLaunchCookie(cookie, (ItemInfo) v.getTag(), options);
@@ -1244,15 +1252,25 @@
         TransitionFilter homeCheck = new TransitionFilter();
         // No need to handle the transition that also dismisses keyguard.
         homeCheck.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+
         homeCheck.mRequirements =
                 new TransitionFilter.Requirement[]{new TransitionFilter.Requirement(),
+                        new TransitionFilter.Requirement(),
                         new TransitionFilter.Requirement()};
+
         homeCheck.mRequirements[0].mActivityType = ACTIVITY_TYPE_HOME;
         homeCheck.mRequirements[0].mTopActivity = mLauncher.getComponentName();
         homeCheck.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
         homeCheck.mRequirements[0].mOrder = CONTAINER_ORDER_TOP;
+
         homeCheck.mRequirements[1].mActivityType = ACTIVITY_TYPE_STANDARD;
         homeCheck.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
+
+        homeCheck.mRequirements[2].mNot = true;
+        homeCheck.mRequirements[2].mCustomAnimation = true;
+        homeCheck.mRequirements[2].mMustBeTask = true;
+        homeCheck.mRequirements[2].mMustBeIndependent = true;
+
         SystemUiProxy.INSTANCE.get(mLauncher)
                 .registerRemoteTransition(mLauncherOpenTransition, homeCheck);
         if (mBackAnimationController != null) {
@@ -1644,7 +1662,15 @@
     }
 
     private void addCujInstrumentation(Animator anim, int cuj) {
-        anim.addListener(new AnimationSuccessListener() {
+        anim.addListener(getCujAnimationSuccessListener(cuj));
+    }
+
+    private void addCujInstrumentation(RectFSpringAnim anim, int cuj) {
+        anim.addAnimatorListener(getCujAnimationSuccessListener(cuj));
+    }
+
+    private AnimationSuccessListener getCujAnimationSuccessListener(int cuj) {
+        return new AnimationSuccessListener() {
             @Override
             public void onAnimationStart(Animator animation) {
                 mDragLayer.getViewTreeObserver().addOnDrawListener(
@@ -1678,7 +1704,7 @@
             public void onAnimationSuccess(Animator animator) {
                 InteractionJankMonitorWrapper.end(cuj);
             }
-        });
+        };
     }
 
     /**
@@ -1759,10 +1785,6 @@
         // invisibility on touch down, and only reset it after the animation to home
         // is initialized.
         if (launcherIsForceInvisibleOrOpening || fromPredictiveBack) {
-            addCujInstrumentation(anim, playFallBackAnimation
-                    ? Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK
-                    : Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
-
             AnimatorListenerAdapter endListener = new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
@@ -1772,6 +1794,14 @@
                 }
             };
 
+            if (rectFSpringAnim != null && anim.getChildAnimations().isEmpty()) {
+                addCujInstrumentation(rectFSpringAnim, Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
+            } else {
+                addCujInstrumentation(anim, playFallBackAnimation
+                        ? Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK
+                        : Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
+            }
+
             if (fromPredictiveBack && rectFSpringAnim != null) {
                 rectFSpringAnim.addAnimatorListener(endListener);
             } else {
@@ -1912,6 +1942,21 @@
                 anim.addListener(mForceInvisibleListener);
             }
 
+            // Syncs the app launch animation and taskbar stash animation (if exists).
+            if (enableScalingRevealHomeAnimation()) {
+                LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController();
+                if (taskbarController != null) {
+                    taskbarController.setIgnoreInAppFlagForSync(false);
+
+                    if (launcherClosing) {
+                        Animator taskbar = taskbarController.createAnimToApp();
+                        if (taskbar != null) {
+                            anim.play(taskbar);
+                        }
+                    }
+                }
+            }
+
             result.setAnimation(anim, mLauncher, mOnEndCallback::executeAllAndDestroy,
                     skipFirstFrame);
         }
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 44d8a5c..e925af6 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -46,12 +46,12 @@
 import com.android.launcher3.model.WidgetPredictionsRequester;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.popup.PopupDataProvider;
-import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import com.android.launcher3.widget.model.WidgetsListContentEntry;
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -59,9 +59,8 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
-import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 
 /** An Activity that can host Launcher's widget picker. */
 public class WidgetPickerActivity extends BaseActivity {
@@ -110,7 +109,8 @@
     private WidgetsModel mModel;
     private LauncherAppState mApp;
     private WidgetPredictionsRequester mWidgetPredictionsRequester;
-    private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
+    private final WidgetPickerDataProvider mWidgetPickerDataProvider =
+            new WidgetPickerDataProvider();
 
     private int mDesiredWidgetWidth;
     private int mDesiredWidgetHeight;
@@ -128,9 +128,23 @@
     /** A set of user ids that should be filtered out from the selected widgets. */
     @NonNull
     Set<Integer> mFilteredUserIds = new HashSet<>();
+
     @Nullable
     private WidgetsFullSheet mWidgetSheet;
 
+    private final Predicate<WidgetItem> mWidgetsFilter = widget -> {
+        final WidgetAcceptabilityVerdict verdict =
+                isWidgetAcceptable(widget, /* applySizeFilter=*/ false);
+        verdict.maybeLogVerdict();
+        return verdict.isAcceptable;
+    };
+    private final Predicate<WidgetItem> mDefaultWidgetsFilter = widget -> {
+        final WidgetAcceptabilityVerdict verdict =
+                isWidgetAcceptable(widget, /* applySizeFilter=*/ true);
+        verdict.maybeLogVerdict();
+        return verdict.isAcceptable;
+    };
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -200,8 +214,8 @@
 
     @NonNull
     @Override
-    public PopupDataProvider getPopupDataProvider() {
-        return mPopupDataProvider;
+    public WidgetPickerDataProvider getWidgetPickerDataProvider() {
+        return mWidgetPickerDataProvider;
     }
 
     @Override
@@ -271,47 +285,40 @@
         };
     }
 
-    /** Updates the model with widgets, applies filters and launches the widgets sheet once
-     * widgets are available */
+    /**
+     * Updates the model with widgets, applies filters and launches the widgets sheet once
+     * widgets are available
+     */
     private void refreshAndBindWidgets() {
         MODEL_EXECUTOR.execute(() -> {
             LauncherAppState app = LauncherAppState.getInstance(this);
             mModel.update(app, null);
-            final List<WidgetsListBaseEntry> allWidgets =
-                    mModel.getFilteredWidgetsListForPicker(
-                            app.getContext(),
-                            /*widgetItemFilter=*/ widget -> {
-                                final WidgetAcceptabilityVerdict verdict =
-                                        isWidgetAcceptable(widget);
-                                verdict.maybeLogVerdict();
-                                return verdict.isAcceptable;
-                            }
-                    );
-            bindWidgets(allWidgets);
+            bindWidgets(mModel.getWidgetsByPackageItem());
             // Open sheet once widgets are available, so that it doesn't interrupt the open
             // animation.
             openWidgetsSheet();
             if (mUiSurface != null) {
-                Map<ComponentKey, WidgetItem> allWidgetItems = allWidgets.stream()
-                        .filter(entry -> entry instanceof WidgetsListContentEntry)
-                        .flatMap(entry -> entry.mWidgets.stream())
-                        .distinct()
-                        .collect(Collectors.toMap(
-                                widget -> new ComponentKey(widget.componentName, widget.user),
-                                Function.identity()
-                        ));
                 mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(),
-                        mUiSurface, allWidgetItems);
+                        mUiSurface, mModel.getWidgetsByComponentKey());
                 mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets);
             }
         });
     }
 
-    private void bindWidgets(List<WidgetsListBaseEntry> widgets) {
-        MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets));
+    private void bindWidgets(Map<PackageItemInfo, List<WidgetItem>> widgets) {
+        WidgetsListBaseEntriesBuilder builder = new WidgetsListBaseEntriesBuilder(
+                mApp.getContext());
+
+        final List<WidgetsListBaseEntry> allWidgets = builder.build(widgets, mWidgetsFilter);
+        final List<WidgetsListBaseEntry> defaultWidgets =
+                shouldShowDefaultWidgets() ? builder.build(widgets,
+                        mDefaultWidgetsFilter) : List.of();
+
+        MAIN_EXECUTOR.execute(
+                () -> mWidgetPickerDataProvider.setWidgets(allWidgets, defaultWidgets));
     }
 
-   private void openWidgetsSheet() {
+    private void openWidgetsSheet() {
         MAIN_EXECUTOR.execute(() -> {
             mWidgetSheet = WidgetsFullSheet.show(this, true);
             mWidgetSheet.mayUpdateTitleAndDescription(mTitle, mDescription);
@@ -323,7 +330,7 @@
     private void bindRecommendedWidgets(List<ItemInfo> recommendedWidgets) {
         // Bind recommendations once picker has finished open animation.
         MAIN_EXECUTOR.getHandler().postDelayed(
-                () -> mPopupDataProvider.setRecommendedWidgets(recommendedWidgets),
+                () -> mWidgetPickerDataProvider.setWidgetRecommendations(recommendedWidgets),
                 mDeviceProfile.bottomSheetOpenDuration);
     }
 
@@ -378,9 +385,15 @@
             mActiveOnBackAnimationCallback.onBackCancelled();
             mActiveOnBackAnimationCallback = null;
         }
-    };
+    }
 
-    private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget) {
+    private boolean shouldShowDefaultWidgets() {
+        // If optional filters such as size filter are present, we display them as default widgets.
+        return mDesiredWidgetWidth != 0 || mDesiredWidgetHeight != 0;
+    }
+
+    private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget,
+            boolean applySizeFilter) {
         final AppWidgetProviderInfo info = widget.widgetInfo;
         if (info == null) {
             return rejectWidget(widget, "shortcut");
@@ -401,61 +414,63 @@
                     info.widgetCategory);
         }
 
-        if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) {
-            // Accept the widget if the desired dimensions are unspecified.
-            return acceptWidget(widget);
-        }
-
-        final boolean isHorizontallyResizable =
-                (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
-        if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) {
-            if (info.maxResizeWidth > 0
-                    && info.maxResizeWidth >= info.minWidth
-                    && info.maxResizeWidth < mDesiredWidgetWidth) {
-                return rejectWidget(
-                        widget,
-                        "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
-                        info.maxResizeWidth,
-                        mDesiredWidgetWidth);
+        if (applySizeFilter) {
+            if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) {
+                // Accept the widget if the desired dimensions are unspecified.
+                return acceptWidget(widget);
             }
 
-            final int minWidth = Math.min(info.minResizeWidth, info.minWidth);
-            if (minWidth > mDesiredWidgetWidth) {
-                return rejectWidget(
-                        widget,
-                        "min(minWidth[%d], minResizeWidth[%d]) > mDesiredWidgetWidth[%d]",
-                        info.minWidth,
-                        info.minResizeWidth,
-                        mDesiredWidgetWidth);
-            }
-        }
+            final boolean isHorizontallyResizable =
+                    (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
+            if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) {
+                if (info.maxResizeWidth > 0
+                        && info.maxResizeWidth >= info.minWidth
+                        && info.maxResizeWidth < mDesiredWidgetWidth) {
+                    return rejectWidget(
+                            widget,
+                            "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
+                            info.maxResizeWidth,
+                            mDesiredWidgetWidth);
+                }
 
-        final boolean isVerticallyResizable =
-                (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
-        if (mDesiredWidgetHeight > 0 && isVerticallyResizable) {
-            if (info.maxResizeHeight > 0
-                    && info.maxResizeHeight >= info.minHeight
-                    && info.maxResizeHeight < mDesiredWidgetHeight) {
-                return rejectWidget(
-                        widget,
-                        "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
-                        info.maxResizeHeight,
-                        mDesiredWidgetHeight);
+                final int minWidth = Math.min(info.minResizeWidth, info.minWidth);
+                if (minWidth > mDesiredWidgetWidth) {
+                    return rejectWidget(
+                            widget,
+                            "min(minWidth[%d], minResizeWidth[%d]) > mDesiredWidgetWidth[%d]",
+                            info.minWidth,
+                            info.minResizeWidth,
+                            mDesiredWidgetWidth);
+                }
             }
 
-            final int minHeight = Math.min(info.minResizeHeight, info.minHeight);
-            if (minHeight > mDesiredWidgetHeight) {
-                return rejectWidget(
-                        widget,
-                        "min(minHeight[%d], minResizeHeight[%d]) > mDesiredWidgetHeight[%d]",
-                        info.minHeight,
-                        info.minResizeHeight,
-                        mDesiredWidgetHeight);
-            }
-        }
+            final boolean isVerticallyResizable =
+                    (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
+            if (mDesiredWidgetHeight > 0 && isVerticallyResizable) {
+                if (info.maxResizeHeight > 0
+                        && info.maxResizeHeight >= info.minHeight
+                        && info.maxResizeHeight < mDesiredWidgetHeight) {
+                    return rejectWidget(
+                            widget,
+                            "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
+                            info.maxResizeHeight,
+                            mDesiredWidgetHeight);
+                }
 
-        if (!isHorizontallyResizable || !isVerticallyResizable) {
-            return rejectWidget(widget, "not resizeable");
+                final int minHeight = Math.min(info.minResizeHeight, info.minHeight);
+                if (minHeight > mDesiredWidgetHeight) {
+                    return rejectWidget(
+                            widget,
+                            "min(minHeight[%d], minResizeHeight[%d]) > mDesiredWidgetHeight[%d]",
+                            info.minHeight,
+                            info.minResizeHeight,
+                            mDesiredWidgetHeight);
+                }
+            }
+
+            if (!isHorizontallyResizable || !isVerticallyResizable) {
+                return rejectWidget(widget, "not resizeable");
+            }
         }
 
         return acceptWidget(widget);
diff --git a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
index 84c2ed2..7a8b58e 100644
--- a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -31,12 +31,12 @@
 import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.ColorInt;
-import androidx.core.content.ContextCompat;
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.FloatingHeaderRow;
 import com.android.launcher3.allapps.FloatingHeaderView;
+import com.android.launcher3.util.Themes;
 
 /**
  * A view which shows a horizontal divider
@@ -84,10 +84,9 @@
                 getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height)
         };
 
-        mStrokeColor = ContextCompat.getColor(context, R.color.material_color_outline_variant);
+        mStrokeColor = Themes.getAttrColor(context, R.attr.materialColorOutlineVariant);
 
-        mAllAppsLabelTextColor = ContextCompat.getColor(context,
-                R.color.material_color_on_surface_variant);
+        mAllAppsLabelTextColor = Themes.getAttrColor(context, R.attr.materialColorOnSurfaceVariant);
 
         mAccessibilityManager = AccessibilityManager.getInstance(context);
         setShowAllAppsLabel(!ALL_APPS_VISITED_COUNT.hasReachedMax(context));
diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
index a16031d..92d9516 100644
--- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -290,6 +290,9 @@
         writer.println(prefix + "\tmPredictionsEnabled: " + mPredictionsEnabled);
         writer.println(prefix + "\tmPredictionUiUpdatePaused: " + mPredictionUiUpdatePaused);
         writer.println(prefix + "\tmNumPredictedAppsPerRow: " + mNumPredictedAppsPerRow);
-        writer.println(prefix + "\tmPredictedApps: " + mPredictedApps);
+        writer.println(prefix + "\tmPredictedApps: " + mPredictedApps.size());
+        for (WorkspaceItemInfo info : mPredictedApps) {
+            writer.println(prefix + "\t\t" + info);
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index de974ec..c50e82d 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -536,6 +536,9 @@
         writer.println(prefix + "HotseatPredictionController");
         writer.println(prefix + "\tFlags: " + getStateString(mPauseFlags));
         writer.println(prefix + "\tmHotSeatItemsCount: " + mHotSeatItemsCount);
-        writer.println(prefix + "\tmPredictedItems: " + mPredictedItems);
+        writer.println(prefix + "\tmPredictedItems: " + mPredictedItems.size());
+        for (ItemInfo info : mPredictedItems) {
+            writer.println(prefix + "\t\t" + info);
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
index 64bb05e..0395d32 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -65,7 +65,7 @@
                 Collectors.toSet());
         Predicate<WidgetItem> notOnWorkspace = w -> !widgetsInWorkspace.contains(w);
         Map<ComponentKey, WidgetItem> allWidgets =
-                dataModel.widgetsModel.getAllWidgetComponentsWithoutShortcuts();
+                dataModel.widgetsModel.getWidgetsByComponentKey();
 
         List<WidgetItem> servicePredictedItems = new ArrayList<>();
 
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index 62cc0bb..6c7fe5b 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -19,10 +19,9 @@
 
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
 
 import android.os.Debug;
-import android.os.SystemProperties;
 import android.util.Log;
 import android.view.View;
 
@@ -145,7 +144,7 @@
                 notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
             }
 
-            if (!enableDesktopWindowingWallpaperActivity() && wasVisible != isVisible) {
+            if (!WALLPAPER_ACTIVITY.isEnabled(mLauncher) && wasVisible != isVisible) {
                 // TODO: b/333533253 - Remove after flag rollout
                 if (mVisibleDesktopTasksCount > 0) {
                     setLauncherViewsVisibility(View.INVISIBLE);
@@ -189,7 +188,7 @@
                 notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
             }
 
-            if (enableDesktopWindowingWallpaperActivity()) {
+            if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
                 return;
             }
             // TODO: b/333533253 - Clean up after flag rollout
@@ -289,7 +288,7 @@
      * TODO: b/333533253 - Remove after flag rollout
      */
     private void setLauncherViewsVisibility(int visibility) {
-        if (enableDesktopWindowingWallpaperActivity()) {
+        if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
             return;
         }
         if (DEBUG) {
@@ -314,7 +313,7 @@
      * TODO: b/333533253 - Remove after flag rollout
      */
     private void markLauncherPaused() {
-        if (enableDesktopWindowingWallpaperActivity()) {
+        if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
             return;
         }
         if (DEBUG) {
@@ -331,7 +330,7 @@
      * TODO: b/333533253 - Remove after flag rollout
      */
     private void markLauncherResumed() {
-        if (enableDesktopWindowingWallpaperActivity()) {
+        if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
             return;
         }
         if (DEBUG) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 779009a..252ebf7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -19,8 +19,9 @@
 import static com.android.launcher3.statemanager.BaseState.FLAG_NON_INTERACTIVE;
 import static com.android.launcher3.taskbar.TaskbarEduTooltipControllerKt.TOOLTIP_STEP_FEATURES;
 import static com.android.launcher3.taskbar.TaskbarLauncherStateController.FLAG_VISIBLE;
+import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IGNORE_IN_APP;
 import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -213,7 +214,7 @@
 
         DesktopVisibilityController desktopController =
                 LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
-        if (!enableDesktopWindowingWallpaperActivity()
+        if (!WALLPAPER_ACTIVITY.isEnabled(mLauncher)
                 && desktopController != null
                 && desktopController.areDesktopTasksVisible()) {
             // TODO: b/333533253 - Remove after flag rollout
@@ -256,6 +257,28 @@
         return mTaskbarLauncherStateController.createAnimToLauncher(toState, callbacks, duration);
     }
 
+    /**
+     * Create Taskbar animation to be played alongside the Launcher app launch animation.
+     */
+    public @Nullable Animator createAnimToApp() {
+        TaskbarStashController stashController = mControllers.taskbarStashController;
+        stashController.updateStateForFlag(TaskbarStashController.FLAG_IN_APP, true);
+        return stashController.createApplyStateAnimator(stashController.getStashDuration());
+    }
+
+    /**
+     * Temporarily ignore FLAG_IN_APP for app launches to prevent premature taskbar stashing.
+     * This is needed because taskbar gets a signal to stash before we actually start the
+     * app launch animation.
+     */
+    public void setIgnoreInAppFlagForSync(boolean enabled) {
+        if (mControllers == null) {
+            // This method can be called before init() is called.
+            return;
+        }
+        mControllers.taskbarStashController.updateStateForFlag(FLAG_IGNORE_IN_APP, enabled);
+    }
+
     public void updateTaskbarLauncherStateGoingHome() {
         mTaskbarLauncherStateController.updateStateForFlag(FLAG_VISIBLE, true);
         mTaskbarLauncherStateController.applyState();
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 0fa3fbc..ea2adcf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -48,7 +48,6 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SHORTCUT_HELPER_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
-import static com.android.wm.shell.Flags.enableTaskbarOnPhones;
 
 import android.animation.ArgbEvaluator;
 import android.animation.ObjectAnimator;
@@ -80,6 +79,7 @@
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.WindowManager;
+import android.view.inputmethod.Flags;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -247,7 +247,7 @@
                 ? context.getColor(R.color.taskbar_nav_icon_light_color)
                 : context.getColor(R.color.taskbar_nav_icon_dark_color);
 
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions = new TaskbarTransitions(mContext, mNavButtonsView);
         }
     }
@@ -274,7 +274,10 @@
                 InputMethodService.canImeRenderGesturalNavButtons() && mContext.imeDrawsImeNavBar();
         if (!mIsImeRenderingNavButtons) {
             // IME switcher
-            mImeSwitcherButton = addButton(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH,
+            final int switcherResId = Flags.imeSwitcherRevamp()
+                    ? com.android.internal.R.drawable.ic_ime_switcher_new
+                    : R.drawable.ic_ime_switcher;
+            mImeSwitcherButton = addButton(switcherResId, BUTTON_IME_SWITCH,
                     isThreeButtonNav ? mStartContextualContainer : mEndContextualContainer,
                     mControllers.navButtonController, R.id.ime_switcher);
             mPropertyHolders.add(new StatePropertyHolder(mImeSwitcherButton,
@@ -361,7 +364,7 @@
                 R.bool.floating_rotation_button_position_left);
         mControllers.rotationButtonController.setRotationButton(mFloatingRotationButton,
                 mRotationButtonListener);
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions.init();
         }
 
@@ -369,7 +372,7 @@
         mPropertyHolders.forEach(StatePropertyHolder::endAnimation);
 
         // Initialize things needed to move nav buttons to separate window.
-        mSeparateWindowParent = new BaseDragLayer<TaskbarActivityContext>(mContext, null, 0) {
+        mSeparateWindowParent = new BaseDragLayer<>(mContext, null, 0) {
             @Override
             public void recreateControllers() {
                 mControllers = new TouchController[0];
@@ -625,7 +628,7 @@
     }
 
     public void setWallpaperVisible(boolean isVisible) {
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions.setWallpaperVisibility(isVisible);
         }
     }
@@ -638,20 +641,20 @@
     }
 
     public void checkNavBarModes() {
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             boolean isBarHidden = (mSysuiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0;
             mTaskbarTransitions.transitionTo(mTransitionMode, !isBarHidden);
         }
     }
 
     public void finishBarAnimations() {
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions.finishAnimations();
         }
     }
 
     public void touchAutoDim(boolean reset) {
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions.setAutoDim(false);
             mHandler.removeCallbacks(mAutoDim);
             if (reset) {
@@ -661,7 +664,7 @@
     }
 
     public void transitionTo(@BarTransitions.TransitionMode int barMode, boolean animate) {
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions.transitionTo(barMode, animate);
         }
     }
@@ -765,7 +768,7 @@
 
     private void onDarkIntensityChanged() {
         updateNavButtonColor();
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions.onDarkIntensityChanged(mTaskbarNavButtonDarkIntensity.value);
         }
     }
@@ -1115,7 +1118,7 @@
                 + mOnBackgroundNavButtonColorOverrideMultiplier.value);
 
         mNavButtonsView.dumpLogs(prefix + "\t", pw);
-        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+        if (mContext.isPhoneMode()) {
             mTaskbarTransitions.dumpLogs(prefix + "\t", pw);
         }
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 3048243..088c3cc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -315,6 +315,7 @@
                 new TaskbarTranslationController(this),
                 new TaskbarSpringOnStashController(this),
                 new TaskbarRecentAppsController(
+                        this,
                         RecentsModel.INSTANCE.get(this),
                         LauncherActivityInterface.INSTANCE::getDesktopVisibilityController),
                 TaskbarEduTooltipController.newInstance(this),
@@ -813,7 +814,7 @@
      */
     public void setUIController(@NonNull TaskbarUIController uiController) {
         mControllers.setUiController(uiController);
-        if (mControllers.bubbleControllers.isEmpty()) {
+        if (BubbleBarController.isBubbleBarEnabled() && mControllers.bubbleControllers.isEmpty()) {
             // if the bubble bar was visible in a previous configuration of taskbar and is being
             // recreated now without bubbles, clean up any bubble bar adjustments from hotseat
             bubbleBarVisibilityChanged(/* isVisible= */ false);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
index 7f9d8a3..19e9872 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
@@ -53,8 +53,7 @@
 
     private val activityContext: ActivityContext = ActivityContext.lookupContext(context)
 
-    private val backgroundColor =
-        Themes.getAttrColor(context, com.android.internal.R.attr.materialColorSurfaceBright)
+    private val backgroundColor = Themes.getAttrColor(context, R.attr.materialColorSurfaceBright)
 
     private val tooltipCornerRadius = Themes.getDialogCornerRadius(context)
     private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index 5c08116..49fc0dd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.taskbar
 
+import android.content.Context
 import androidx.annotation.VisibleForTesting
 import com.android.launcher3.Flags.enableRecentsInTaskbar
 import com.android.launcher3.model.data.ItemInfo
@@ -26,8 +27,8 @@
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.util.DesktopTask
 import com.android.quickstep.util.GroupTask
-import com.android.window.flags.Flags.enableDesktopWindowingMode
 import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps
+import com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE
 import java.io.PrintWriter
 
 /**
@@ -36,6 +37,7 @@
  * - When in Desktop Mode: show the currently running (open) Tasks
  */
 class TaskbarRecentAppsController(
+    context: Context,
     private val recentsModel: RecentsModel,
     // Pass a provider here instead of the actual DesktopVisibilityController instance since that
     // instance might not be available when this constructor is called.
@@ -44,10 +46,13 @@
 
     // TODO(b/335401172): unify DesktopMode checks in Launcher.
     var canShowRunningApps =
-        enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps()
+        DESKTOP_WINDOWING_MODE.isEnabled(context) && enableDesktopWindowingTaskbarRunningApps()
         @VisibleForTesting
         set(isEnabledFromTest) {
             field = isEnabledFromTest
+            if (!field && !canShowRecentApps) {
+                recentsModel.unregisterRecentTasksChangedListener()
+            }
         }
 
     // TODO(b/343532825): Add a setting to disable Recents even when the flag is on.
@@ -55,6 +60,9 @@
         @VisibleForTesting
         set(isEnabledFromTest) {
             field = isEnabledFromTest
+            if (!field && !canShowRunningApps) {
+                recentsModel.unregisterRecentTasksChangedListener()
+            }
         }
 
     // Initialized in init.
@@ -114,8 +122,10 @@
 
     fun init(taskbarControllers: TaskbarControllers) {
         controllers = taskbarControllers
-        recentsModel.registerRecentTasksChangedListener(recentTasksChangedListener)
-        reloadRecentTasksIfNeeded()
+        if (canShowRunningApps || canShowRecentApps) {
+            recentsModel.registerRecentTasksChangedListener(recentTasksChangedListener)
+            reloadRecentTasksIfNeeded()
+        }
     }
 
     fun onDestroy() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 267e19c..0c5ad42 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -95,6 +95,7 @@
     public static final int FLAG_STASHED_SYSUI = 1 << 9; //  app pinning,...
     public static final int FLAG_STASHED_DEVICE_LOCKED = 1 << 10; // device is locked: keyguard, ...
     public static final int FLAG_IN_OVERVIEW = 1 << 11; // launcher is in overview
+    public static final int FLAG_IGNORE_IN_APP = 1 << 12; // used to sync with app launch animation
 
     // If any of these flags are enabled, isInApp should return true.
     private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP;
@@ -331,9 +332,10 @@
         applyState(/* duration = */ 0);
 
         // Hide the background while stashed so it doesn't show on fast swipes home
-        boolean shouldHideTaskbarBackground = enableScalingRevealHomeAnimation()
-                && DisplayController.isTransientTaskbar(mActivity)
-                && isStashed();
+        boolean shouldHideTaskbarBackground = mActivity.isPhoneMode() ||
+                (enableScalingRevealHomeAnimation()
+                        && DisplayController.isTransientTaskbar(mActivity)
+                        && isStashed());
 
         mTaskbarBackgroundAlphaForStash.setValue(shouldHideTaskbarBackground ? 0 : 1);
 
@@ -1262,6 +1264,11 @@
          */
         @Nullable
         public Animator createSetStateAnimator(long flags, long duration) {
+            // We do this when we want to synchronize the app launch and taskbar stash animations.
+            if (hasAnyFlag(FLAG_IGNORE_IN_APP) && hasAnyFlag(flags, FLAG_IN_APP)) {
+                flags = flags & ~FLAG_IN_APP;
+            }
+
             boolean isStashed = mStashCondition.test(flags);
 
             if (DEBUG) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index c42d6c6..e58069a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -72,6 +72,7 @@
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
 import com.android.systemui.shared.recents.model.Task;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
 
 import java.util.List;
 import java.util.function.Predicate;
@@ -246,12 +247,34 @@
     @Override
     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
         if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
-            announceForAccessibility(mContext.getString(R.string.taskbar_a11y_shown_title));
+            announceTaskbarShown();
         } else if (action == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
-            announceForAccessibility(mContext.getString(R.string.taskbar_a11y_hidden_title));
+            announceTaskbarHidden();
         }
         return super.performAccessibilityActionInternal(action, arguments);
+    }
 
+    private void announceTaskbarShown() {
+        BubbleBarLocation bubbleBarLocation = mControllerCallbacks.getBubbleBarLocationIfVisible();
+        if (bubbleBarLocation == null) {
+            announceForAccessibility(mContext.getString(R.string.taskbar_a11y_shown_title));
+        } else if (bubbleBarLocation.isOnLeft(isLayoutRtl())) {
+            announceForAccessibility(
+                    mContext.getString(R.string.taskbar_a11y_shown_with_bubbles_left_title));
+        } else {
+            announceForAccessibility(
+                    mContext.getString(R.string.taskbar_a11y_shown_with_bubbles_right_title));
+        }
+    }
+
+    private void announceTaskbarHidden() {
+        BubbleBarLocation bubbleBarLocation = mControllerCallbacks.getBubbleBarLocationIfVisible();
+        if (bubbleBarLocation == null) {
+            announceForAccessibility(mContext.getString(R.string.taskbar_a11y_hidden_title));
+        } else {
+            announceForAccessibility(
+                    mContext.getString(R.string.taskbar_a11y_hidden_with_bubbles_title));
+        }
     }
 
     protected void announceAccessibilityChanges() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
index 3c646cb..e6cac2f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
@@ -23,8 +23,12 @@
 import android.view.MotionEvent;
 import android.view.View;
 
+import androidx.annotation.Nullable;
+
 import com.android.internal.jank.Cuj;
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
 
 /**
  * Callbacks for {@link TaskbarView} to interact with its controller.
@@ -104,4 +108,18 @@
         mControllers.taskbarScrimViewController.onTaskbarVisibilityChanged(
                 mTaskbarView.getVisibility());
     }
+
+    /**
+     * Get current location of bubble bar, if it is visible.
+     * Returns {@code null} if bubble bar is not shown.
+     */
+    @Nullable
+    public BubbleBarLocation getBubbleBarLocationIfVisible() {
+        BubbleBarViewController bubbleBarViewController =
+                mControllers.bubbleControllers.map(c -> c.bubbleBarViewController).orElse(null);
+        if (bubbleBarViewController != null && bubbleBarViewController.isBubbleBarVisible()) {
+            return bubbleBarViewController.getBubbleBarLocation();
+        }
+        return null;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 527e3a3..b21c414 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -46,6 +46,7 @@
 import android.view.animation.Interpolator;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.core.view.OneShotPreDrawListener;
 
 import com.android.app.animation.Interpolators;
@@ -96,6 +97,8 @@
     public static final int ALPHA_INDEX_SMALL_SCREEN = 6;
     private static final int NUM_ALPHA_CHANNELS = 7;
 
+    private static boolean sEnableModelLoadingForTests = true;
+
     private final TaskbarActivityContext mActivity;
     private final TaskbarView mTaskbarView;
     private final MultiValueAlpha mTaskbarIconAlpha;
@@ -192,7 +195,7 @@
         mTaskbarIconTranslationXForPinning.updateValue(pinningValue);
 
         mModelCallbacks.init(controllers);
-        if (mActivity.isUserSetupComplete()) {
+        if (mActivity.isUserSetupComplete() && sEnableModelLoadingForTests) {
             // Only load the callbacks if user setup is completed
             LauncherAppState.getInstance(mActivity).getModel().addCallbacksAndLoad(mModelCallbacks);
         }
@@ -924,4 +927,10 @@
 
         mModelCallbacks.dumpLogs(prefix + "\t", pw);
     }
+
+    /** Enables model loading for tests. */
+    @VisibleForTesting
+    public static void enableModelLoadingForTests(boolean enable) {
+        sEnableModelLoadingForTests = enable;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 7426dc7..5be0171 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -33,7 +33,6 @@
 
 import android.annotation.BinderThread;
 import android.annotation.Nullable;
-import android.app.Notification;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
@@ -463,15 +462,6 @@
     /** Tells WMShell to show the currently selected bubble. */
     public void showSelectedBubble() {
         if (getSelectedBubbleKey() != null) {
-            if (mSelectedBubble instanceof BubbleBarBubble) {
-                // Because we've visited this bubble, we should suppress the notification.
-                // This is updated on WMShell side when we show the bubble, but that update isn't
-                // passed to launcher, instead we apply it directly here.
-                BubbleInfo info = ((BubbleBarBubble) mSelectedBubble).getInfo();
-                info.setFlags(
-                        info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
-                mSelectedBubble.getView().updateDotVisibility(true /* animate */);
-            }
             mLastSentBubbleBarTop = mBarView.getRestingTopPositionOnScreen();
             mSystemUiProxy.showBubble(getSelectedBubbleKey(), mLastSentBubbleBarTop);
         } else {
@@ -645,8 +635,8 @@
 
         final TypedArray ta = mContext.obtainStyledAttributes(
                 new int[]{
-                        com.android.internal.R.attr.materialColorOnPrimaryFixed,
-                        com.android.internal.R.attr.materialColorPrimaryFixed
+                        R.attr.materialColorOnPrimaryFixed,
+                        R.attr.materialColorPrimaryFixed
                 });
         int overflowIconColor = ta.getColor(0, Color.WHITE);
         int overflowBackgroundColor = ta.getColor(1, Color.BLACK);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index fd989b1..af371f2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -30,6 +30,7 @@
 import android.content.Context;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.LayoutDirection;
@@ -38,6 +39,7 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 
 import androidx.dynamicanimation.animation.SpringForce;
@@ -240,6 +242,10 @@
                         BubbleView firstBubble = (BubbleView) getChildAt(0);
                         mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey());
                     }
+                    // If the bar was just expanded, remove the dot from the selected bubble.
+                    if (mIsBarExpanded && mSelectedBubbleView != null) {
+                        mSelectedBubbleView.markSeen();
+                    }
                     updateWidth();
                 },
                 /* onUpdate= */ animator -> {
@@ -301,6 +307,17 @@
         }
     }
 
+    @Override
+    public void setAlpha(float alpha) {
+        super.setAlpha(alpha);
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View childView = getChildAt(i);
+            if (!(childView instanceof BubbleView)) continue;
+            ((BubbleView) childView).setProvideShadowOutline(alpha == 1f);
+        }
+    }
+
     /**
      * Sets new icon sizes and newBubbleBarPadding between icons and bubble bar borders.
      *
@@ -363,6 +380,47 @@
         }
     }
 
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        // Always show only expand action as the menu is only for collapsed bubble bar
+        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
+        info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_dismiss_all,
+                getResources().getString(R.string.bubble_bar_action_dismiss_all)));
+        if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+            info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_right,
+                    getResources().getString(R.string.bubble_bar_action_move_right)));
+        } else {
+            info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_left,
+                    getResources().getString(R.string.bubble_bar_action_move_left)));
+        }
+    }
+
+    @Override
+    public boolean performAccessibilityActionInternal(int action,
+            @androidx.annotation.Nullable Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
+            mController.expandBubbleBar();
+            return true;
+        }
+        if (action == R.id.action_dismiss_all) {
+            mController.dismissBubbleBar();
+            return true;
+        }
+        if (action == R.id.action_move_left) {
+            mController.updateBubbleBarLocation(BubbleBarLocation.LEFT);
+            return true;
+        }
+        if (action == R.id.action_move_right) {
+            mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT);
+            return true;
+        }
+        return false;
+    }
+
     @SuppressLint("RtlHardcoded")
     private void onBubbleBarLocationChanged() {
         final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
@@ -665,7 +723,7 @@
     }
 
     /** Add a new bubble to the bubble bar. */
-    public void addBubble(View bubble) {
+    public void addBubble(BubbleView bubble) {
         FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
                 Gravity.LEFT);
         if (isExpanded()) {
@@ -673,6 +731,7 @@
             bubble.setScaleX(0f);
             bubble.setScaleY(0f);
             addView(bubble, 0, lp);
+            bubble.showDotIfNeeded(/* animate= */ false);
 
             mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
                     getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl()));
@@ -753,7 +812,6 @@
                 listener);
     }
 
-    // TODO: (b/280605790) animate it
     @Override
     public void addView(View child, int index, ViewGroup.LayoutParams params) {
         super.addView(child, index, params);
@@ -825,6 +883,23 @@
         updateBubbleAccessibilityStates();
         updateContentDescription();
         mDismissedByDragBubbleView = null;
+        updateNotificationDotsIfCollapsed();
+    }
+
+    private void updateNotificationDotsIfCollapsed() {
+        if (isExpanded()) {
+            return;
+        }
+        for (int i = 0; i < getChildCount(); i++) {
+            BubbleView bubbleView = (BubbleView) getChildAt(i);
+            // when we're collapsed, the first bubble should show the dot if it has it. the rest of
+            // the bubbles should hide their dots.
+            if (i == 0 && bubbleView.hasUnseenContent()) {
+                bubbleView.showDotIfNeeded(/* animate= */ true);
+            } else {
+                bubbleView.hideDot();
+            }
+        }
     }
 
     private void updateWidth() {
@@ -865,7 +940,6 @@
         float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight();
         // When translating X & Y the scale is ignored, so need to deduct it from the translations
         final float ty = bubbleBarAnimatedTop + mBubbleBarPadding - getScaleIconShift();
-        final boolean animate = getVisibility() == VISIBLE;
         final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl());
         // elevation state is opposite to widthState - when expanded all icons are flat
         float elevationState = (1 - widthState);
@@ -897,10 +971,10 @@
             bv.setZ(fullElevationForChild * elevationState);
 
             // only update the dot scale if we're expanding or collapsing
-            // TODO b/351904597: update the dot for the first bubble after removal and reorder
-            // since those might happen when the bar is collapsed and will need their dot back
             if (mWidthAnimator.isRunning()) {
-                bv.setDotScale(widthState);
+                // The dot for the selected bubble scales in the opposite direction of the expansion
+                // animation.
+                bv.showDotIfNeeded(bv == mSelectedBubbleView ? 1 - widthState : widthState);
             }
 
             if (mIsBarExpanded) {
@@ -1025,6 +1099,7 @@
             }
             updateBubblesLayoutProperties(mBubbleBarLocation);
             updateContentDescription();
+            updateNotificationDotsIfCollapsed();
         }
     }
 
@@ -1049,6 +1124,14 @@
         if (mBubbleAnimator == null) {
             updateArrowForSelected(previouslySelectedBubble != null);
         }
+        if (view != null) {
+            if (isExpanded()) {
+                view.markSeen();
+            } else {
+                // when collapsed, the selected bubble should show the dot if it has it
+                view.showDotIfNeeded(/* animate= */ true);
+            }
+        }
     }
 
     /**
@@ -1156,6 +1239,7 @@
                 mWidthAnimator.reverse();
             }
             updateBubbleAccessibilityStates();
+            announceExpandedStateChange();
         }
     }
 
@@ -1272,6 +1356,26 @@
         setContentDescription(contentDesc);
     }
 
+    private void announceExpandedStateChange() {
+        final CharSequence selectedBubbleContentDesc;
+        if (mSelectedBubbleView != null) {
+            selectedBubbleContentDesc = mSelectedBubbleView.getContentDescription();
+        } else {
+            selectedBubbleContentDesc = getResources().getString(
+                    R.string.bubble_bar_bubble_fallback_description);
+        }
+
+        final String msg;
+        if (mIsBarExpanded) {
+            msg = getResources().getString(R.string.bubble_bar_accessibility_announce_expand,
+                    selectedBubbleContentDesc);
+        } else {
+            msg = getResources().getString(R.string.bubble_bar_accessibility_announce_collapse,
+                    selectedBubbleContentDesc);
+        }
+        announceForAccessibility(msg);
+    }
+
     private boolean isIconSizeOrPaddingUpdated(float newIconSize, float newBubbleBarPadding) {
         return isIconSizeUpdated(newIconSize) || isPaddingUpdated(newBubbleBarPadding);
     }
@@ -1316,6 +1420,7 @@
     public void dump(PrintWriter pw) {
         pw.println("BubbleBarView state:");
         pw.println("  visibility: " + getVisibility());
+        pw.println("  alpha: " + getAlpha());
         pw.println("  translation Y: " + getTranslationY());
         pw.println("  bubbles in bar (childCount = " + getChildCount() + ")");
         for (BubbleView bubbleView: getBubbles()) {
@@ -1351,5 +1456,14 @@
 
         /** Notifies the controller that the bubble bar was touched while it was animating. */
         void onBubbleBarTouchedWhileAnimating();
+
+        /** Requests the controller to expand bubble bar */
+        void expandBubbleBar();
+
+        /** Requests the controller to dismiss the bubble bar */
+        void dismissBubbleBar();
+
+        /** Requests the controller to update bubble bar location to the given value */
+        void updateBubbleBarLocation(BubbleBarLocation location);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 24b9139..9270f68 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -31,6 +31,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -62,6 +63,7 @@
     private final TaskbarActivityContext mActivity;
     private final BubbleBarView mBarView;
     private int mIconSize;
+    private int mBubbleBarPadding;
 
     // Initialized in init.
     private BubbleStashController mBubbleStashController;
@@ -71,6 +73,7 @@
     private TaskbarInsetsController mTaskbarInsetsController;
     private View.OnClickListener mBubbleClickListener;
     private View.OnClickListener mBubbleBarClickListener;
+    private BubbleView.Controller mBubbleViewController;
 
     // These are exposed to {@link BubbleStashController} to animate for stashing/un-stashing
     private final MultiValueAlpha mBubbleBarAlpha;
@@ -110,13 +113,12 @@
         mTaskbarStashController = controllers.taskbarStashController;
         mTaskbarInsetsController = controllers.taskbarInsetsController;
         mBubbleBarViewAnimator = new BubbleBarViewAnimator(mBarView, mBubbleStashController);
-
+        onBubbleBarConfigurationChanged(/* animate= */ false);
         mActivity.addOnDeviceProfileChangeListener(
-                dp -> updateBubbleBarIconSize(dp.taskbarIconSize, /* animate= */ true));
-        updateBubbleBarIconSize(mActivity.getDeviceProfile().taskbarIconSize, /* animate= */ false);
+                dp -> onBubbleBarConfigurationChanged(/* animate= */ true));
         mBubbleBarScale.updateValue(1f);
-        mBubbleClickListener = v -> onBubbleClicked(v);
-        mBubbleBarClickListener = v -> onBubbleBarClicked();
+        mBubbleClickListener = v -> onBubbleClicked((BubbleView) v);
+        mBubbleBarClickListener = v -> expandBubbleBar();
         mBubbleDragController.setupBubbleBarView(mBarView);
         mBarView.setOnClickListener(mBubbleBarClickListener);
         mBarView.addOnLayoutChangeListener(
@@ -136,11 +138,52 @@
             public void onBubbleBarTouchedWhileAnimating() {
                 BubbleBarViewController.this.onBubbleBarTouchedWhileAnimating();
             }
+
+            @Override
+            public void expandBubbleBar() {
+                BubbleBarViewController.this.expandBubbleBar();
+            }
+
+            @Override
+            public void dismissBubbleBar() {
+                onDismissAllBubbles();
+            }
+
+            @Override
+            public void updateBubbleBarLocation(BubbleBarLocation location) {
+                mBubbleBarController.updateBubbleBarLocation(location);
+            }
         });
+
+        mBubbleViewController = new BubbleView.Controller() {
+            @Override
+            public BubbleBarLocation getBubbleBarLocation() {
+                return BubbleBarViewController.this.getBubbleBarLocation();
+            }
+
+            @Override
+            public void dismiss(BubbleView bubble) {
+                if (bubble.getBubble() != null) {
+                    notifySysUiBubbleDismissed(bubble.getBubble());
+                }
+                onBubbleDismissed(bubble);
+            }
+
+            @Override
+            public void collapse() {
+                collapseBubbleBar();
+            }
+
+            @Override
+            public void updateBubbleBarLocation(BubbleBarLocation location) {
+                mBubbleBarController.updateBubbleBarLocation(location);
+            }
+        };
     }
 
-    private void onBubbleClicked(View v) {
-        BubbleBarItem bubble = ((BubbleView) v).getBubble();
+    private void onBubbleClicked(BubbleView bubbleView) {
+        bubbleView.markSeen();
+        BubbleBarItem bubble = bubbleView.getBubble();
         if (bubble == null) {
             Log.e(TAG, "bubble click listener, bubble was null");
         }
@@ -148,8 +191,7 @@
         final String currentlySelected = mBubbleBarController.getSelectedBubbleKey();
         if (mBarView.isExpanded() && Objects.equals(bubble.getKey(), currentlySelected)) {
             // Tapping the currently selected bubble while expanded collapses the view.
-            setExpanded(false);
-            mBubbleStashController.stashBubbleBar();
+            collapseBubbleBar();
         } else {
             mBubbleBarController.showAndSelectBubble(bubble);
         }
@@ -160,7 +202,7 @@
         mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY());
     }
 
-    private void onBubbleBarClicked() {
+    private void expandBubbleBar() {
         if (mShouldShowEducation) {
             mShouldShowEducation = false;
             // Get the bubble bar bounds on screen
@@ -179,6 +221,11 @@
         }
     }
 
+    private void collapseBubbleBar() {
+        setExpanded(false);
+        mBubbleStashController.stashBubbleBar();
+    }
+
     /** Notifies that the stash state is changing. */
     public void onStashStateChanging() {
         if (isAnimatingNewBubble()) {
@@ -203,7 +250,7 @@
         return mBubbleBarTranslationY;
     }
 
-    float getBubbleBarCollapsedHeight() {
+    public float getBubbleBarCollapsedHeight() {
         return mBarView.getBubbleBarCollapsedHeight();
     }
 
@@ -334,27 +381,60 @@
     // Modifying view related properties.
     //
 
-    private void updateBubbleBarIconSize(int newIconSize, boolean animate) {
+    /** Notifies controller of configuration change, so bubble bar can be adjusted */
+    public void onBubbleBarConfigurationChanged(boolean animate) {
+        int newIconSize;
+        int newPadding;
         Resources res = mActivity.getResources();
+        if (mBubbleStashController.isBubblesShowingOnHome()) {
+            newIconSize = getBubbleBarIconSizeFromDeviceProfile(res);
+            newPadding = getBubbleBarPaddingFromDeviceProfile(res);
+        } else {
+            // the bubble bar is shown inside the persistent task bar, use preset sizes
+            newIconSize = res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_persistent_taskbar);
+            newPadding = res.getDimensionPixelSize(
+                    R.dimen.bubblebar_icon_spacing_persistent_taskbar);
+        }
+        updateBubbleBarIconSizeAndPadding(newIconSize, newPadding, animate);
+    }
+
+
+    private int getBubbleBarIconSizeFromDeviceProfile(Resources res) {
+        DeviceProfile deviceProfile = mActivity.getDeviceProfile();
         DisplayMetrics dm = res.getDisplayMetrics();
         float smallIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                 APP_ICON_SMALL_DP, dm);
         float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                 APP_ICON_MEDIUM_DP, dm);
-        float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
-                APP_ICON_LARGE_DP, dm);
         float smallMediumThreshold = (smallIconSize + mediumIconSize) / 2f;
-        float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f;
-        mIconSize = newIconSize <= smallMediumThreshold
+        int taskbarIconSize = deviceProfile.taskbarIconSize;
+        return taskbarIconSize <= smallMediumThreshold
                 ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_small) :
                 res.getDimensionPixelSize(R.dimen.bubblebar_icon_size);
-        float bubbleBarPadding = newIconSize >= mediumLargeThreshold
+
+    }
+
+    private int getBubbleBarPaddingFromDeviceProfile(Resources res) {
+        DeviceProfile deviceProfile = mActivity.getDeviceProfile();
+        DisplayMetrics dm = res.getDisplayMetrics();
+        float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                APP_ICON_MEDIUM_DP, dm);
+        float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                APP_ICON_LARGE_DP, dm);
+        float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f;
+        return deviceProfile.taskbarIconSize >= mediumLargeThreshold
                 ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_large) :
                 res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing);
+    }
+
+    private void updateBubbleBarIconSizeAndPadding(int iconSize, int padding, boolean animate) {
+        if (mIconSize == iconSize && mBubbleBarPadding == padding) return;
+        mIconSize = iconSize;
+        mBubbleBarPadding = padding;
         if (animate) {
-            mBarView.animateBubbleBarIconSize(mIconSize, bubbleBarPadding);
+            mBarView.animateBubbleBarIconSize(iconSize, padding);
         } else {
-            mBarView.setIconSizeAndPadding(mIconSize, bubbleBarPadding);
+            mBarView.setIconSizeAndPadding(iconSize, padding);
         }
     }
 
@@ -390,6 +470,7 @@
     public void removeBubble(BubbleBarBubble b) {
         if (b != null) {
             mBarView.removeBubble(b.getView());
+            b.getView().setController(null);
         } else {
             Log.w(TAG, "removeBubble, bubble was null!");
         }
@@ -400,6 +481,8 @@
             BubbleBarBubble removedBubble, boolean isExpanding, boolean suppressAnimation) {
         mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), removedBubble.getView());
         addedBubble.getView().setOnClickListener(mBubbleClickListener);
+        addedBubble.getView().setController(mBubbleViewController);
+        removedBubble.getView().setController(null);
         mBubbleDragController.setupBubbleView(addedBubble.getView());
         if (!suppressAnimation) {
             animateBubbleNotification(addedBubble, isExpanding, /* isUpdate= */ false);
@@ -414,6 +497,7 @@
             mBarView.addBubble(b.getView());
             b.getView().setOnClickListener(mBubbleClickListener);
             mBubbleDragController.setupBubbleView(b.getView());
+            b.getView().setController(mBubbleViewController);
 
             if (b instanceof BubbleBarOverflow) {
                 return;
@@ -530,8 +614,8 @@
         mSystemUiProxy.stopBubbleDrag(location, mBarView.getRestingTopPositionOnScreen());
     }
 
-    /** Notifies {@link BubbleBarView} that the dragged bubble was dismissed. */
-    public void onBubbleDragDismissed(BubbleView bubble) {
+    /** Handle given bubble being dismissed */
+    public void onBubbleDismissed(BubbleView bubble) {
         mBubbleBarController.onBubbleDismissed(bubble);
         mBarView.removeBubble(bubble);
     }
@@ -574,17 +658,16 @@
     }
 
     /**
-     * Called when bubble was dragged into the dismiss target. Notifies System
-     * @param bubble dismissed bubble item
+     * Notify SystemUI that the given bubble has been dismissed.
      */
-    public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) {
+    public void notifySysUiBubbleDismissed(@NonNull BubbleBarItem bubble) {
         mSystemUiProxy.dragBubbleToDismiss(bubble.getKey(), mTimeSource.currentTimeMillis());
     }
 
     /**
-     * Called when bubble stack was dragged into the dismiss target
+     * Called when bubble stack was dismissed
      */
-    public void onDismissAllBubblesWhileDragging() {
+    public void onDismissAllBubbles() {
         mSystemUiProxy.removeAllBubbles();
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
index a6096e2..5eebbd8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
@@ -143,10 +143,10 @@
         if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) {
             BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject();
             if (bubbleView.getBubble() != null) {
-                mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble());
+                mBubbleBarViewController.notifySysUiBubbleDismissed(bubbleView.getBubble());
             }
         } else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) {
-            mBubbleBarViewController.onDismissAllBubblesWhileDragging();
+            mBubbleBarViewController.onDismissAllBubbles();
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index 8316b5b..656a266 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -153,7 +153,7 @@
             @Override
             protected void onDragDismiss() {
                 mBubblePinController.onDragEnd();
-                mBubbleBarViewController.onBubbleDragDismissed(bubbleView);
+                mBubbleBarViewController.onBubbleDismissed(bubbleView);
                 mBubbleBarViewController.onBubbleDragEnd();
             }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 4c468bb..6db42a4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -16,17 +16,20 @@
 package com.android.launcher3.taskbar.bubbles;
 
 import android.annotation.Nullable;
+import android.app.Notification;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Outline;
 import android.graphics.Path;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewOutlineProvider;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.ImageView;
 
 import androidx.constraintlayout.widget.ConstraintLayout;
@@ -35,6 +38,8 @@
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconNormalizer;
 import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
+import com.android.wm.shell.common.bubbles.BubbleInfo;
 
 // TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
 
@@ -65,12 +70,17 @@
     // The current scale value of the dot
     private float mDotScale;
 
+    private boolean mProvideShadowOutline = true;
+
     // TODO: (b/273310265) handle RTL
     // Whether the bubbles are positioned on the left or right side of the screen
     private boolean mOnLeft = false;
 
     private BubbleBarItem mBubble;
 
+    @Nullable
+    private Controller mController;
+
     public BubbleView(Context context) {
         this(context, null);
     }
@@ -105,17 +115,28 @@
         });
     }
 
+    //TODO(b/345490679) remove once proper shadow is applied
+    /** Set whether provide an outline. */
+    public void setProvideShadowOutline(boolean provideOutline) {
+        if (mProvideShadowOutline == provideOutline) return;
+        mProvideShadowOutline = provideOutline;
+        invalidateOutline();
+    }
+
     private void getOutline(Outline outline) {
         updateBubbleSizeAndDotRender();
         final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize);
         final int inset = (mBubbleSize - normalizedSize) / 2;
-        outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
+        if (mProvideShadowOutline) {
+            outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
+        }
     }
 
     private void updateBubbleSizeAndDotRender() {
         int updatedBubbleSize = Math.min(getWidth(), getHeight());
         if (updatedBubbleSize == mBubbleSize) return;
         mBubbleSize = updatedBubbleSize;
+        invalidateOutline();
         if (mBubble == null || mBubble instanceof BubbleBarOverflow) return;
         Path dotPath = ((BubbleBarBubble) mBubble).getDotPath();
         mDotRenderer = new DotRenderer(mBubbleSize, dotPath, DEFAULT_PATH_SIZE);
@@ -178,6 +199,58 @@
         mDotRenderer.draw(canvas, mDrawParams);
     }
 
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
+        if (mBubble instanceof BubbleBarBubble) {
+            info.addAction(AccessibilityNodeInfo.ACTION_DISMISS);
+        }
+        if (mController != null) {
+            if (mController.getBubbleBarLocation().isOnLeft(isLayoutRtl())) {
+                info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_right,
+                        getResources().getString(R.string.bubble_bar_action_move_right)));
+            } else {
+                info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_left,
+                        getResources().getString(R.string.bubble_bar_action_move_left)));
+            }
+        }
+    }
+
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
+            if (mController != null) {
+                mController.collapse();
+            }
+            return true;
+        }
+        if (action == AccessibilityNodeInfo.ACTION_DISMISS) {
+            if (mController != null) {
+                mController.dismiss(this);
+            }
+            return true;
+        }
+        if (action == R.id.action_move_left) {
+            if (mController != null) {
+                mController.updateBubbleBarLocation(BubbleBarLocation.LEFT);
+            }
+        }
+        if (action == R.id.action_move_right) {
+            if (mController != null) {
+                mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT);
+            }
+        }
+        return false;
+    }
+
+    void setController(@Nullable Controller controller) {
+        mController = controller;
+    }
+
     /** Sets the bubble being rendered in this view. */
     public void setBubble(BubbleBarBubble bubble) {
         mBubble = bubble;
@@ -217,9 +290,9 @@
     }
 
     void updateDotVisibility(boolean animate) {
-        final float targetScale = shouldDrawDot() ? 1f : 0f;
+        final float targetScale = hasUnseenContent() ? 1f : 0f;
         if (animate) {
-            animateDotScale();
+            animateDotScale(targetScale);
         } else {
             mDotScale = targetScale;
             mAnimatingToDotScale = targetScale;
@@ -241,18 +314,27 @@
         mAppIcon.setVisibility(show ? VISIBLE : GONE);
     }
 
-    /** Whether the dot indicating unseen content in a bubble should be shown. */
-    private boolean shouldDrawDot() {
-        boolean bubbleHasUnseenContent = mBubble != null
+    boolean hasUnseenContent() {
+        return mBubble != null
                 && mBubble instanceof BubbleBarBubble
                 && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();
-        // Always render the dot if it's animating, since it could be animating out. Otherwise, show
-        // it if the bubble wants to show it, and we aren't suppressing it.
-        return bubbleHasUnseenContent || mDotIsAnimating;
     }
 
-    /** How big the dot should be, fraction from 0 to 1. */
-    void setDotScale(float fraction) {
+    /**
+     * Used to determine if we can skip drawing frames.
+     *
+     * <p>Generally we should draw the dot when it is requested to be shown and there is unseen
+     * content. But when the dot is removed, we still want to draw frames so that it can be scaled
+     * out.
+     */
+    private boolean shouldDrawDot() {
+        // if there's no dot there's nothing to draw, unless the dot was removed and we're in the
+        // middle of removing it
+        return hasUnseenContent() || mDotIsAnimating;
+    }
+
+    /** Updates the dot scale to the specified fraction from 0 to 1. */
+    private void setDotScale(float fraction) {
         if (!shouldDrawDot()) {
             return;
         }
@@ -260,11 +342,41 @@
         invalidate();
     }
 
-    /**
-     * Animates the dot to the given scale.
-     */
-    private void animateDotScale() {
-        float toScale = shouldDrawDot() ? 1f : 0f;
+    void showDotIfNeeded(float fraction) {
+        if (!hasUnseenContent()) {
+            return;
+        }
+        setDotScale(fraction);
+    }
+
+    void showDotIfNeeded(boolean animate) {
+        // only show the dot if we have unseen content
+        if (!hasUnseenContent()) {
+            return;
+        }
+        if (animate) {
+            animateDotScale(1f);
+        } else {
+            setDotScale(1f);
+        }
+    }
+
+    void hideDot() {
+        animateDotScale(0f);
+    }
+
+    /** Marks this bubble such that it no longer has unseen content, and hides the dot. */
+    void markSeen() {
+        if (mBubble instanceof BubbleBarBubble bubble) {
+            BubbleInfo info = bubble.getInfo();
+            info.setFlags(
+                    info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
+            hideDot();
+        }
+    }
+
+    /** Animates the dot to the given scale. */
+    private void animateDotScale(float toScale) {
         boolean isDotScaleChanging = Float.compare(mDotScale, toScale) != 0;
 
         // Don't restart the animation if we're already animating to the given value or if the dot
@@ -277,8 +389,6 @@
 
         final boolean showDot = toScale > 0f;
 
-        // Do NOT wait until after animation ends to setShowDot
-        // to avoid overriding more recent showDot states.
         clearAnimation();
         animate()
                 .setDuration(200)
@@ -293,10 +403,24 @@
                 }).start();
     }
 
-
     @Override
     public String toString() {
         String toString = mBubble != null ? mBubble.getKey() : "null";
         return "BubbleView{" + toString + "}";
     }
+
+    /** Interface for BubbleView to communicate with its controller */
+    public interface Controller {
+        /** Get current bubble bar {@link BubbleBarLocation} */
+        BubbleBarLocation getBubbleBarLocation();
+
+        /** This bubble should be dismissed */
+        void dismiss(BubbleView bubble);
+
+        /** Collapse the bubble bar */
+        void collapse();
+
+        /** Request bubble bar location to be updated to the given location */
+        void updateBubbleBarLocation(BubbleBarLocation location);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
new file mode 100644
index 0000000..59fc76c
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.stashing
+
+import android.view.InsetsController
+import android.view.MotionEvent
+import android.view.View
+import com.android.launcher3.taskbar.TaskbarInsetsController
+import com.android.launcher3.taskbar.bubbles.BubbleBarView
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
+import com.android.wm.shell.common.bubbles.BubbleBarLocation
+import com.android.wm.shell.shared.animation.PhysicsAnimator
+import java.io.PrintWriter
+
+/** StashController that defines stashing behaviour for the taskbar modes. */
+interface BubbleStashController {
+
+    /**
+     * Abstraction on the task bar activity context to only provide the dimensions required for
+     * [BubbleBarView] translation Y computation.
+     */
+    interface TaskbarHotseatDimensionsProvider {
+
+        /** Provides taskbar bottom space in pixels. */
+        fun getTaskbarBottomSpace(): Int
+
+        /** Provides taskbar height in pixels. */
+        fun getTaskbarHeight(): Int
+
+        /** Provides hotseat bottom space in pixels. */
+        fun getHotseatBottomSpace(): Int
+
+        /** Provides hotseat height in pixels. */
+        fun getHotseatHeight(): Int
+    }
+
+    /** Execute passed action only after controllers are initiated. */
+    interface ControllersAfterInitAction {
+        /** Execute action after controllers are initiated. */
+        fun runAfterInit(action: () -> Unit)
+    }
+
+    /** Whether bubble bar is currently stashed */
+    val isStashed: Boolean
+
+    /** Whether launcher enters or exits the home page. */
+    var isBubblesShowingOnHome: Boolean
+
+    /** Whether launcher enters or exits the overview page. */
+    var isBubblesShowingOnOverview: Boolean
+
+    /** Updated when sysui locked state changes, when locked, bubble bar is not shown. */
+    var isSysuiLocked: Boolean
+
+    /** Whether there is a transient taskbar mode */
+    val isTransientTaskBar: Boolean
+
+    /** Whether stash control has a handle view */
+    val hasHandleView: Boolean
+
+    /** Initialize controller */
+    fun init(
+        taskbarInsetsController: TaskbarInsetsController,
+        bubbleBarViewController: BubbleBarViewController,
+        bubbleStashedHandleViewController: BubbleStashedHandleViewController?,
+        controllersAfterInitAction: ControllersAfterInitAction
+    )
+
+    /** Shows the bubble bar at [bubbleBarTranslationY] position immediately without animation. */
+    fun showBubbleBarImmediate()
+
+    /** Shows the bubble bar at [bubbleBarTranslationY] position immediately without animation. */
+    fun showBubbleBarImmediate(bubbleBarTranslationY: Float)
+
+    /** Stashes the bubble bar immediately without animation. */
+    fun stashBubbleBarImmediate()
+
+    /** Returns the touchable height of the bubble bar based on it's stashed state. */
+    fun getTouchableHeight(): Int
+
+    /** Whether bubble bar is currently visible */
+    fun isBubbleBarVisible(): Boolean
+
+    /**
+     * Updates the values of the internal animators after the new bubble animation was interrupted
+     *
+     * @param isStashed whether the current state should be stashed
+     * @param bubbleBarTranslationY the current bubble bar translation. this is only used if the
+     *   bubble bar is showing to ensure that the stash animator runs smoothly.
+     */
+    fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float)
+
+    /** Checks whether the motion event is over the stash handle or bubble bar. */
+    fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean
+
+    /** Set a bubble bar location */
+    fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation)
+
+    /**
+     * Stashes the bubble bar (transform to the handle view), or just shrink width of the expanded
+     * bubble bar based on the controller implementation.
+     */
+    fun stashBubbleBar()
+
+    /** Shows the bubble bar, and expands bubbles depending on [expandBubbles]. */
+    fun showBubbleBar(expandBubbles: Boolean)
+
+    // TODO(b/354218264): Move to BubbleBarViewAnimator
+    /**
+     * The difference on the Y axis between the center of the handle and the center of the bubble
+     * bar.
+     */
+    fun getDiffBetweenHandleAndBarCenters(): Float
+
+    // TODO(b/354218264): Move to BubbleBarViewAnimator
+    /** The distance the handle moves as part of the new bubble animation. */
+    fun getStashedHandleTranslationForNewBubbleAnimation(): Float
+
+    // TODO(b/354218264): Move to BubbleBarViewAnimator
+    /** Returns the [PhysicsAnimator] for the stashed handle view. */
+    fun getStashedHandlePhysicsAnimator(): PhysicsAnimator<View>?
+
+    // TODO(b/354218264): Move to BubbleBarViewAnimator
+    /** Notifies taskbar that it should update its touchable region. */
+    fun updateTaskbarTouchRegion()
+
+    // TODO(b/354218264): Move to BubbleBarViewAnimator
+    /** Set the translation Y for the stashed handle. */
+    fun setHandleTranslationY(translationY: Float)
+
+    /**
+     * Returns bubble bar Y position according to [isBubblesShowingOnHome] and
+     * [isBubblesShowingOnOverview] values. Default implementation only analyse
+     * [isBubblesShowingOnHome] and return translationY to align with the hotseat vertical center.
+     * For Other cases align bubbles with the taskbar.
+     */
+    val bubbleBarTranslationY: Float
+        get() =
+            if (isBubblesShowingOnHome) {
+                bubbleBarTranslationYForHotseat
+            } else {
+                bubbleBarTranslationYForTaskbar
+            }
+
+    /** Translation Y to align the bubble bar with the hotseat. */
+    val bubbleBarTranslationYForTaskbar: Float
+
+    /** Return translation Y to align the bubble bar with the taskbar. */
+    val bubbleBarTranslationYForHotseat: Float
+
+    /** Dumps the state of BubbleStashController. */
+    fun dump(pw: PrintWriter) {
+        pw.println("Bubble stash controller state:")
+        pw.println("  isStashed: $isStashed")
+        pw.println("  isBubblesShowingOnOverview: $isBubblesShowingOnOverview")
+        pw.println("  isBubblesShowingOnHome: $isBubblesShowingOnHome")
+        pw.println("  isSysuiLocked: $isSysuiLocked")
+    }
+
+    companion object {
+        /** How long to stash/unstash. */
+        const val BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE.toLong()
+
+        /** How long to translate Y coordinate of the BubbleBar. */
+        const val BAR_TRANSLATION_DURATION = 300L
+
+        /** The scale bubble bar animates to when being stashed. */
+        const val STASHED_BAR_SCALE = 0.5f
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
new file mode 100644
index 0000000..62fe221
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.stashing
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.view.MotionEvent
+import android.view.View
+import com.android.launcher3.anim.AnimatedFloat
+import com.android.launcher3.taskbar.TaskbarInsetsController
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_DURATION
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_TRANSLATION_DURATION
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.ControllersAfterInitAction
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider
+import com.android.launcher3.util.MultiPropertyFactory
+import com.android.wm.shell.common.bubbles.BubbleBarLocation
+import com.android.wm.shell.shared.animation.PhysicsAnimator
+
+class PersistentBubbleStashController(
+    private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider,
+) : BubbleStashController {
+
+    private lateinit var taskbarInsetsController: TaskbarInsetsController
+    private lateinit var bubbleBarViewController: BubbleBarViewController
+    private lateinit var bubbleBarTranslationYAnimator: AnimatedFloat
+    private lateinit var bubbleBarAlphaAnimator: MultiPropertyFactory<View>.MultiProperty
+    private lateinit var bubbleBarScaleAnimator: AnimatedFloat
+    private lateinit var controllersAfterInitAction: ControllersAfterInitAction
+
+    override var isBubblesShowingOnHome: Boolean = false
+        set(onHome) {
+            if (field == onHome) return
+            field = onHome
+            if (!bubbleBarViewController.hasBubbles()) {
+                // if there are no bubbles, there's nothing to show, so just return.
+                return
+            }
+            if (onHome) {
+                // When transition to home we should show collapse the bubble bar
+                updateExpandedState(expand = false)
+            }
+            animateBubbleBarY()
+            bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true)
+        }
+
+    override var isBubblesShowingOnOverview: Boolean = false
+        set(onOverview) {
+            if (field == onOverview) return
+            field = onOverview
+            if (!onOverview) {
+                // When transition from overview we should show collapse the bubble bar
+                updateExpandedState(expand = false)
+            }
+            bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true)
+        }
+
+    override var isSysuiLocked: Boolean = false
+        set(isLocked) {
+            if (field == isLocked) return
+            field = isLocked
+            if (!isLocked && bubbleBarViewController.hasBubbles()) {
+                animateAfterUnlock()
+            }
+        }
+
+    override var isTransientTaskBar: Boolean = false
+
+    /** When the bubble bar is shown for the persistent task bar, there is no handle view. */
+    override val hasHandleView: Boolean = false
+
+    /** For persistent task bar we never stash the bubble bar */
+    override val isStashed: Boolean = false
+
+    override val bubbleBarTranslationYForTaskbar: Float
+        get() {
+            val taskbarBottomMargin = taskbarHotseatDimensionsProvider.getTaskbarBottomSpace()
+            val bubbleBarHeight: Float = bubbleBarViewController.bubbleBarCollapsedHeight
+            val taskbarHeight = taskbarHotseatDimensionsProvider.getTaskbarHeight()
+            return -taskbarBottomMargin - (taskbarHeight - bubbleBarHeight) / 2f
+        }
+
+    override val bubbleBarTranslationYForHotseat: Float
+        get() {
+            val hotseatBottomSpace = taskbarHotseatDimensionsProvider.getHotseatBottomSpace()
+            val hotseatCellHeight = taskbarHotseatDimensionsProvider.getHotseatHeight()
+            val bubbleBarHeight: Float = bubbleBarViewController.bubbleBarCollapsedHeight
+            return -hotseatBottomSpace - (hotseatCellHeight - bubbleBarHeight) / 2
+        }
+
+    override fun init(
+        taskbarInsetsController: TaskbarInsetsController,
+        bubbleBarViewController: BubbleBarViewController,
+        bubbleStashedHandleViewController: BubbleStashedHandleViewController?,
+        controllersAfterInitAction: ControllersAfterInitAction
+    ) {
+        this.taskbarInsetsController = taskbarInsetsController
+        this.bubbleBarViewController = bubbleBarViewController
+        this.controllersAfterInitAction = controllersAfterInitAction
+        bubbleBarTranslationYAnimator = bubbleBarViewController.bubbleBarTranslationY
+        // bubble bar has only alpha property, getting it at index 0
+        bubbleBarAlphaAnimator = bubbleBarViewController.bubbleBarAlpha.get(/* index= */ 0)
+        bubbleBarScaleAnimator = bubbleBarViewController.bubbleBarScale
+    }
+
+    private fun animateAfterUnlock() {
+        val animatorSet = AnimatorSet()
+        if (isBubblesShowingOnHome || isBubblesShowingOnOverview) {
+            animatorSet.playTogether(
+                bubbleBarScaleAnimator.animateToValue(1f),
+                bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY),
+                bubbleBarAlphaAnimator.animateToValue(1f)
+            )
+        }
+        updateTouchRegionOnAnimationEnd(animatorSet)
+        animatorSet.setDuration(BAR_STASH_DURATION).start()
+    }
+
+    override fun showBubbleBarImmediate() = showBubbleBarImmediate(bubbleBarTranslationY)
+
+    override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) {
+        bubbleBarTranslationYAnimator.updateValue(bubbleBarTranslationY)
+        bubbleBarAlphaAnimator.setValue(1f)
+        bubbleBarScaleAnimator.updateValue(1f)
+    }
+
+    override fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) {
+        // When the bubble bar is shown for the persistent task bar, there is no handle view, so no
+        // operation is performed.
+    }
+
+    override fun stashBubbleBar() {
+        updateExpandedState(expand = false)
+    }
+
+    override fun showBubbleBar(expandBubbles: Boolean) {
+        updateExpandedState(expandBubbles)
+    }
+
+    override fun stashBubbleBarImmediate() {
+        // When the bubble bar is shown for the persistent task bar, there is no handle view, so no
+        // operation is performed.
+    }
+
+    /** If bubble bar is visible return bubble bar height, 0 otherwise */
+    override fun getTouchableHeight() =
+        if (isBubbleBarVisible()) {
+            bubbleBarViewController.bubbleBarCollapsedHeight.toInt()
+        } else {
+            0
+        }
+
+    override fun isBubbleBarVisible(): Boolean = bubbleBarViewController.hasBubbles()
+
+    override fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float) {
+        showBubbleBarImmediate(bubbleBarTranslationY)
+    }
+
+    override fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean =
+        bubbleBarViewController.isEventOverAnyItem(ev)
+
+    override fun getDiffBetweenHandleAndBarCenters(): Float {
+        // distance from the bottom of the screen and the bubble bar center.
+        return -bubbleBarViewController.bubbleBarCollapsedHeight / 2f
+    }
+
+    /** When the bubble bar is shown for the persistent task bar, there is no handle view. */
+    override fun getStashedHandleTranslationForNewBubbleAnimation(): Float = 0f
+
+    /** When the bubble bar is shown for the persistent task bar, there is no handle view. */
+    override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator<View>? = null
+
+    override fun updateTaskbarTouchRegion() {
+        taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    /**
+     * When the bubble bar is shown for the persistent task bar the bar does not stash, so no
+     * operation is performed
+     */
+    override fun setHandleTranslationY(translationY: Float) {
+        // no op since does not have a handle view
+    }
+
+    private fun updateExpandedState(expand: Boolean) {
+        if (bubbleBarViewController.isHiddenForNoBubbles) {
+            // If there are no bubbles the bar is invisible, nothing to do here.
+            return
+        }
+        if (bubbleBarViewController.isExpanded != expand) {
+            bubbleBarViewController.isExpanded = expand
+        }
+    }
+
+    /** Animates bubble bar Y accordingly to the showing mode */
+    private fun animateBubbleBarY() {
+        val animator =
+            bubbleBarViewController.bubbleBarTranslationY.animateToValue(bubbleBarTranslationY)
+        updateTouchRegionOnAnimationEnd(animator)
+        animator.setDuration(BAR_TRANSLATION_DURATION)
+        animator.start()
+    }
+
+    private fun updateTouchRegionOnAnimationEnd(animator: Animator) {
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+
+                override fun onAnimationEnd(animation: Animator) {
+                    controllersAfterInitAction.runAfterInit {
+                        taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+                    }
+                }
+            }
+        )
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
new file mode 100644
index 0000000..23e009b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.stashing
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.content.res.Resources
+import android.view.MotionEvent
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.R
+import com.android.launcher3.anim.AnimatedFloat
+import com.android.launcher3.taskbar.StashedHandleViewController
+import com.android.launcher3.taskbar.TaskbarInsetsController
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_DURATION
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_TRANSLATION_DURATION
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.STASHED_BAR_SCALE
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.ControllersAfterInitAction
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider
+import com.android.launcher3.util.MultiPropertyFactory
+import com.android.wm.shell.common.bubbles.BubbleBarLocation
+import com.android.wm.shell.shared.animation.PhysicsAnimator
+
+class TransientBubbleStashController(
+    private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider,
+    resources: Resources
+) : BubbleStashController {
+
+    private lateinit var bubbleBarViewController: BubbleBarViewController
+    private lateinit var taskbarInsetsController: TaskbarInsetsController
+    private lateinit var controllersAfterInitAction: ControllersAfterInitAction
+
+    // stash view properties
+    private var bubbleStashedHandleViewController: BubbleStashedHandleViewController? = null
+    private var stashHandleViewAlpha: MultiPropertyFactory<View>.MultiProperty? = null
+    private var stashedHeight: Int = 0
+
+    // bubble bar properties
+    private lateinit var bubbleBarAlpha: MultiPropertyFactory<View>.MultiProperty
+    private lateinit var bubbleBarTranslationYAnimator: AnimatedFloat
+    private lateinit var bubbleBarScale: AnimatedFloat
+    private val mHandleCenterFromScreenBottom =
+        resources.getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f
+
+    private var animator: AnimatorSet? = null
+
+    override var isStashed: Boolean = false
+        @VisibleForTesting set
+
+    override var isBubblesShowingOnHome: Boolean = false
+        set(onHome) {
+            if (field == onHome) return
+            field = onHome
+            if (!bubbleBarViewController.hasBubbles()) {
+                // if there are no bubbles, there's nothing to show, so just return.
+                return
+            }
+            if (onHome) {
+                updateStashedAndExpandedState(stash = false, expand = false)
+                // When transitioning from app to home we need to animate the bubble bar
+                // here to align with hotseat center.
+                animateBubbleBarYToHotseat()
+            } else if (!bubbleBarViewController.isExpanded) {
+                updateStashedAndExpandedState(stash = true, expand = false)
+            }
+            bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true)
+        }
+
+    override var isBubblesShowingOnOverview: Boolean = false
+        set(onOverview) {
+            if (field == onOverview) return
+            field = onOverview
+            if (onOverview) {
+                // When transitioning to overview we need to animate the bubble bar to align with
+                // the taskbar bottom.
+                animateBubbleBarYToTaskbar()
+            } else {
+                updateStashedAndExpandedState(stash = true, expand = false)
+            }
+            bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true)
+        }
+
+    override var isSysuiLocked: Boolean = false
+        set(isLocked) {
+            if (field == isLocked) return
+            field = isLocked
+            if (!isLocked && bubbleBarViewController.hasBubbles()) {
+                animateAfterUnlock()
+            }
+        }
+
+    override val isTransientTaskBar: Boolean = true
+
+    override val bubbleBarTranslationYForHotseat: Float
+        get() {
+            val hotseatBottomSpace = taskbarHotseatDimensionsProvider.getHotseatBottomSpace()
+            val hotseatCellHeight = taskbarHotseatDimensionsProvider.getHotseatHeight()
+            val bubbleBarHeight: Float = bubbleBarViewController.bubbleBarCollapsedHeight
+            return -hotseatBottomSpace - (hotseatCellHeight - bubbleBarHeight) / 2
+        }
+
+    override val bubbleBarTranslationYForTaskbar: Float =
+        -taskbarHotseatDimensionsProvider.getTaskbarBottomSpace().toFloat()
+
+    /** Check if we have handle view controller */
+    override val hasHandleView: Boolean
+        get() = bubbleStashedHandleViewController != null
+
+    override fun init(
+        taskbarInsetsController: TaskbarInsetsController,
+        bubbleBarViewController: BubbleBarViewController,
+        bubbleStashedHandleViewController: BubbleStashedHandleViewController?,
+        controllersAfterInitAction: ControllersAfterInitAction
+    ) {
+        this.taskbarInsetsController = taskbarInsetsController
+        this.bubbleBarViewController = bubbleBarViewController
+        this.bubbleStashedHandleViewController = bubbleStashedHandleViewController
+        this.controllersAfterInitAction = controllersAfterInitAction
+        bubbleBarTranslationYAnimator = bubbleBarViewController.bubbleBarTranslationY
+        // bubble bar has only alpha property, getting it at index 0
+        bubbleBarAlpha = bubbleBarViewController.bubbleBarAlpha.get(/* index= */ 0)
+        bubbleBarScale = bubbleBarViewController.bubbleBarScale
+        stashedHeight = bubbleStashedHandleViewController?.stashedHeight ?: 0
+        stashHandleViewAlpha =
+            bubbleStashedHandleViewController
+                ?.stashedHandleAlpha
+                ?.get(StashedHandleViewController.ALPHA_INDEX_STASHED)
+    }
+
+    private fun animateAfterUnlock() {
+        val animatorSet = AnimatorSet()
+        if (isBubblesShowingOnHome || isBubblesShowingOnOverview) {
+            isStashed = false
+            animatorSet.playTogether(
+                bubbleBarScale.animateToValue(1f),
+                bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY),
+                bubbleBarAlpha.animateToValue(1f)
+            )
+        } else {
+            isStashed = true
+            stashHandleViewAlpha?.let { animatorSet.playTogether(it.animateToValue(1f)) }
+        }
+        animatorSet.updateTouchRegionOnAnimationEnd().setDuration(BAR_STASH_DURATION).start()
+    }
+
+    override fun showBubbleBarImmediate() {
+        showBubbleBarImmediate(bubbleBarTranslationY)
+    }
+
+    override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) {
+        bubbleStashedHandleViewController?.setTranslationYForSwipe(0f)
+        stashHandleViewAlpha?.value = 0f
+        this.bubbleBarTranslationYAnimator.updateValue(bubbleBarTranslationY)
+        bubbleBarAlpha.setValue(1f)
+        bubbleBarScale.updateValue(1f)
+        isStashed = false
+        onIsStashedChanged()
+    }
+
+    override fun stashBubbleBarImmediate() {
+        bubbleStashedHandleViewController?.setTranslationYForSwipe(0f)
+        stashHandleViewAlpha?.value = 1f
+        this.bubbleBarTranslationYAnimator.updateValue(getStashTranslation())
+        bubbleBarAlpha.setValue(0f)
+        bubbleBarScale.updateValue(STASHED_BAR_SCALE)
+        isStashed = true
+        onIsStashedChanged()
+    }
+
+    override fun getTouchableHeight(): Int =
+        when {
+            isStashed -> stashedHeight
+            isBubbleBarVisible() -> bubbleBarViewController.bubbleBarCollapsedHeight.toInt()
+            else -> 0
+        }
+
+    override fun isBubbleBarVisible(): Boolean = bubbleBarViewController.hasBubbles() && !isStashed
+
+    override fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float) =
+        if (isStashed) {
+            stashBubbleBarImmediate()
+        } else {
+            showBubbleBarImmediate(bubbleBarTranslationY)
+        }
+
+    /** Check if [ev] belongs to the stash handle or the bubble bar views. */
+    override fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean {
+        val isOverHandle = bubbleStashedHandleViewController?.isEventOverHandle(ev) ?: false
+        return isOverHandle || bubbleBarViewController.isEventOverAnyItem(ev)
+    }
+
+    /** Set the bubble bar stash handle location . */
+    override fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) {
+        bubbleStashedHandleViewController?.setBubbleBarLocation(bubbleBarLocation)
+    }
+
+    override fun stashBubbleBar() {
+        updateStashedAndExpandedState(stash = true, expand = false)
+    }
+
+    override fun showBubbleBar(expandBubbles: Boolean) {
+        updateStashedAndExpandedState(stash = false, expandBubbles)
+    }
+
+    override fun getDiffBetweenHandleAndBarCenters(): Float {
+        // the difference between the centers of the handle and the bubble bar is the difference
+        // between their distance from the bottom of the screen.
+        val barCenter: Float = bubbleBarViewController.bubbleBarCollapsedHeight / 2f
+        return mHandleCenterFromScreenBottom - barCenter
+    }
+
+    override fun getStashedHandleTranslationForNewBubbleAnimation(): Float {
+        return -mHandleCenterFromScreenBottom
+    }
+
+    override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator<View>? {
+        return bubbleStashedHandleViewController?.physicsAnimator
+    }
+
+    override fun updateTaskbarTouchRegion() {
+        taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    override fun setHandleTranslationY(translationY: Float) {
+        bubbleStashedHandleViewController?.setTranslationYForSwipe(translationY)
+    }
+
+    private fun getStashTranslation(): Float {
+        return (bubbleBarViewController.bubbleBarCollapsedHeight - stashedHeight) / 2f
+    }
+
+    /**
+     * Create a stash animation.
+     *
+     * @param isStashed whether it's a stash animation or an unstash animation
+     * @param duration duration of the animation
+     * @return the animation
+     */
+    @Suppress("SameParameterValue")
+    private fun createStashAnimator(isStashed: Boolean, duration: Long): AnimatorSet {
+        val animatorSet = AnimatorSet()
+        val fullLengthAnimatorSet = AnimatorSet()
+        // Not exactly half and may overlap. See [first|second]HalfDurationScale below.
+        val firstHalfAnimatorSet = AnimatorSet()
+        val secondHalfAnimatorSet = AnimatorSet()
+        val firstHalfDurationScale: Float
+        val secondHalfDurationScale: Float
+        val stashHandleAlphaValue: Float
+        if (isStashed) {
+            firstHalfDurationScale = 0.75f
+            secondHalfDurationScale = 0.5f
+            stashHandleAlphaValue = 1f
+            fullLengthAnimatorSet.play(
+                bubbleBarTranslationYAnimator.animateToValue(getStashTranslation())
+            )
+            firstHalfAnimatorSet.playTogether(
+                bubbleBarAlpha.animateToValue(0f),
+                bubbleBarScale.animateToValue(STASHED_BAR_SCALE)
+            )
+        } else {
+            firstHalfDurationScale = 0.5f
+            secondHalfDurationScale = 0.75f
+            stashHandleAlphaValue = 0f
+            fullLengthAnimatorSet.playTogether(
+                bubbleBarScale.animateToValue(1f),
+                bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY)
+            )
+            secondHalfAnimatorSet.playTogether(bubbleBarAlpha.animateToValue(1f))
+        }
+        stashHandleViewAlpha?.let {
+            secondHalfAnimatorSet.playTogether(it.animateToValue(stashHandleAlphaValue))
+        }
+        bubbleStashedHandleViewController?.createRevealAnimToIsStashed(isStashed)?.let {
+            fullLengthAnimatorSet.play(it)
+        }
+        fullLengthAnimatorSet.setDuration(duration)
+        firstHalfAnimatorSet.setDuration((duration * firstHalfDurationScale).toLong())
+        secondHalfAnimatorSet.setDuration((duration * secondHalfDurationScale).toLong())
+        secondHalfAnimatorSet.startDelay = (duration * (1 - secondHalfDurationScale)).toLong()
+        animatorSet.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet, secondHalfAnimatorSet)
+        animatorSet.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    animator = null
+                    controllersAfterInitAction.runAfterInit {
+                        if (isStashed) {
+                            bubbleBarViewController.isExpanded = false
+                        }
+                        taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+                    }
+                }
+            }
+        )
+        return animatorSet
+    }
+
+    private fun onIsStashedChanged() {
+        controllersAfterInitAction.runAfterInit {
+            taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+            bubbleStashedHandleViewController?.onIsStashedChanged()
+        }
+    }
+
+    private fun animateBubbleBarYToHotseat() {
+        translateBubbleBarYUpdateTouchRegionOnCompletion(bubbleBarTranslationYForHotseat)
+    }
+
+    private fun animateBubbleBarYToTaskbar() {
+        translateBubbleBarYUpdateTouchRegionOnCompletion(bubbleBarTranslationYForTaskbar)
+    }
+
+    private fun translateBubbleBarYUpdateTouchRegionOnCompletion(toY: Float) {
+        bubbleBarViewController.bubbleBarTranslationY
+            .animateToValue(toY)
+            .updateTouchRegionOnAnimationEnd()
+            .setDuration(BAR_TRANSLATION_DURATION)
+            .start()
+    }
+
+    @VisibleForTesting
+    fun updateStashedAndExpandedState(stash: Boolean, expand: Boolean) {
+        if (bubbleBarViewController.isHiddenForNoBubbles) {
+            // If there are no bubbles the bar and handle are invisible, nothing to do here.
+            return
+        }
+        val isStashed = stash && !isBubblesShowingOnHome && !isBubblesShowingOnOverview
+        if (this.isStashed != isStashed) {
+            this.isStashed = isStashed
+            // notify the view controller that the stash state is about to change so that it can
+            // cancel an ongoing animation if there is one.
+            // note that this has to be called before updating mIsStashed with the new value,
+            // otherwise interrupting an ongoing animation may update it again with the wrong state
+            bubbleBarViewController.onStashStateChanging()
+            animator?.cancel()
+            animator =
+                createStashAnimator(isStashed, BAR_STASH_DURATION).apply {
+                    updateTouchRegionOnAnimationEnd()
+                    start()
+                }
+        }
+        if (bubbleBarViewController.isExpanded != expand) {
+            bubbleBarViewController.isExpanded = expand
+        }
+    }
+
+    private fun Animator.updateTouchRegionOnAnimationEnd(): Animator {
+        this.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    onIsStashedChanged()
+                }
+            }
+        )
+        return this
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index be6f690..d678c46 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -64,10 +64,10 @@
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FALLBACK;
 import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
 import static com.android.quickstep.util.SplitAnimationTimings.TABLET_HOME_TO_SPLIT;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -201,6 +201,8 @@
 import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver;
 import com.android.systemui.unfold.updates.RotationChangeProvider;
 
+import kotlin.Unit;
+
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -213,8 +215,6 @@
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
-import kotlin.Unit;
-
 public class QuickstepLauncher extends Launcher implements RecentsViewContainer {
     private static final boolean TRACE_LAYOUTS =
             SystemProperties.getBoolean("persist.debug.trace_layouts", false);
@@ -256,6 +256,10 @@
 
     private boolean mIsPredictiveBackToHomeInProgress;
 
+    private boolean mCanShowAllAppsEducationView;
+
+    private boolean mIsOverlayVisible;
+
     public static QuickstepLauncher getLauncher(Context context) {
         return fromContext(context);
     }
@@ -276,7 +280,7 @@
         // TODO(b/337863494): Explore use of the same OverviewComponentObserver across launcher
         OverviewComponentObserver overviewComponentObserver = new OverviewComponentObserver(
                 asContext(), deviceState);
-        if (enableDesktopWindowingMode()) {
+        if (DESKTOP_WINDOWING_MODE.isEnabled(this)) {
             mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
                     getStateManager(), systemUiProxy, getIApplicationThread(),
                     getDepthController());
@@ -296,7 +300,7 @@
 
         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
         mDepthController = new DepthController(this);
-        if (enableDesktopWindowingMode()) {
+        if (DESKTOP_WINDOWING_MODE.isEnabled(this)) {
             mDesktopVisibilityController = new DesktopVisibilityController(this);
             mDesktopVisibilityController.registerSystemUiListener();
             mSplitSelectStateController.initSplitFromDesktopController(this,
@@ -493,7 +497,8 @@
                     (getActivityFlags() & ACTIVITY_STATE_USER_WILL_BE_ACTIVE) != 0;
             boolean visible = (state == NORMAL || state == OVERVIEW)
                     && (willUserBeActive || isUserActive())
-                    && !profile.isVerticalBarLayout();
+                    && !profile.isVerticalBarLayout()
+                    && !mIsOverlayVisible;
             SystemUiProxy.INSTANCE.get(this)
                     .setLauncherKeepClearAreaHeight(visible, profile.hotseatBarSizePx);
         }
@@ -503,6 +508,12 @@
     }
 
     @Override
+    public void onOverlayVisibilityChanged(boolean visible) {
+        super.onOverlayVisibilityChanged(visible);
+        mIsOverlayVisible = visible;
+    }
+
+    @Override
     public void bindExtraContainerItems(FixedContainerItems item) {
         if (item.containerId == Favorites.CONTAINER_PREDICTION) {
             mAllAppsPredictions = item;
@@ -513,7 +524,7 @@
         } else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) {
             mHotseatPredictionController.setPredictedItems(item);
         } else if (item.containerId == Favorites.CONTAINER_WIDGETS_PREDICTION) {
-            getPopupDataProvider().setRecommendedWidgets(item.items);
+            getWidgetPickerDataProvider().setWidgetRecommendations(item.items);
         }
     }
 
@@ -1004,7 +1015,7 @@
 
     @Override
     public void setResumed() {
-        if (!enableDesktopWindowingWallpaperActivity()
+        if (!WALLPAPER_ACTIVITY.isEnabled(this)
                 && mDesktopVisibilityController != null
                 && mDesktopVisibilityController.areDesktopTasksVisible()
                 && !mDesktopVisibilityController.isRecentsGestureInProgress()) {
@@ -1494,4 +1505,12 @@
     public boolean isRecentsViewVisible() {
         return getStateManager().getState().isRecentsViewVisible;
     }
+
+    public boolean isCanShowAllAppsEducationView() {
+        return mCanShowAllAppsEducationView;
+    }
+
+    public void setCanShowAllAppsEducationView(boolean canShowAllAppsEducationView) {
+        mCanShowAllAppsEducationView = canShowAllAppsEducationView;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
index 3325009..d1aa472 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -39,7 +39,6 @@
 import android.view.ViewConfiguration;
 
 import com.android.internal.jank.Cuj;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -88,13 +87,17 @@
     // Normal to Hint animation has flag SKIP_OVERVIEW, so we update this scrim with this animator.
     private ObjectAnimator mNormalToHintOverviewScrimAnimator;
 
+    private final QuickstepLauncher mLauncher;
+    private boolean mIsTrackpadSwipe;
+
     /**
      * @param cancelSplitRunnable Called when split placeholder view needs to be cancelled.
      *                            Animation should be added to the provided AnimatorSet
      */
-    public NoButtonNavbarToOverviewTouchController(Launcher l,
+    public NoButtonNavbarToOverviewTouchController(QuickstepLauncher l,
             BiConsumer<AnimatorSet, Long> cancelSplitRunnable) {
         super(l);
+        mLauncher = l;
         mRecentsView = l.getOverviewPanel();
         mMotionPauseDetector = new MotionPauseDetector(l);
         mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
@@ -104,7 +107,9 @@
 
     @Override
     protected boolean canInterceptTouch(MotionEvent ev) {
-        if (!isTrackpadMotionEvent(ev) && DisplayController.getNavigationMode(mLauncher)
+        mIsTrackpadSwipe = isTrackpadMotionEvent(ev);
+        mLauncher.setCanShowAllAppsEducationView(!mIsTrackpadSwipe);
+        if (!mIsTrackpadSwipe && DisplayController.getNavigationMode(mLauncher)
                 == THREE_BUTTONS) {
             return false;
         }
@@ -148,6 +153,7 @@
         super.onDragStart(start, startDisplacement);
 
         mMotionPauseDetector.clear();
+        mMotionPauseDetector.setIsTrackpadGesture(mIsTrackpadSwipe);
 
         if (handlingOverviewAnim()) {
             InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS,
@@ -191,6 +197,7 @@
         }
 
         mMotionPauseDetector.clear();
+        mIsTrackpadSwipe = false;
         mNormalToHintOverviewScrimAnimator = null;
         if (mLauncher.isInState(OVERVIEW)) {
             // Normally we would cleanup the state based on mCurrentAnimation, but since we stop
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index ab277b6..0da7b2d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -121,6 +121,7 @@
     private AnimatorPlaybackController mNonOverviewAnim;
     private AnimatorPlaybackController mXOverviewAnim;
     private AnimatedFloat mYOverviewAnim;
+    private boolean mIsTrackpadSwipe;
 
     public NoButtonQuickSwitchTouchController(QuickstepLauncher launcher) {
         mLauncher = launcher;
@@ -177,7 +178,8 @@
             return false;
         }
         if (isTrackpadMultiFingerSwipe(ev)) {
-            return isTrackpadFourFingerSwipe(ev);
+            mIsTrackpadSwipe = isTrackpadFourFingerSwipe(ev);
+            return mIsTrackpadSwipe;
         }
         return true;
     }
@@ -185,6 +187,7 @@
     @Override
     public void onDragStart(boolean start) {
         mMotionPauseDetector.clear();
+        mMotionPauseDetector.setIsTrackpadGesture(mIsTrackpadSwipe);
         if (start) {
             InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
             InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS,
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index f020c8f..5a03ae6 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -62,6 +62,7 @@
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -148,10 +149,12 @@
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
-import com.android.window.flags.Flags;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
 import com.android.wm.shell.startingsurface.SplashScreenExitAnimationUtils;
 
+import kotlin.Unit;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -161,8 +164,6 @@
 import java.util.OptionalInt;
 import java.util.function.Consumer;
 
-import kotlin.Unit;
-
 /**
  * Handles the navigation gestures when Launcher is the default home activity.
  */
@@ -952,7 +953,7 @@
     public void onRecentsAnimationStart(RecentsAnimationController controller,
             RecentsAnimationTargets targets) {
         super.onRecentsAnimationStart(controller, targets);
-        if (targets.hasDesktopTasks()) {
+        if (targets.hasDesktopTasks(mContext)) {
             mRemoteTargetHandles = mTargetGluer.assignTargetsForDesktop(targets);
         } else {
             int untrimmedAppCount = mRemoteTargetHandles.length;
@@ -1272,9 +1273,9 @@
         TaskView currentPageTaskView = mRecentsView != null
                 ? mRecentsView.getCurrentPageTaskView() : null;
 
-        if (Flags.enableDesktopWindowingMode()
-                && !(Flags.enableDesktopWindowingWallpaperActivity()
-                && Flags.enableDesktopWindowingQuickSwitch())) {
+        if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+                && !(DesktopModeFlags.WALLPAPER_ACTIVITY.isEnabled(mContext)
+                && DesktopModeFlags.QUICK_SWITCH.isEnabled(mContext))) {
             if ((nextPageTaskView instanceof DesktopTaskView
                     || currentPageTaskView instanceof DesktopTaskView)
                     && endTarget == NEW_TASK) {
@@ -1445,9 +1446,9 @@
             setClampScrollOffset(false);
         };
 
-        if (Flags.enableDesktopWindowingMode()
-                && !(Flags.enableDesktopWindowingWallpaperActivity()
-                && Flags.enableDesktopWindowingQuickSwitch())) {
+        if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+                && !(DesktopModeFlags.WALLPAPER_ACTIVITY.isEnabled(mContext)
+                && DesktopModeFlags.QUICK_SWITCH.isEnabled(mContext))) {
             if (mRecentsView != null && (mRecentsView.getCurrentPageTaskView() != null
                     && !(mRecentsView.getCurrentPageTaskView() instanceof DesktopTaskView))) {
                 ActiveGestureLog.INSTANCE.trackEvent(ActiveGestureErrorDetector.GestureEvent
@@ -1631,14 +1632,17 @@
                             mRecentsAnimationController.screenshotTask(taskId));
                 });
 
-                // let SystemUi reparent the overlay leash as soon as possible
+                // let SystemUi reparent the overlay leash as soon as possible;
+                // make sure to pass in an empty src-rect-hint if overlay is present, since we
+                // use our own calculated source-rect-hint for the animation.
                 SystemUiProxy.INSTANCE.get(mContext).stopSwipePipToHome(
                         mSwipePipToHomeAnimator.getTaskId(),
                         mSwipePipToHomeAnimator.getComponentName(),
                         mSwipePipToHomeAnimator.getDestinationBounds(),
                         mSwipePipToHomeAnimator.getContentOverlay(),
                         mSwipePipToHomeAnimator.getAppBounds(),
-                        mSwipePipToHomeAnimator.getSourceRectHint());
+                        mSwipePipToHomeAnimator.getContentOverlay() != null ? new Rect()
+                                : mSwipePipToHomeAnimator.getSourceRectHint());
 
                 windowAnim = mSwipePipToHomeAnimators;
             } else {
@@ -2290,9 +2294,9 @@
                     mRecentsAnimationController, mRecentsAnimationTargets);
         });
 
-        if (Flags.enableDesktopWindowingMode()
-                && !(Flags.enableDesktopWindowingWallpaperActivity()
-                        && Flags.enableDesktopWindowingQuickSwitch())) {
+        if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+                && !(DesktopModeFlags.WALLPAPER_ACTIVITY.isEnabled(mContext)
+                        && DesktopModeFlags.QUICK_SWITCH.isEnabled(mContext))) {
             if (mRecentsView.getNextPageTaskView() instanceof DesktopTaskView
                     || mRecentsView.getCurrentPageTaskView() instanceof DesktopTaskView) {
                 mRecentsViewScrollLinked = false;
diff --git a/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt b/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
index 3549a12..904ed69 100644
--- a/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
+++ b/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
@@ -64,11 +64,19 @@
             "Enable two stage for LPNH duration and touch slop"
         )
 
-    val twoStageMultiplier =
+    val twoStageDurationPercentage =
         propReader.get(
-            "TWO_STAGE_MULTIPLIER",
-            2,
-            "Extends the duration and touch slop if the initial slop is passed"
+            "TWO_STAGE_DURATION_PERCENTAGE",
+            200,
+            "Extends the duration to trigger a long press after a fraction of the gesture " +
+                "slop is passed, expressed as a percentage (i.e. 200 = 2x)."
+        )
+
+    val twoStageSlopPercentage =
+        propReader.get(
+            "TWO_STAGE_SLOP_PERCENTAGE",
+            50,
+            "Percentage of gesture slop region to trigger the extended long press duration."
         )
 
     val animateLpnh = propReader.get("ANIMATE_LPNH", false, "Animates navbar when long pressing")
diff --git a/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt b/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
index 27bd03d..7d22c52 100644
--- a/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
+++ b/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
@@ -5,6 +5,7 @@
 import android.app.backup.BackupRestoreEventLogger.BackupRestoreDataType
 import android.app.backup.BackupRestoreEventLogger.BackupRestoreError
 import android.content.Context
+import androidx.annotation.VisibleForTesting
 import com.android.launcher3.Flags.enableLauncherBrMetricsFixed
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger
@@ -29,8 +30,8 @@
         @BackupRestoreDataType private const val DATA_TYPE_APP_PAIR = "app_pair"
     }
 
-    private val restoreEventLogger: BackupRestoreEventLogger =
-        BackupManager(context).delayedRestoreLogger
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    val restoreEventLogger: BackupRestoreEventLogger = BackupManager(context).delayedRestoreLogger
 
     /**
      * For logging when multiple items of a given data type failed to restore.
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 3d4167a..95c86fa 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -20,13 +20,14 @@
 
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.quickstep.util.SplitScreenUtils.convertShellSplitBoundsToLauncher;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
 import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_FREEFORM;
 
 import android.app.ActivityManager;
 import android.app.KeyguardManager;
 import android.app.TaskInfo;
 import android.content.ComponentName;
+import android.content.Context;
 import android.os.Process;
 import android.os.RemoteException;
 import android.util.SparseBooleanArray;
@@ -58,6 +59,7 @@
 
     private static final TaskLoadResult INVALID_RESULT = new TaskLoadResult(-1, false, 0);
 
+    private final Context mContext;
     private final KeyguardManager mKeyguardManager;
     private final LooperExecutor mMainThreadExecutor;
     private final SystemUiProxy mSysUiProxy;
@@ -76,8 +78,10 @@
     // Tasks are stored in order of least recently launched to most recently launched.
     private ArrayList<ActivityManager.RunningTaskInfo> mRunningTasks;
 
-    public RecentTasksList(LooperExecutor mainThreadExecutor, KeyguardManager keyguardManager,
-            SystemUiProxy sysUiProxy, TopTaskTracker topTaskTracker) {
+    public RecentTasksList(Context context, LooperExecutor mainThreadExecutor,
+            KeyguardManager keyguardManager, SystemUiProxy sysUiProxy,
+            TopTaskTracker topTaskTracker) {
+        mContext = context;
         mMainThreadExecutor = mainThreadExecutor;
         mKeyguardManager = keyguardManager;
         mChangeId = 1;
@@ -300,8 +304,12 @@
     @VisibleForTesting
     TaskLoadResult loadTasksInBackground(int numTasks, int requestId, boolean loadKeysOnly) {
         int currentUserId = Process.myUserHandle().getIdentifier();
-        ArrayList<GroupedRecentTaskInfo> rawTasks =
-                mSysUiProxy.getRecentTasks(numTasks, currentUserId);
+        ArrayList<GroupedRecentTaskInfo> rawTasks;
+        try {
+            rawTasks = mSysUiProxy.getRecentTasks(numTasks, currentUserId);
+        } catch (SystemUiProxy.GetRecentTasksException e) {
+            return INVALID_RESULT;
+        }
         // The raw tasks are given in most-recent to least-recent order, we need to reverse it
         Collections.reverse(rawTasks);
 
@@ -321,9 +329,9 @@
         int numVisibleTasks = 0;
         for (GroupedRecentTaskInfo rawTask : rawTasks) {
             if (rawTask.getType() == TYPE_FREEFORM) {
-                // TYPE_FREEFORM tasks is only created when enableDesktopWindowingMode() is true,
+                // TYPE_FREEFORM tasks is only created whenDESKTOP_WINDOWING_MODE.isEnabled is true,
                 // leftover TYPE_FREEFORM tasks created when flag was on should be ignored.
-                if (enableDesktopWindowingMode()) {
+                if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)) {
                     GroupTask desktopTask = createDesktopTask(rawTask);
                     if (desktopTask != null) {
                         allTasks.add(desktopTask);
@@ -416,8 +424,12 @@
         }
         writer.println(prefix + "  ]");
         int currentUserId = Process.myUserHandle().getIdentifier();
-        ArrayList<GroupedRecentTaskInfo> rawTasks =
-                mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
+        ArrayList<GroupedRecentTaskInfo> rawTasks;
+        try {
+            rawTasks = mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
+        } catch (SystemUiProxy.GetRecentTasksException e) {
+            rawTasks = new ArrayList<>();
+        }
         writer.println(prefix + "  rawTasks=[");
         for (GroupedRecentTaskInfo task : rawTasks) {
             TaskInfo taskInfo1 = task.getTaskInfo1();
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 18461a6..e84200d 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -27,7 +27,7 @@
 import static com.android.quickstep.OverviewComponentObserver.startHomeIntentSafely;
 import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
 import static com.android.quickstep.TaskViewUtils.createRecentsWindowAnimator;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -147,7 +147,7 @@
         mActionsView = findViewById(R.id.overview_actions_view);
         getRootView().getSysUiScrim().getSysUIProgress().updateValue(0);
         mDragLayer.recreateControllers();
-        if (enableDesktopWindowingMode()) {
+        if (DESKTOP_WINDOWING_MODE.isEnabled(this)) {
             mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
                     getStateManager(), systemUiProxy, getIApplicationThread(),
                     null /* depthController */
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index cd62265..f902284 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -33,6 +33,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
@@ -416,7 +417,8 @@
                         | SYSUI_STATE_QUICK_SETTINGS_EXPANDED
                         | SYSUI_STATE_MAGNIFICATION_OVERLAP
                         | SYSUI_STATE_DEVICE_DREAMING
-                        | SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
+                        | SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION
+                        | SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING;
         return (gestureDisablingStates & mSystemUiStateFlags) == 0 && homeOrOverviewEnabled;
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
index 82bb453..d104911 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
@@ -18,9 +18,10 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
 
 import android.app.WindowConfiguration;
+import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.view.RemoteAnimationTarget;
@@ -54,8 +55,8 @@
      *
      * @return {@code true} if at least one target app is a desktop task
      */
-    public boolean hasDesktopTasks() {
-        if (!enableDesktopWindowingMode()) {
+    public boolean hasDesktopTasks(Context context) {
+        if (!DESKTOP_WINDOWING_MODE.isEnabled(context)) {
             return false;
         }
         for (RemoteAnimationTarget target : apps) {
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index b796951..db03dac 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -18,6 +18,7 @@
 import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
 
 import static com.android.launcher3.Flags.enableGridOnlyOverview;
+import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
 
@@ -89,7 +90,9 @@
 
     private RecentsModel(Context context, IconProvider iconProvider) {
         this(context,
-                new RecentTasksList(MAIN_EXECUTOR,
+                new RecentTasksList(
+                        context,
+                        MAIN_EXECUTOR,
                         context.getSystemService(KeyguardManager.class),
                         SystemUiProxy.INSTANCE.get(context),
                         TopTaskTracker.INSTANCE.get(context)),
@@ -108,7 +111,7 @@
         mIconCache = iconCache;
         mIconCache.registerTaskVisualsChangeListener(this);
         mThumbnailCache = thumbnailCache;
-        if (enableGridOnlyOverview()) {
+        if (isCachePreloadingEnabled()) {
             mCallbacks = new ComponentCallbacks() {
                 @Override
                 public void onConfigurationChanged(Configuration configuration) {
@@ -342,7 +345,7 @@
      * highResLoadingState is enabled
      */
     public void preloadCacheIfNeeded() {
-        if (!enableGridOnlyOverview()) {
+        if (!isCachePreloadingEnabled()) {
             return;
         }
 
@@ -368,7 +371,7 @@
      * Updates cache size and preloads more tasks if cache size increases
      */
     public void updateCacheSizeAndPreloadIfNeeded() {
-        if (!enableGridOnlyOverview()) {
+        if (!isCachePreloadingEnabled()) {
             return;
         }
 
@@ -387,6 +390,10 @@
         mTaskStackChangeListeners.unregisterTaskStackListener(this);
     }
 
+    private boolean isCachePreloadingEnabled() {
+        return enableGridOnlyOverview() || enableRefactorTaskThumbnail();
+    }
+
     /**
      * Listener for receiving running tasks changes
      */
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 66aa897..f2db5af 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -23,8 +23,8 @@
 import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENT_TASKS_MISSING;
 import static com.android.quickstep.util.LogUtils.splitFailureMessage;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
 import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
 
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
@@ -1394,10 +1394,26 @@
         }
     }
 
-    public ArrayList<GroupedRecentTaskInfo> getRecentTasks(int numTasks, int userId) {
+    public static class GetRecentTasksException extends Exception {
+        public GetRecentTasksException(String message) {
+            super(message);
+        }
+
+        public GetRecentTasksException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+
+    /**
+     * Retrieves a list of Recent tasks from ActivityManager.
+     * @throws GetRecentTasksException if IRecentTasks is not initialized, or when we get
+     * RemoteException from server side
+     */
+    public ArrayList<GroupedRecentTaskInfo> getRecentTasks(int numTasks, int userId)
+            throws GetRecentTasksException {
         if (mRecentTasks == null) {
-            Log.w(TAG, "getRecentTasks() failed due to null mRecentTasks");
-            return new ArrayList<>();
+            Log.e(TAG, "getRecentTasks() failed due to null mRecentTasks");
+            throw new GetRecentTasksException("null mRecentTasks");
         }
         try {
             final GroupedRecentTaskInfo[] rawTasks = mRecentTasks.getRecentTasks(numTasks,
@@ -1407,8 +1423,8 @@
             }
             return new ArrayList<>(Arrays.asList(rawTasks));
         } catch (RemoteException e) {
-            Log.w(TAG, "Failed call getRecentTasks", e);
-            return new ArrayList<>();
+            Log.e(TAG, "Failed call getRecentTasks", e);
+            throw new GetRecentTasksException("Failed call getRecentTasks", e);
         }
     }
 
@@ -1428,7 +1444,8 @@
 
     private boolean shouldEnableRunningTasksForDesktopMode() {
         // TODO(b/335401172): unify DesktopMode checks in Launcher
-        return enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps();
+        return DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+                && enableDesktopWindowingTaskbarRunningApps();
     }
 
     private boolean handleMessageAsync(Message msg) {
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 08bb6cd..723aa03 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -332,7 +332,8 @@
 
         if (ENABLE_SHELL_TRANSITIONS) {
             final ActivityOptions options = ActivityOptions.makeBasic();
-            options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
+            options.setPendingIntentBackgroundActivityStartMode(
+                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
             // Use regular (non-transient) launch for all apps page to control IME.
             if (!containerInterface.allowAllAppsFromOverview()) {
                 options.setTransientLaunch();
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index 80902e3..f4ff4b2 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -184,6 +184,10 @@
             return mTaskContainer.getTaskView();
         }
 
+        public View getSnapshotView() {
+            return mTaskContainer.getSnapshotView();
+        }
+
         /**
          * Called when the current task is interactive for the user
          */
@@ -312,7 +316,7 @@
             // the difference between the bitmap bounds and the projected view bounds.
             Matrix boundsToBitmapSpace = new Matrix();
             Matrix thumbnailMatrix = enableRefactorTaskThumbnail()
-                    ? mHelper.getEnabledState().getThumbnailMatrix()
+                    ? mHelper.getThumbnailMatrix()
                     : mTaskContainer.getThumbnailViewDeprecated().getThumbnailMatrix();
             thumbnailMatrix.invert(boundsToBitmapSpace);
             RectF boundsInBitmapSpace = new RectF();
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 77124bf..dad34ac 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -23,7 +23,6 @@
 import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
 
 import android.app.ActivityOptions;
 import android.graphics.Bitmap;
@@ -63,6 +62,7 @@
 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
 import com.android.systemui.shared.recents.view.RecentsTransition;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 import java.util.Collections;
 import java.util.List;
@@ -394,7 +394,7 @@
             return Settings.Global.getInt(
                     container.asContext().getContentResolver(),
                     Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0
-                    && !enableDesktopWindowingMode();
+                    && !DesktopModeStatus.canEnterDesktopMode(container.asContext());
         }
     };
 
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
index 848a43a..f4d3695 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -27,6 +27,8 @@
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.util.DisplayController;
@@ -48,7 +50,7 @@
     private static final boolean DEBUG_NAV_HANDLE = Utilities.isPropertyEnabled(
             NAV_HANDLE_LONG_PRESS);
 
-    private final NavHandleLongPressHandler mNavHandleLongPressHandler;
+    private NavHandleLongPressHandler mNavHandleLongPressHandler;
     private final float mNavHandleWidth;
     private final float mScreenWidth;
 
@@ -73,17 +75,32 @@
         super(delegate, inputMonitor);
         mScreenWidth = DisplayController.INSTANCE.get(context).getInfo().currentSize.x;
         mDeepPressEnabled = DeviceConfigWrapper.get().getEnableLpnhDeepPress();
-        int twoStageMultiplier = DeviceConfigWrapper.get().getTwoStageMultiplier();
         AssistStateManager assistStateManager = AssistStateManager.INSTANCE.get(context);
         if (assistStateManager.getLPNHDurationMillis().isPresent()) {
             mLongPressTimeout = assistStateManager.getLPNHDurationMillis().get().intValue();
         } else {
             mLongPressTimeout = ViewConfiguration.getLongPressTimeout();
         }
-        mOuterLongPressTimeout = mLongPressTimeout * twoStageMultiplier;
-        mTouchSlopSquaredOriginal = deviceState.getSquaredTouchSlop();
-        mTouchSlopSquared = mTouchSlopSquaredOriginal;
-        mOuterTouchSlopSquared = mTouchSlopSquared * (twoStageMultiplier * twoStageMultiplier);
+        float twoStageDurationMultiplier =
+                (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
+        mOuterLongPressTimeout = (int) (mLongPressTimeout * twoStageDurationMultiplier);
+
+        float gestureNavTouchSlopSquared = deviceState.getSquaredTouchSlop();
+        float twoStageSlopMultiplier =
+                (DeviceConfigWrapper.get().getTwoStageSlopPercentage() / 100f);
+        float twoStageSlopMultiplierSquared = twoStageSlopMultiplier * twoStageSlopMultiplier;
+        if (DeviceConfigWrapper.get().getEnableLpnhTwoStages()) {
+            // For 2 stages, the outer touch slop should match gesture nav.
+            mTouchSlopSquared = gestureNavTouchSlopSquared * twoStageSlopMultiplierSquared;
+            mOuterTouchSlopSquared = gestureNavTouchSlopSquared;
+        } else {
+            // For single stage, the touch slop should match gesture nav.
+            mTouchSlopSquared = gestureNavTouchSlopSquared;
+            // Note: This outer slop is not actually used for single-stage (flag disabled).
+            mOuterTouchSlopSquared = gestureNavTouchSlopSquared;
+        }
+        mTouchSlopSquaredOriginal = mTouchSlopSquared;
+
         mGestureState = gestureState;
         mGestureState.setIsInExtendedSlopRegion(false);
         if (DEBUG_NAV_HANDLE) {
@@ -250,4 +267,9 @@
     protected String getDelegatorName() {
         return "NavHandleLongPressInputConsumer";
     }
+
+    @VisibleForTesting
+    void setNavHandleLongPressHandler(NavHandleLongPressHandler navHandleLongPressHandler) {
+        mNavHandleLongPressHandler = navHandleLongPressHandler;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 13b6447..69d3bc9 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -409,6 +409,7 @@
         mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs);
         mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished);
         mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler.getMotionPauseListener());
+        mMotionPauseDetector.setIsTrackpadGesture(mGestureState.isTrackpadGesture());
         mInteractionHandler.initWhenReady(
                 "OtherActivityInputConsumer.startTouchTrackingForWindowAnimation");
 
diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
index 5ac04da..717f6c8 100644
--- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
+++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
@@ -39,7 +39,8 @@
 import android.util.Log;
 import android.util.Xml;
 
-import com.android.launcher3.AutoInstallsLayout;
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
 import com.android.launcher3.logging.InstanceId;
@@ -73,7 +74,6 @@
             new MainThreadInitializedObject<>(SettingsChangeLogger::new);
 
     private static final String TAG = "SettingsChangeLogger";
-    private static final String ROOT_TAG = "androidx.preference.PreferenceScreen";
     private static final String BOOLEAN_PREF = "SwitchPreference";
 
     private final Context mContext;
@@ -85,8 +85,13 @@
     private StatsLogManager.LauncherEvent mHomeScreenSuggestionEvent;
 
     private SettingsChangeLogger(Context context) {
+        this(context, StatsLogManager.newInstance(context));
+    }
+
+    @VisibleForTesting
+    SettingsChangeLogger(Context context, StatsLogManager statsLogManager) {
         mContext = context;
-        mStatsLogManager = StatsLogManager.newInstance(mContext);
+        mStatsLogManager = statsLogManager;
         mLoggablePrefs = loadPrefKeys(context);
         DisplayController.INSTANCE.get(context).addChangeListener(this);
         mNavMode = DisplayController.getNavigationMode(context);
@@ -105,7 +110,13 @@
         ArrayMap<String, LoggablePref> result = new ArrayMap<>();
 
         try {
-            AutoInstallsLayout.beginDocument(parser, ROOT_TAG);
+            // Move cursor to first tag because it could be
+            // androidx.preference.PreferenceScreen or PreferenceScreen
+            int eventType = parser.getEventType();
+            while (eventType != XmlPullParser.START_TAG
+                    && eventType != XmlPullParser.END_DOCUMENT) {
+                eventType = parser.next();
+            }
             final int depth = parser.getDepth();
             int type;
             while (((type = parser.next()) != XmlPullParser.END_TAG
@@ -189,13 +200,19 @@
                 prefs.getBoolean(key, lp.defaultValue) ? lp.eventIdOn : lp.eventIdOff));
     }
 
+    @VisibleForTesting
+    ArrayMap<String, LoggablePref> getLoggingPrefs() {
+        return mLoggablePrefs;
+    }
+
     @Override
     public void close() {
         getPrefs(mContext).unregisterOnSharedPreferenceChangeListener(this);
         getDevicePrefs(mContext).unregisterOnSharedPreferenceChangeListener(this);
     }
 
-    private static class LoggablePref {
+    @VisibleForTesting
+    static class LoggablePref {
         public boolean defaultValue;
         public int eventIdOn;
         public int eventIdOff;
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
new file mode 100644
index 0000000..feed2fd
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+/**
+ * Container to hold [com.android.launcher3.DeviceProfile] related to Recents.
+ *
+ * @property isLargeScreen whether the current device posture has a large screen
+ */
+data class RecentsDeviceProfile(
+    val isLargeScreen: Boolean,
+    val widthPx: Int,
+    val heightPx: Int,
+)
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt
new file mode 100644
index 0000000..13cf56d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+interface RecentsDeviceProfileRepository {
+    fun getRecentsDeviceProfile(): RecentsDeviceProfile
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
new file mode 100644
index 0000000..ce39ff1
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.views.RecentsViewContainer
+
+/**
+ * Repository for shrink down version of [com.android.launcher3.DeviceProfile] that only contains
+ * data related to Recents.
+ */
+class RecentsDeviceProfileRepositoryImpl(private val container: RecentsViewContainer) :
+    RecentsDeviceProfileRepository {
+
+    override fun getRecentsDeviceProfile() =
+        with(container.deviceProfile) {
+            RecentsDeviceProfile(isLargeScreen = isTablet, widthPx = widthPx, heightPx = heightPx)
+        }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt
new file mode 100644
index 0000000..2c2a744
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import android.view.Surface
+
+/**
+ * Container to hold orientation/rotation related information related to Recents.
+ *
+ * @property activityRotation rotation of the activity hosting RecentsView
+ */
+data class RecentsRotationState(
+    @Surface.Rotation val activityRotation: Int,
+    @Surface.Rotation val orientationHandlerRotation: Int,
+)
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt
new file mode 100644
index 0000000..ed074d2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+interface RecentsRotationStateRepository {
+    fun getRecentsRotationState(): RecentsRotationState
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.kt
new file mode 100644
index 0000000..8417b06
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.util.RecentsOrientedState
+
+/**
+ * Repository for [RecentsRotationState] which holds orientation/rotation related information
+ * related to Recents
+ */
+class RecentsRotationStateRepositoryImpl(private val state: RecentsOrientedState) :
+    RecentsRotationStateRepository {
+    override fun getRecentsRotationState() =
+        with(state) {
+            RecentsRotationState(
+                activityRotation = recentsActivityRotation,
+                orientationHandlerRotation = orientationHandler.rotation
+            )
+        }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index f73db5a..71be75b 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -130,9 +130,15 @@
                                 icon,
                                 contentDescription,
                                 title ->
-                                continuation.resume(
-                                    TaskIconQueryResponse(icon, contentDescription, title)
-                                )
+                                icon.constantState?.let {
+                                    continuation.resume(
+                                        TaskIconQueryResponse(
+                                            it.newDrawable().mutate(),
+                                            contentDescription,
+                                            title
+                                        )
+                                    )
+                                }
                             }
                         continuation.invokeOnCancellation { cancellableTask?.cancel() }
                     }
@@ -157,7 +163,7 @@
     }
 }
 
-private data class TaskIconQueryResponse(
+data class TaskIconQueryResponse(
     val icon: Drawable,
     val contentDescription: String,
     val title: String
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
new file mode 100644
index 0000000..eba7688
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.di
+
+import android.content.Context
+import android.util.Log
+import android.view.View
+import com.android.quickstep.RecentsModel
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.data.TasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.GetThumbnailUseCase
+import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.GetSplashSizeUseCase
+import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
+import com.android.quickstep.task.thumbnail.TaskThumbnailViewData
+import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
+import com.android.quickstep.task.viewmodel.TaskViewData
+import com.android.quickstep.task.viewmodel.TaskViewModel
+import com.android.quickstep.views.TaskViewType
+import com.android.systemui.shared.recents.model.Task
+
+internal typealias RecentsScopeId = String
+
+class RecentsDependencies private constructor(private val appContext: Context) {
+    private val scopes = mutableMapOf<RecentsScopeId, RecentsDependenciesScope>()
+
+    init {
+        startDefaultScope(appContext)
+    }
+
+    /**
+     * This function initialised the default scope with RecentsView dependencies. These dependencies
+     * are used multiple times and should be a singleton to share across Recents classes.
+     */
+    private fun startDefaultScope(appContext: Context) {
+        createScope(DEFAULT_SCOPE_ID).apply {
+            set(RecentsViewData::class.java.simpleName, RecentsViewData())
+
+            // Create RecentsTaskRepository singleton
+            val recentTasksRepository: RecentTasksRepository =
+                with(RecentsModel.INSTANCE.get(appContext)) {
+                    TasksRepository(this, thumbnailCache, iconCache)
+                }
+            set(RecentTasksRepository::class.java.simpleName, recentTasksRepository)
+        }
+    }
+
+    inline fun <reified T> inject(
+        scopeId: RecentsScopeId = "",
+        extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
+        noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+    ): T = inject(T::class.java, scopeId = scopeId, extras = extras, factory = factory)
+
+    @Suppress("UNCHECKED_CAST")
+    @JvmOverloads
+    fun <T> inject(
+        modelClass: Class<T>,
+        scopeId: RecentsScopeId = DEFAULT_SCOPE_ID,
+        extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
+        factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+    ): T {
+        val currentScopeId = scopeId.ifEmpty { DEFAULT_SCOPE_ID }
+        val scope = scopes[currentScopeId] ?: createScope(currentScopeId)
+
+        log("inject ${modelClass.simpleName} into ${scope.scopeId}", Log.INFO)
+        var instance: T?
+        synchronized(this) {
+            instance = getDependency(scope, modelClass)
+            log("found instance? $instance", Log.INFO)
+            if (instance == null) {
+                instance =
+                    factory?.invoke(extras) as T ?: createDependency(modelClass, scopeId, extras)
+                scope[modelClass.simpleName] = instance!!
+            }
+        }
+        return instance!!
+    }
+
+    inline fun <reified T> provide(scopeId: RecentsScopeId = "", noinline factory: () -> T): T =
+        provide(T::class.java, scopeId = scopeId, factory = factory)
+
+    @JvmOverloads
+    fun <T> provide(
+        modelClass: Class<T>,
+        scopeId: RecentsScopeId = DEFAULT_SCOPE_ID,
+        factory: () -> T,
+    ) = inject(modelClass, scopeId, factory = { factory.invoke() })
+
+    private fun <T> getDependency(scope: RecentsDependenciesScope, modelClass: Class<T>): T? {
+        var instance: T? = scope[modelClass.simpleName] as T?
+        if (instance == null) {
+            instance =
+                scope.scopeIdsLinked.firstNotNullOfOrNull { scopeId ->
+                    getScope(scopeId)[modelClass.simpleName]
+                } as T?
+        }
+        if (instance != null) log("Found dependency: $instance", Log.INFO)
+        return instance
+    }
+
+    fun getScope(scope: Any): RecentsDependenciesScope {
+        val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
+        return getScope(scopeId)
+    }
+
+    fun getScope(scopeId: RecentsScopeId): RecentsDependenciesScope =
+        scopes[scopeId] ?: createScope(scopeId)
+
+    // TODO(b/353912757): Create a factory so we can prevent this method of growing indefinitely.
+    //  Each class should be responsible for providing a factory function to create a new instance.
+    @Suppress("UNCHECKED_CAST")
+    private fun <T> createDependency(
+        modelClass: Class<T>,
+        scopeId: RecentsScopeId,
+        extras: RecentsDependenciesExtras,
+    ): T {
+        log("createDependency ${modelClass.simpleName} with $scopeId and $extras", Log.WARN)
+        val instance: Any =
+            when (modelClass) {
+                RecentTasksRepository::class.java -> {
+                    with(RecentsModel.INSTANCE.get(appContext)) {
+                        TasksRepository(this, thumbnailCache, iconCache)
+                    }
+                }
+                RecentsViewData::class.java -> RecentsViewData()
+                TaskViewModel::class.java -> TaskViewModel(taskViewData = inject(scopeId, extras))
+                TaskViewData::class.java -> {
+                    val taskViewType = extras["TaskViewType"] as TaskViewType
+                    TaskViewData(taskViewType)
+                }
+                TaskContainerData::class.java -> TaskContainerData()
+                TaskThumbnailViewData::class.java -> TaskThumbnailViewData()
+                TaskThumbnailViewModel::class.java ->
+                    TaskThumbnailViewModel(
+                        recentsViewData = inject(),
+                        taskViewData = inject(scopeId, extras),
+                        taskContainerData = inject(scopeId),
+                        getThumbnailPositionUseCase = inject(),
+                        tasksRepository = inject(),
+                        splashAlphaUseCase = inject(scopeId),
+                        getSplashSizeUseCase = inject(scopeId),
+                    )
+                TaskOverlayViewModel::class.java -> {
+                    val task = extras["Task"] as Task
+                    TaskOverlayViewModel(
+                        task = task,
+                        recentsViewData = inject(),
+                        recentTasksRepository = inject(),
+                        getThumbnailPositionUseCase = inject()
+                    )
+                }
+                GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
+                SysUiStatusNavFlagsUseCase::class.java ->
+                    SysUiStatusNavFlagsUseCase(taskRepository = inject())
+                GetThumbnailPositionUseCase::class.java ->
+                    GetThumbnailPositionUseCase(
+                        deviceProfileRepository = inject(),
+                        rotationStateRepository = inject(),
+                        tasksRepository = inject()
+                    )
+                SplashAlphaUseCase::class.java ->
+                    SplashAlphaUseCase(
+                        recentsViewData = inject(),
+                        taskContainerData = inject(scopeId),
+                        taskThumbnailViewData = inject(scopeId),
+                        tasksRepository = inject(),
+                        rotationStateRepository = inject(),
+                    )
+                GetSplashSizeUseCase::class.java ->
+                    GetSplashSizeUseCase(
+                        taskThumbnailViewData = inject(scopeId),
+                        taskViewData = inject(scopeId, extras),
+                        deviceProfileRepository = inject(),
+                    )
+                else -> {
+                    log("Factory for ${modelClass.simpleName} not defined!", Log.ERROR)
+                    error("Factory for ${modelClass.simpleName} not defined!")
+                }
+            }
+        return instance as T
+    }
+
+    private fun createScope(scopeId: RecentsScopeId): RecentsDependenciesScope {
+        return RecentsDependenciesScope(scopeId).also { scopes[scopeId] = it }
+    }
+
+    private fun log(message: String, @Log.Level level: Int = Log.DEBUG) {
+        if (DEBUG) {
+            when (level) {
+                Log.WARN -> Log.w(TAG, message)
+                Log.VERBOSE -> Log.v(TAG, message)
+                Log.INFO -> Log.i(TAG, message)
+                Log.ERROR -> Log.e(TAG, message)
+                else -> Log.d(TAG, message)
+            }
+        }
+    }
+
+    companion object {
+        private const val DEFAULT_SCOPE_ID = "RecentsDependencies::GlobalScope"
+        private const val TAG = "RecentsDependencies"
+        private const val DEBUG = false
+
+        @Volatile private lateinit var instance: RecentsDependencies
+
+        fun initialize(view: View): RecentsDependencies = initialize(view.context)
+
+        fun initialize(context: Context): RecentsDependencies {
+            synchronized(this) {
+                if (!Companion::instance.isInitialized) {
+                    instance = RecentsDependencies(context.applicationContext)
+                }
+            }
+            return instance
+        }
+
+        fun getInstance(): RecentsDependencies {
+            if (!Companion::instance.isInitialized) {
+                throw UninitializedPropertyAccessException(
+                    "Recents dependencies are not initialized. " +
+                        "Call `RecentsDependencies.initialize` before using this container."
+                )
+            }
+            return instance
+        }
+
+        fun destroy() {
+            instance.scopes.clear()
+            instance.startDefaultScope(instance.appContext)
+        }
+    }
+}
+
+inline fun <reified T> RecentsDependencies.Companion.inject(
+    scope: Any = "",
+    vararg extras: Pair<String, Any>,
+    noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+): Lazy<T> = lazy { get(scope, RecentsDependenciesExtras(extras), factory) }
+
+inline fun <reified T> RecentsDependencies.Companion.get(
+    scope: Any = "",
+    extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
+    noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+): T {
+    val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
+    return getInstance().inject(scopeId, extras, factory)
+}
+
+inline fun <reified T> RecentsDependencies.Companion.get(
+    scope: Any = "",
+    vararg extras: Pair<String, Any>,
+    noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+): T = get(scope, RecentsDependenciesExtras(extras), factory)
+
+fun RecentsDependencies.Companion.getScope(scopeId: Any) = getInstance().getScope(scopeId)
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt
new file mode 100644
index 0000000..753cb6e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.di
+
+data class RecentsDependenciesExtras(private val data: MutableMap<String, Any> = mutableMapOf()) {
+    constructor(value: Array<out Pair<String, Any>>) : this(value.toMap().toMutableMap())
+
+    operator fun get(key: String) = data[key]
+
+    operator fun set(key: String, value: Any) {
+        data[key] = value
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt
new file mode 100644
index 0000000..56bb1ed
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.di
+
+import android.util.Log
+
+class RecentsDependenciesScope(
+    val scopeId: RecentsScopeId,
+    private val dependencies: MutableMap<String, Any> = mutableMapOf(),
+    private val scopeIds: MutableList<RecentsScopeId> = mutableListOf()
+) {
+    val scopeIdsLinked: List<RecentsScopeId>
+        get() = scopeIds.toList()
+
+    operator fun get(identifier: String): Any? {
+        log("get $identifier")
+        return dependencies[identifier]
+    }
+
+    operator fun set(key: String, value: Any) {
+        synchronized(this) {
+            log("set $key")
+            dependencies[key] = value
+        }
+    }
+
+    fun remove(key: String): Any? {
+        synchronized(this) {
+            log("remove $key")
+            return dependencies.remove(key)
+        }
+    }
+
+    fun linkTo(scope: RecentsDependenciesScope) {
+        log("linking to ${scope.scopeId}")
+        scopeIds += scope.scopeId
+    }
+
+    fun close() {
+        log("reset")
+        synchronized(this) { dependencies.clear() }
+    }
+
+    private fun log(message: String) {
+        if (DEBUG) Log.d(TAG, "[scopeId=$scopeId] $message")
+    }
+
+    override fun toString(): String =
+        "scopeId: $scopeId" +
+            "\n dependencies: ${dependencies.map { "${it.key}=${it.value}" }.joinToString(", ")}" +
+            "\n linked to: ${scopeIds.joinToString(", ")}"
+
+    private companion object {
+        private const val TAG = "RecentsDependenciesScope"
+        private const val DEBUG = false
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt
new file mode 100644
index 0000000..f060d7d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.usecase
+
+import android.graphics.Matrix
+import android.graphics.Rect
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
+import com.android.quickstep.recents.data.RecentsRotationStateRepository
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import kotlinx.coroutines.flow.firstOrNull
+
+/** Use case for retrieving [Matrix] for positioning Thumbnail in a View */
+class GetThumbnailPositionUseCase(
+    private val deviceProfileRepository: RecentsDeviceProfileRepository,
+    private val rotationStateRepository: RecentsRotationStateRepository,
+    private val tasksRepository: RecentTasksRepository,
+    private val previewPositionHelper: PreviewPositionHelper = PreviewPositionHelper()
+) {
+    suspend fun run(taskId: Int, width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
+        val thumbnailData =
+            tasksRepository.getThumbnailById(taskId).firstOrNull() ?: return MissingThumbnail
+        val thumbnail = thumbnailData.thumbnail ?: return MissingThumbnail
+        previewPositionHelper.updateThumbnailMatrix(
+            Rect(0, 0, thumbnail.width, thumbnail.height),
+            thumbnailData,
+            width,
+            height,
+            deviceProfileRepository.getRecentsDeviceProfile().isLargeScreen,
+            rotationStateRepository.getRecentsRotationState().activityRotation,
+            isRtl
+        )
+        return MatrixScaling(
+            previewPositionHelper.matrix,
+            previewPositionHelper.isOrientationChanged
+        )
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt
similarity index 95%
rename from quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt
rename to quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt
index e8dd04c3..3aa808e 100644
--- a/quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt
+++ b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.quickstep.task.util
+package com.android.quickstep.recents.usecase
 
 import android.graphics.Bitmap
 import com.android.quickstep.recents.data.RecentTasksRepository
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt
new file mode 100644
index 0000000..1d19c7d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.usecase
+
+import android.view.WindowInsetsController
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_STATUS
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS
+import com.android.quickstep.recents.data.RecentTasksRepository
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+
+/** UseCase to calculate flags for status bar and navigation bar */
+class SysUiStatusNavFlagsUseCase(private val taskRepository: RecentTasksRepository) {
+    fun getSysUiStatusNavFlags(taskId: Int): Int {
+        val thumbnailData =
+            runBlocking { taskRepository.getThumbnailById(taskId).firstOrNull() } ?: return 0
+
+        val thumbnailAppearance = thumbnailData.appearance
+        var flags = 0
+        flags =
+            flags or
+                if (
+                    thumbnailAppearance and WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS != 0
+                )
+                    FLAG_LIGHT_STATUS
+                else FLAG_DARK_STATUS
+        flags =
+            flags or
+                if (
+                    thumbnailAppearance and
+                        WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS != 0
+                )
+                    FLAG_LIGHT_NAV
+                else FLAG_DARK_NAV
+        return flags
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt b/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt
new file mode 100644
index 0000000..1a1bef7
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.usecase
+
+import android.graphics.Matrix
+
+/** State on how a task Thumbnail can fit on given canvas */
+sealed class ThumbnailPositionState {
+    data object MissingThumbnail : ThumbnailPositionState()
+
+    data class MatrixScaling(val matrix: Matrix, val isRotated: Boolean) : ThumbnailPositionState()
+}
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
index fdb62df..f5e0243 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
@@ -31,4 +31,9 @@
 
     // The settled set of visible taskIds that is updated after RecentsView scroll settles.
     val settledFullyVisibleTaskIds = MutableStateFlow(emptySet<Int>())
+
+    // Color tint on foreground scrim
+    val tintAmount = MutableStateFlow(0f)
+
+    val thumbnailSplashProgress = MutableStateFlow(0f)
 }
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
new file mode 100644
index 0000000..6148d4b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.viewmodel
+
+import com.android.quickstep.recents.data.RecentTasksRepository
+
+class RecentsViewModel(
+    private val recentsTasksRepository: RecentTasksRepository,
+    private val recentsViewData: RecentsViewData
+) {
+    fun refreshAllTaskData() {
+        recentsTasksRepository.getAllTaskData(true)
+    }
+
+    fun updateVisibleTasks(visibleTaskIdList: List<Int>) {
+        recentsTasksRepository.setVisibleTasks(visibleTaskIdList)
+    }
+
+    fun updateScale(scale: Float) {
+        recentsViewData.scale.value = scale
+    }
+
+    fun updateFullscreenProgress(fullscreenProgress: Float) {
+        recentsViewData.fullscreenProgress.value = fullscreenProgress
+    }
+
+    fun updateTasksFullyVisible(taskIds: Set<Int>) {
+        recentsViewData.settledFullyVisibleTaskIds.value = taskIds
+    }
+
+    fun setOverlayEnabled(isOverlayEnabled: Boolean) {
+        recentsViewData.overlayEnabled.value = isOverlayEnabled
+    }
+
+    fun setTintAmount(tintAmount: Float) {
+        recentsViewData.tintAmount.value = tintAmount
+    }
+
+    fun updateThumbnailSplashProgress(taskThumbnailSplashAlpha: Float) {
+        recentsViewData.thumbnailSplashProgress.value = taskThumbnailSplashAlpha
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
new file mode 100644
index 0000000..168c1e0
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.viewmodel
+
+import android.graphics.Bitmap
+import com.android.quickstep.recents.usecase.GetThumbnailUseCase
+import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
+import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+
+class TaskContainerViewModel(
+    private val sysUiStatusNavFlagsUseCase: SysUiStatusNavFlagsUseCase,
+    private val getThumbnailUseCase: GetThumbnailUseCase,
+    private val splashAlphaUseCase: SplashAlphaUseCase,
+) {
+    fun getThumbnail(taskId: Int): Bitmap? = getThumbnailUseCase.run(taskId)
+
+    fun getSysUiStatusNavFlags(taskId: Int) =
+        sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(taskId)
+
+    fun shouldShowThumbnailSplash(taskId: Int): Boolean =
+        (runBlocking { splashAlphaUseCase.execute(taskId).firstOrNull() } ?: 0f) > 0f
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCase.kt b/quickstep/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCase.kt
new file mode 100644
index 0000000..145957a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCase.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.graphics.Point
+import android.graphics.drawable.Drawable
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
+import com.android.quickstep.task.viewmodel.TaskViewData
+import kotlin.math.min
+
+class GetSplashSizeUseCase(
+    private val taskThumbnailViewData: TaskThumbnailViewData,
+    private val taskViewData: TaskViewData,
+    private val deviceProfileRepository: RecentsDeviceProfileRepository,
+) {
+    fun execute(splashImage: Drawable): Point {
+        val recentsDeviceProfile = deviceProfileRepository.getRecentsDeviceProfile()
+        val screenWidth = recentsDeviceProfile.widthPx
+        val screenHeight = recentsDeviceProfile.heightPx
+        val scaleAtFullscreen =
+            min(
+                screenWidth / taskThumbnailViewData.width.value,
+                screenHeight / taskThumbnailViewData.height.value,
+            )
+        val scaleFactor: Float = 1f / taskViewData.nonGridScale.value / scaleAtFullscreen
+        return Point(
+            (splashImage.intrinsicWidth * scaleFactor / taskThumbnailViewData.scaleX.value).toInt(),
+            (splashImage.intrinsicHeight * scaleFactor / taskThumbnailViewData.scaleY.value)
+                .toInt(),
+        )
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt b/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt
new file mode 100644
index 0000000..e5618fc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.graphics.Bitmap
+import android.view.Surface
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.data.RecentsRotationStateRepository
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import com.android.systemui.shared.recents.utilities.Utilities
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+class SplashAlphaUseCase(
+    private val recentsViewData: RecentsViewData,
+    private val taskContainerData: TaskContainerData,
+    private val taskThumbnailViewData: TaskThumbnailViewData,
+    private val tasksRepository: RecentTasksRepository,
+    private val rotationStateRepository: RecentsRotationStateRepository,
+) {
+    fun execute(taskId: Int): Flow<Float> =
+        combine(
+                taskThumbnailViewData.width,
+                taskThumbnailViewData.height,
+                tasksRepository.getThumbnailById(taskId),
+                taskContainerData.thumbnailSplashProgress,
+                recentsViewData.thumbnailSplashProgress
+            ) { width, height, thumbnailData, taskSplashProgress, globalSplashProgress ->
+                val thumbnail = thumbnailData?.thumbnail
+                when {
+                    thumbnail == null -> 1f
+                    taskSplashProgress > 0f -> taskSplashProgress
+                    globalSplashProgress > 0f &&
+                        isInaccurateThumbnail(thumbnail, thumbnailData.rotation, width, height) ->
+                        globalSplashProgress
+                    else -> 0f
+                }
+            }
+            .distinctUntilChanged()
+
+    private fun isInaccurateThumbnail(
+        thumbnail: Bitmap,
+        thumbnailRotation: Int,
+        width: Int,
+        height: Int
+    ): Boolean {
+        return isThumbnailAspectRatioDifferentFromThumbnailData(thumbnail, width, height) ||
+            isThumbnailRotationDifferentFromTask(thumbnailRotation)
+    }
+
+    private fun isThumbnailAspectRatioDifferentFromThumbnailData(
+        thumbnail: Bitmap,
+        viewWidth: Int,
+        viewHeight: Int
+    ): Boolean {
+        val viewAspect: Float = viewWidth / viewHeight.toFloat()
+        val thumbnailAspect: Float = thumbnail.width / thumbnail.height.toFloat()
+        return Utilities.isRelativePercentDifferenceGreaterThan(
+            viewAspect,
+            thumbnailAspect,
+            PreviewPositionHelper.MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT
+        )
+    }
+
+    private fun isThumbnailRotationDifferentFromTask(thumbnailRotation: Int): Boolean {
+        val rotationState = rotationStateRepository.getRecentsRotationState()
+        return if (rotationState.orientationHandlerRotation == Surface.ROTATION_0) {
+            (rotationState.activityRotation - thumbnailRotation) % 2 != 0
+        } else {
+            rotationState.orientationHandlerRotation != thumbnailRotation
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
index feee11f..5fb5b90 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
@@ -17,15 +17,10 @@
 package com.android.quickstep.task.thumbnail
 
 import android.graphics.Bitmap
-import android.graphics.Matrix
 
 /** Ui state for [com.android.quickstep.TaskOverlayFactory.TaskOverlay] */
 sealed class TaskOverlayUiState {
     data object Disabled : TaskOverlayUiState()
 
-    data class Enabled(
-        val isRealSnapshot: Boolean,
-        val thumbnail: Bitmap?,
-        val thumbnailMatrix: Matrix
-    ) : TaskOverlayUiState()
+    data class Enabled(val isRealSnapshot: Boolean, val thumbnail: Bitmap?) : TaskOverlayUiState()
 }
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
index 40f9b28..aa7d26c 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
@@ -17,18 +17,33 @@
 package com.android.quickstep.task.thumbnail
 
 import android.graphics.Bitmap
-import android.graphics.Rect
+import android.graphics.Point
+import android.graphics.drawable.Drawable
+import android.view.Surface
 import androidx.annotation.ColorInt
 
 sealed class TaskThumbnailUiState {
     data object Uninitialized : TaskThumbnailUiState()
+
     data object LiveTile : TaskThumbnailUiState()
+
     data class BackgroundOnly(@ColorInt val backgroundColor: Int) : TaskThumbnailUiState()
+
+    data class SnapshotSplash(
+        val snapshot: Snapshot,
+        val splash: Splash,
+    ) : TaskThumbnailUiState()
+
     data class Snapshot(
         val bitmap: Bitmap,
-        val drawnRect: Rect,
+        @Surface.Rotation val thumbnailRotation: Int,
         @ColorInt val backgroundColor: Int
-    ) : TaskThumbnailUiState()
+    )
+
+    data class Splash(
+        val icon: Drawable?,
+        val size: Point,
+    )
 }
 
 data class TaskThumbnail(val taskId: Int, val isRunning: Boolean)
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index d22fc94..41aee52 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -28,18 +28,21 @@
 import android.widget.ImageView
 import androidx.annotation.ColorInt
 import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
 import com.android.launcher3.util.ViewPool
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.inject
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
 import com.android.quickstep.util.TaskCornerRadius
-import com.android.quickstep.views.RecentsView
-import com.android.quickstep.views.RecentsViewContainer
-import com.android.quickstep.views.TaskView
 import com.android.systemui.shared.system.QuickStepContract
+import kotlin.math.abs
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -49,26 +52,19 @@
 import kotlinx.coroutines.flow.onEach
 
 class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
-    // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
-    //  to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
-    //  This is using a lazy for now because the dependencies cannot be obtained without DI.
-    val viewModel by lazy {
-        val recentsView =
-            RecentsViewContainer.containerFromContext<RecentsViewContainer>(context)
-                .getOverviewPanel<RecentsView<*, *>>()
-        TaskThumbnailViewModel(
-            recentsView.mRecentsViewData!!,
-            (parent as TaskView).taskViewData,
-            (parent as TaskView).getTaskContainerForTaskThumbnailView(this)!!.taskContainerData,
-            recentsView.mTasksRepository!!,
-        )
-    }
+
+    private val viewData: TaskThumbnailViewData by RecentsDependencies.inject(this)
+    private val viewModel: TaskThumbnailViewModel by RecentsDependencies.inject(this)
+
     private lateinit var viewAttachedScope: CoroutineScope
 
     private val scrimView: View by lazy { findViewById(R.id.task_thumbnail_scrim) }
     private val liveTileView: LiveTileView by lazy { findViewById(R.id.task_thumbnail_live_tile) }
-    private val thumbnail: ImageView by lazy { findViewById(R.id.task_thumbnail) }
+    private val thumbnailView: ImageView by lazy { findViewById(R.id.task_thumbnail) }
+    private val splashContainer: FrameLayout by lazy { findViewById(R.id.splash_container) }
+    private val splashIcon: ImageView by lazy { findViewById(R.id.splash_icon) }
 
+    private var uiState: TaskThumbnailUiState = Uninitialized
     private var inheritedScale: Float = 1f
 
     private val _measuredBounds = Rect()
@@ -97,20 +93,21 @@
             CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskThumbnailView"))
         viewModel.uiState
             .onEach { viewModelUiState ->
+                uiState = viewModelUiState
                 resetViews()
                 when (viewModelUiState) {
                     is Uninitialized -> {}
                     is LiveTile -> drawLiveWindow()
-                    is Snapshot -> drawSnapshot(viewModelUiState)
+                    is SnapshotSplash -> drawSnapshotSplash(viewModelUiState)
                     is BackgroundOnly -> drawBackground(viewModelUiState.backgroundColor)
                 }
             }
             .launchIn(viewAttachedScope)
         viewModel.dimProgress
-            .onEach { dimProgress ->
-                // TODO(b/348195366) Add fade in/out for scrim
-                scrimView.alpha = dimProgress * MAX_SCRIM_ALPHA
-            }
+            .onEach { dimProgress -> scrimView.alpha = dimProgress }
+            .launchIn(viewAttachedScope)
+        viewModel.splashAlpha
+            .onEach { splashAlpha -> splashContainer.alpha = splashAlpha }
             .launchIn(viewAttachedScope)
         viewModel.cornerRadiusProgress.onEach { invalidateOutline() }.launchIn(viewAttachedScope)
         viewModel.inheritedScale
@@ -135,7 +132,36 @@
     }
 
     override fun onRecycle() {
-        // Do nothing
+        uiState = Uninitialized
+    }
+
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        super.onLayout(changed, left, top, right, bottom)
+        if (changed) {
+            viewData.width.value = abs(right - left)
+            viewData.height.value = abs(bottom - top)
+        }
+    }
+
+    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+        super.onSizeChanged(w, h, oldw, oldh)
+        if (uiState is SnapshotSplash) {
+            setImageMatrix()
+        }
+    }
+
+    override fun setScaleX(scaleX: Float) {
+        super.setScaleX(scaleX)
+        viewData.scaleX.value = scaleX
+        // Splash icon should ignore scale
+        splashIcon.scaleX = 1 / scaleX
+    }
+
+    override fun setScaleY(scaleY: Float) {
+        super.setScaleY(scaleY)
+        viewData.scaleY.value = scaleY
+        // Splash icon should ignore scale
+        splashIcon.scaleY = 1 / scaleY
     }
 
     override fun onConfigurationChanged(newConfig: Configuration?) {
@@ -148,7 +174,8 @@
 
     private fun resetViews() {
         liveTileView.isVisible = false
-        thumbnail.isVisible = false
+        thumbnailView.isVisible = false
+        splashContainer.alpha = 0f
         scrimView.alpha = 0f
         setBackgroundColor(Color.BLACK)
     }
@@ -161,10 +188,27 @@
         liveTileView.isVisible = true
     }
 
+    private fun drawSnapshotSplash(snapshotSplash: SnapshotSplash) {
+        drawSnapshot(snapshotSplash.snapshot)
+
+        splashContainer.isVisible = true
+        splashContainer.setBackgroundColor(snapshotSplash.snapshot.backgroundColor)
+        splashIcon.setImageDrawable(snapshotSplash.splash.icon)
+        splashIcon.updateLayoutParams<LayoutParams> {
+            width = snapshotSplash.splash.size.x
+            height = snapshotSplash.splash.size.y
+        }
+    }
+
     private fun drawSnapshot(snapshot: Snapshot) {
         drawBackground(snapshot.backgroundColor)
-        thumbnail.setImageBitmap(snapshot.bitmap)
-        thumbnail.isVisible = true
+        thumbnailView.setImageBitmap(snapshot.bitmap)
+        thumbnailView.isVisible = true
+        setImageMatrix()
+    }
+
+    private fun setImageMatrix() {
+        thumbnailView.imageMatrix = viewModel.getThumbnailPositionState(width, height, isLayoutRtl)
     }
 
     private fun getCurrentCornerRadius() =
@@ -173,8 +217,4 @@
             overviewCornerRadius,
             fullscreenCornerRadius
         ) / inheritedScale
-
-    private companion object {
-        const val MAX_SCRIM_ALPHA = 0.4f
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
new file mode 100644
index 0000000..1f8c0bc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class TaskThumbnailViewData {
+    val width = MutableStateFlow(0)
+    val height = MutableStateFlow(0)
+    val scaleX = MutableStateFlow(1f)
+    val scaleY = MutableStateFlow(1f)
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
deleted file mode 100644
index d8729a6..0000000
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.task.thumbnail
-
-import android.annotation.ColorInt
-import android.graphics.Rect
-import androidx.core.graphics.ColorUtils
-import com.android.quickstep.recents.data.RecentTasksRepository
-import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
-import com.android.quickstep.task.viewmodel.TaskContainerData
-import com.android.quickstep.task.viewmodel.TaskViewData
-import com.android.systemui.shared.recents.model.Task
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-
-@OptIn(ExperimentalCoroutinesApi::class)
-class TaskThumbnailViewModel(
-    recentsViewData: RecentsViewData,
-    taskViewData: TaskViewData,
-    taskContainerData: TaskContainerData,
-    private val tasksRepository: RecentTasksRepository,
-) {
-    private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
-    private var boundTaskIsRunning = false
-
-    /**
-     * Progress for changes in corner radius. progress: 0 = overview corner radius; 1 = fullscreen
-     * corner radius.
-     */
-    val cornerRadiusProgress =
-        if (taskViewData.isOutlineFormedByThumbnailView) recentsViewData.fullscreenProgress
-        else MutableStateFlow(1f).asStateFlow()
-    val inheritedScale =
-        combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
-            recentsScale * taskScale
-        }
-    val dimProgress: Flow<Float> = taskContainerData.taskMenuOpenProgress
-    val uiState: Flow<TaskThumbnailUiState> =
-        task
-            .flatMapLatest { taskFlow ->
-                taskFlow.map { taskVal ->
-                    when {
-                        taskVal == null -> Uninitialized
-                        boundTaskIsRunning -> LiveTile
-                        isBackgroundOnly(taskVal) ->
-                            BackgroundOnly(taskVal.colorBackground.removeAlpha())
-                        isSnapshotState(taskVal) -> {
-                            val bitmap = taskVal.thumbnail?.thumbnail!!
-                            Snapshot(
-                                bitmap,
-                                Rect(0, 0, bitmap.width, bitmap.height),
-                                taskVal.colorBackground.removeAlpha()
-                            )
-                        }
-                        else -> Uninitialized
-                    }
-                }
-            }
-            .distinctUntilChanged()
-
-    fun bind(taskThumbnail: TaskThumbnail) {
-        boundTaskIsRunning = taskThumbnail.isRunning
-        task.value = tasksRepository.getTaskDataById(taskThumbnail.taskId)
-    }
-
-    private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null
-
-    private fun isSnapshotState(task: Task): Boolean {
-        val thumbnailPresent = task.thumbnail?.thumbnail != null
-        val taskLocked = task.isLocked
-
-        return thumbnailPresent && !taskLocked
-    }
-
-    @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
-}
diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
index 5e55e2e..9253dbf 100644
--- a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
+++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
@@ -18,70 +18,85 @@
 
 import android.util.Log
 import com.android.quickstep.TaskOverlayFactory
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.get
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
 import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
-import com.android.quickstep.views.RecentsView
-import com.android.quickstep.views.RecentsViewContainer
 import com.android.systemui.shared.recents.model.Task
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
 
 /**
  * Helper for [TaskOverlayFactory.TaskOverlay] to interact with [TaskOverlayViewModel], this helper
  * should merge with [TaskOverlayFactory.TaskOverlay] when it's migrated to MVVM.
  */
 class TaskOverlayHelper(val task: Task, val overlay: TaskOverlayFactory.TaskOverlay<*>) {
-    private lateinit var job: Job
+    private lateinit var overlayInitializedScope: CoroutineScope
     private var uiState: TaskOverlayUiState = Disabled
 
-    // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
-    //  to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
-    //  This is using a lazy for now because the dependencies cannot be obtained without DI.
-    private val taskOverlayViewModel by lazy {
-        val recentsView =
-            RecentsViewContainer.containerFromContext<RecentsViewContainer>(
-                    overlay.taskView.context
-                )
-                .getOverviewPanel<RecentsView<*, *>>()
-        TaskOverlayViewModel(task, recentsView.mRecentsViewData!!, recentsView.mTasksRepository!!)
+    private val viewModel: TaskOverlayViewModel by lazy {
+        TaskOverlayViewModel(
+            task = task,
+            recentsViewData = RecentsDependencies.get(),
+            getThumbnailPositionUseCase = RecentsDependencies.get(),
+            recentTasksRepository = RecentsDependencies.get()
+        )
     }
 
     // TODO(b/331753115): TaskOverlay should listen for state changes and react.
     val enabledState: Enabled
         get() = uiState as Enabled
 
+    fun getThumbnailMatrix() = getThumbnailPositionState().matrix
+
+    private fun getThumbnailPositionState() =
+        viewModel.getThumbnailPositionState(
+            overlay.snapshotView.width,
+            overlay.snapshotView.height,
+            overlay.snapshotView.isLayoutRtl
+        )
+
     fun init() {
-        // TODO(b/335396935): This should be changed to TaskView's scope.
-        job =
-            MainScope().launch {
-                taskOverlayViewModel.overlayState.collect {
-                    uiState = it
-                    if (it is Enabled) {
-                        Log.d(
-                            TAG,
-                            "initOverlay - taskId: ${task.key.id}, thumbnail: ${it.thumbnail}"
-                        )
-                        overlay.initOverlay(
-                            task,
-                            it.thumbnail,
-                            it.thumbnailMatrix,
-                            /* rotated= */ false
-                        )
-                    } else {
-                        Log.d(TAG, "reset - taskId: ${task.key.id}")
-                        overlay.reset()
-                    }
+        overlayInitializedScope =
+            CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskOverlayHelper"))
+        viewModel.overlayState
+            .onEach {
+                uiState = it
+                if (it is Enabled) {
+                    initOverlay(it)
+                } else {
+                    reset()
                 }
             }
+            .launchIn(overlayInitializedScope)
+        overlay.snapshotView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+            (uiState as? Enabled)?.let { initOverlay(it) }
+        }
+    }
+
+    private fun initOverlay(enabledState: Enabled) {
+        Log.d(TAG, "initOverlay - taskId: ${task.key.id}, thumbnail: ${enabledState.thumbnail}")
+        with(getThumbnailPositionState()) {
+            overlay.initOverlay(task, enabledState.thumbnail, matrix, isRotated)
+        }
+    }
+
+    private fun reset() {
+        Log.d(TAG, "reset - taskId: ${task.key.id}")
+        overlay.reset()
     }
 
     fun destroy() {
-        job.cancel()
+        overlayInitializedScope.cancel()
         uiState = Disabled
-        overlay.reset()
+        reset()
     }
 
     companion object {
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
index 769424c..5f2de94 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
@@ -20,4 +20,6 @@
 
 class TaskContainerData {
     val taskMenuOpenProgress = MutableStateFlow(0f)
+
+    val thumbnailSplashProgress = MutableStateFlow(0f)
 }
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
index 47f32fb..4e13d1c 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
@@ -18,6 +18,9 @@
 
 import android.graphics.Matrix
 import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
@@ -25,31 +28,52 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
 
 /** View model for TaskOverlay */
 class TaskOverlayViewModel(
-    task: Task,
+    private val task: Task,
     recentsViewData: RecentsViewData,
-    tasksRepository: RecentTasksRepository,
+    private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
+    recentTasksRepository: RecentTasksRepository,
 ) {
     val overlayState =
         combine(
                 recentsViewData.overlayEnabled,
                 recentsViewData.settledFullyVisibleTaskIds.map { it.contains(task.key.id) },
-                tasksRepository.getThumbnailById(task.key.id)
+                recentTasksRepository.getThumbnailById(task.key.id)
             ) { isOverlayEnabled, isFullyVisible, thumbnailData ->
                 if (isOverlayEnabled && isFullyVisible) {
                     Enabled(
                         isRealSnapshot = (thumbnailData?.isRealSnapshot ?: false) && !task.isLocked,
                         thumbnailData?.thumbnail,
-                        // TODO(b/343101424): Use PreviewPositionHelper, listen from a common source
-                        // with
-                        //  TaskThumbnailView.
-                        Matrix.IDENTITY_MATRIX
                     )
                 } else {
                     Disabled
                 }
             }
             .distinctUntilChanged()
+
+    fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
+        return runBlocking {
+            val matrix: Matrix
+            val isRotated: Boolean
+            when (
+                val thumbnailPositionState =
+                    getThumbnailPositionUseCase.run(task.key.id, width, height, isRtl)
+            ) {
+                is MatrixScaling -> {
+                    matrix = thumbnailPositionState.matrix
+                    isRotated = thumbnailPositionState.isRotated
+                }
+                is MissingThumbnail -> {
+                    matrix = Matrix.IDENTITY_MATRIX
+                    isRotated = false
+                }
+            }
+            ThumbnailPositionState(matrix, isRotated)
+        }
+    }
+
+    data class ThumbnailPositionState(val matrix: Matrix, val isRotated: Boolean)
 }
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
new file mode 100644
index 0000000..de33919
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.viewmodel
+
+import android.annotation.ColorInt
+import android.graphics.Matrix
+import android.graphics.Point
+import androidx.core.graphics.ColorUtils
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.ThumbnailPositionState
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.GetSplashSizeUseCase
+import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
+import com.android.quickstep.task.thumbnail.TaskThumbnail
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Splash
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.systemui.shared.recents.model.Task
+import kotlin.math.max
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class TaskThumbnailViewModel(
+    recentsViewData: RecentsViewData,
+    taskViewData: TaskViewData,
+    taskContainerData: TaskContainerData,
+    private val tasksRepository: RecentTasksRepository,
+    private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
+    private val splashAlphaUseCase: SplashAlphaUseCase,
+    private val getSplashSizeUseCase: GetSplashSizeUseCase,
+) {
+    private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
+    private val splashProgress = MutableStateFlow(flowOf(0f))
+    private lateinit var taskThumbnail: TaskThumbnail
+
+    /**
+     * Progress for changes in corner radius. progress: 0 = overview corner radius; 1 = fullscreen
+     * corner radius.
+     */
+    val cornerRadiusProgress =
+        if (taskViewData.isOutlineFormedByThumbnailView) recentsViewData.fullscreenProgress
+        else MutableStateFlow(1f).asStateFlow()
+    val inheritedScale =
+        combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
+            recentsScale * taskScale
+        }
+    val dimProgress: Flow<Float> =
+        combine(taskContainerData.taskMenuOpenProgress, recentsViewData.tintAmount) {
+            taskMenuOpenProgress,
+            tintAmount ->
+            max(taskMenuOpenProgress * MAX_SCRIM_ALPHA, tintAmount)
+        }
+    val splashAlpha = splashProgress.flatMapLatest { it }
+    val uiState: Flow<TaskThumbnailUiState> =
+        task
+            .flatMapLatest { taskFlow ->
+                taskFlow.map { taskVal ->
+                    when {
+                        taskVal == null -> Uninitialized
+                        taskThumbnail.isRunning -> LiveTile
+                        isBackgroundOnly(taskVal) ->
+                            BackgroundOnly(taskVal.colorBackground.removeAlpha())
+                        isSnapshotSplashState(taskVal) ->
+                            SnapshotSplash(createSnapshotState(taskVal), createSplashState(taskVal))
+                        else -> Uninitialized
+                    }
+                }
+            }
+            .distinctUntilChanged()
+
+    fun bind(taskThumbnail: TaskThumbnail) {
+        this.taskThumbnail = taskThumbnail
+        task.value = tasksRepository.getTaskDataById(taskThumbnail.taskId)
+        splashProgress.value = splashAlphaUseCase.execute(taskThumbnail.taskId)
+    }
+
+    fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix {
+        return runBlocking {
+            when (
+                val thumbnailPositionState =
+                    getThumbnailPositionUseCase.run(taskThumbnail.taskId, width, height, isRtl)
+            ) {
+                is ThumbnailPositionState.MatrixScaling -> thumbnailPositionState.matrix
+                is ThumbnailPositionState.MissingThumbnail -> Matrix.IDENTITY_MATRIX
+            }
+        }
+    }
+
+    private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null
+
+    private fun isSnapshotSplashState(task: Task): Boolean {
+        val thumbnailPresent = task.thumbnail?.thumbnail != null
+        val taskLocked = task.isLocked
+
+        return thumbnailPresent && !taskLocked
+    }
+
+    private fun createSnapshotState(task: Task): Snapshot {
+        val thumbnailData = task.thumbnail
+        val bitmap = thumbnailData?.thumbnail!!
+        return Snapshot(bitmap, thumbnailData.rotation, task.colorBackground.removeAlpha())
+    }
+
+    private fun createSplashState(task: Task): Splash {
+        val taskIcon = task.icon
+        val size = if (taskIcon == null) Point() else getSplashSizeUseCase.execute(taskIcon)
+        return Splash(taskIcon, size)
+    }
+
+    @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
+
+    private companion object {
+        const val MAX_SCRIM_ALPHA = 0.4f
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
index 7a9ecf2..07dfc29 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
@@ -23,6 +23,8 @@
     // This is typically a View concern but it is used to invalidate rendering in other Views
     val scale = MutableStateFlow(1f)
 
+    val nonGridScale = MutableStateFlow(1f)
+
     // TODO(b/331753115): This property should not be in TaskViewData once TaskView is MVVM.
     /** Whether outline of TaskView is formed by outline thumbnail view(s). */
     val isOutlineFormedByThumbnailView: Boolean = taskViewType != TaskViewType.DESKTOP
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
new file mode 100644
index 0000000..30ee360
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class TaskViewModel(private val taskViewData: TaskViewData) : ViewModel() {
+    fun updateScale(scale: Float) {
+        taskViewData.scale.value = scale
+    }
+
+    fun updateNonGridScale(nonGridScale: Float) {
+        taskViewData.nonGridScale.value = nonGridScale
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/AnimUtils.java b/quickstep/src/com/android/quickstep/util/AnimUtils.java
index 8e3d44f..31aca03 100644
--- a/quickstep/src/com/android/quickstep/util/AnimUtils.java
+++ b/quickstep/src/com/android/quickstep/util/AnimUtils.java
@@ -17,18 +17,28 @@
 package com.android.quickstep.util;
 
 import static com.android.app.animation.Interpolators.clampToProgress;
+import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
+import android.animation.AnimatorSet;
+import android.annotation.NonNull;
 import android.os.Bundle;
 import android.os.IRemoteCallback;
 import android.view.animation.Interpolator;
 
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.util.RunnableList;
+import com.android.quickstep.views.RecentsViewContainer;
 
 /**
  * Utility class containing methods to help manage animations, interpolators, and timings.
  */
 public class AnimUtils {
+    private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350;
+
     /**
      * Fetches device-specific timings for the Overview > Split animation
      * (splitscreen initiated from Overview).
@@ -59,6 +69,33 @@
     }
 
     /**
+     * Synchronizes the timing for the split dismiss animation to the current transition to
+     * NORMAL (launcher home/workspace)
+     */
+    public static void goToNormalStateWithSplitDismissal(@NonNull StateManager stateManager,
+            @NonNull RecentsViewContainer container,
+            @NonNull StatsLogManager.LauncherEvent exitReason,
+            @NonNull SplitAnimationController animationController) {
+        StateAnimationConfig config = new StateAnimationConfig();
+        BaseState startState = stateManager.getState();
+        long duration = startState.getTransitionDuration(container.asContext(),
+                false /*isToState*/);
+        if (duration == 0) {
+            // Case where we're in contextual on workspace (NORMAL), which by default has 0
+            // transition duration
+            duration = DURATION_DEFAULT_SPLIT_DISMISS;
+        }
+        config.duration = duration;
+        AnimatorSet stateAnim = stateManager.createAtomicAnimation(
+                startState, NORMAL, config);
+        AnimatorSet dismissAnim = animationController
+                .createPlaceholderDismissAnim(container, exitReason, duration);
+        stateAnim.play(dismissAnim);
+        stateManager.setCurrentAnimation(stateAnim, NORMAL);
+        stateAnim.start();
+    }
+
+    /**
      * Returns a IRemoteCallback which completes the provided list as a result
      */
     public static IRemoteCallback completeRunnableListCallback(RunnableList list) {
diff --git a/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt b/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt
index d36dc7e..2dd727e 100644
--- a/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt
+++ b/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt
@@ -26,19 +26,21 @@
 import androidx.annotation.WorkerThread
 import com.android.launcher3.BuildConfig
 import com.android.launcher3.util.Executors
+import java.util.concurrent.CopyOnWriteArrayList
 
 /** Utility class to manage a set of device configurations */
 class DeviceConfigHelper<ConfigType>(private val factory: (PropReader) -> ConfigType) {
 
     var config: ConfigType
         private set
+
     private val allKeys: Set<String>
     private val propertiesListener = OnPropertiesChangedListener { onDevicePropsChanges(it) }
     private val sharedPrefChangeListener = OnSharedPreferenceChangeListener { _, _ ->
         recreateConfig()
     }
 
-    private val changeListeners = mutableListOf<Runnable>()
+    private val changeListeners = CopyOnWriteArrayList<Runnable>()
 
     init {
         // Initialize the default config once.
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index b8bc828..15081da 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -66,6 +66,7 @@
     private Float mPreviousVelocity = null;
 
     private OnMotionPauseListener mOnMotionPauseListener;
+    private boolean mIsTrackpadGesture;
     private boolean mIsPaused;
     // Bias more for the first pause to make it feel extra responsive.
     private boolean mHasEverBeenPaused;
@@ -115,6 +116,10 @@
         mOnMotionPauseListener = listener;
     }
 
+    public void setIsTrackpadGesture(boolean isTrackpadGesture) {
+        mIsTrackpadGesture = isTrackpadGesture;
+    }
+
     /**
      * @param disallowPause If true, we will not detect any pauses until this is set to false again.
      */
@@ -179,7 +184,8 @@
                     // We want to be more aggressive about detecting the first pause to ensure it
                     // feels as responsive as possible; getting two very slow speeds back to back
                     // takes too long, so also check for a rapid deceleration.
-                    boolean isRapidDeceleration = speed < previousSpeed * RAPID_DECELERATION_FACTOR;
+                    boolean isRapidDeceleration =
+                            speed < previousSpeed * getRapidDecelerationFactor();
                     isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast;
                     isPausedReason = new ActiveGestureLog.CompoundString(
                             "Didn't have back to back slow speeds, checking for rapid ")
@@ -253,6 +259,7 @@
         mVelocityProvider.clear();
         mPreviousVelocity = null;
         setOnMotionPauseListener(null);
+        mIsTrackpadGesture = false;
         mIsPaused = mHasEverBeenPaused = false;
         mSlowStartTime = 0;
         mForcePauseTimeout.cancelAlarm();
@@ -262,6 +269,12 @@
         return mIsPaused;
     }
 
+    private float getRapidDecelerationFactor() {
+        return mIsTrackpadGesture ? Float.parseFloat(
+                Utilities.getSystemProperty("trackpad_in_app_swipe_up_deceleration_factor",
+                        String.valueOf(RAPID_DECELERATION_FACTOR))) : RAPID_DECELERATION_FACTOR;
+    }
+
     public interface OnMotionPauseListener {
         /** Called only the first time motion pause is detected. */
         void onMotionPauseDetected();
diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
index cfe5b2c..70ef47c 100644
--- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
+++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
@@ -106,7 +106,8 @@
                         return;
                     }
                     mShouldIncreaseCount = toState == HINT_STATE
-                            && launcher.getWorkspace().getNextPage() == Workspace.DEFAULT_PAGE;
+                            && launcher.getWorkspace().getNextPage() == Workspace.DEFAULT_PAGE
+                            && launcher.isCanShowAllAppsEducationView();
                 }
 
                 @Override
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index e31a828..0335fa1 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -187,7 +187,6 @@
     ) {
         val snapshot = taskContainer.snapshotView
         val iconView: View = taskContainer.iconView.asView()
-        // TODO(334826842): Switch to splash state in TaskThumbnailView
         if (!enableRefactorTaskThumbnail()) {
             val thumbnailViewDeprecated = taskContainer.thumbnailViewDeprecated
             builder.add(
@@ -198,6 +197,15 @@
                 )
             )
             thumbnailViewDeprecated.setShowSplashForSplitSelection(true)
+        } else {
+            builder.add(
+                ValueAnimator.ofFloat(0f, 1f).apply {
+                    addUpdateListener {
+                        taskContainer.taskContainerData.thumbnailSplashProgress.value =
+                            it.animatedFraction
+                    }
+                }
+            )
         }
         // With the new `IconAppChipView`, we always want to keep the chip pinned to the
         // top left of the task / thumbnail.
diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java
deleted file mode 100644
index bb4a7ec..0000000
--- a/quickstep/src/com/android/quickstep/views/IconView.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.views.ActivityContext;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.RecentsOrientedState;
-
-/**
- * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
- * when the drawable changes.
- */
-public class IconView extends View implements TaskViewIcon {
-    private static final int NUM_ALPHA_CHANNELS = 2;
-    private static final int INDEX_CONTENT_ALPHA = 0;
-    private static final int INDEX_MODAL_ALPHA = 1;
-
-    private final MultiValueAlpha mMultiValueAlpha;
-
-    @Nullable
-    private Drawable mDrawable;
-    private int mDrawableWidth, mDrawableHeight;
-
-    public IconView(Context context) {
-        this(context, null);
-    }
-
-    public IconView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public IconView(Context context, AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
-    }
-
-    public IconView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-        mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS);
-        mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true);
-    }
-
-    /**
-     * Sets a {@link Drawable} to be displayed.
-     */
-    @Override
-    public void setDrawable(@Nullable Drawable d) {
-        if (mDrawable != null) {
-            mDrawable.setCallback(null);
-        }
-        mDrawable = d;
-        if (mDrawable != null) {
-            mDrawable.setCallback(this);
-            setDrawableSizeInternal(getWidth(), getHeight());
-        }
-        invalidate();
-    }
-
-    /**
-     * Sets the size of the icon drawable.
-     */
-    @Override
-    public void setDrawableSize(int iconWidth, int iconHeight) {
-        mDrawableWidth = iconWidth;
-        mDrawableHeight = iconHeight;
-        if (mDrawable != null) {
-            setDrawableSizeInternal(getWidth(), getHeight());
-        }
-    }
-
-    private void setDrawableSizeInternal(int selfWidth, int selfHeight) {
-        Rect selfRect = new Rect(0, 0, selfWidth, selfHeight);
-        Rect drawableRect = new Rect();
-        Gravity.apply(Gravity.CENTER, mDrawableWidth, mDrawableHeight, selfRect, drawableRect);
-        mDrawable.setBounds(drawableRect);
-    }
-
-    @Override
-    @Nullable
-    public Drawable getDrawable() {
-        return mDrawable;
-    }
-
-    @Override
-    public int getDrawableWidth() {
-        return mDrawableWidth;
-    }
-
-    @Override
-    public int getDrawableHeight() {
-        return mDrawableHeight;
-    }
-
-    @Override
-    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
-        super.onSizeChanged(w, h, oldw, oldh);
-        if (mDrawable != null) {
-            setDrawableSizeInternal(w, h);
-        }
-    }
-
-    @Override
-    protected boolean verifyDrawable(Drawable who) {
-        return super.verifyDrawable(who) || who == mDrawable;
-    }
-
-    @Override
-    protected void drawableStateChanged() {
-        super.drawableStateChanged();
-
-        final Drawable drawable = mDrawable;
-        if (drawable != null && drawable.isStateful()
-                && drawable.setState(getDrawableState())) {
-            invalidateDrawable(drawable);
-        }
-    }
-
-    @Override
-    protected void onDraw(Canvas canvas) {
-        if (mDrawable != null) {
-            mDrawable.draw(canvas);
-        }
-    }
-
-    @Override
-    public boolean hasOverlappingRendering() {
-        return false;
-    }
-
-    @Override
-    public void setContentAlpha(float alpha) {
-        mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha);
-    }
-
-    @Override
-    public void setModalAlpha(float alpha) {
-        mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha);
-    }
-
-    /**
-     * Set the tint color of the icon, useful for scrimming or dimming.
-     *
-     * @param color to blend in.
-     * @param amount [0,1] 0 no tint, 1 full tint
-     */
-    @Override
-    public void setIconColorTint(int color, float amount) {
-        if (mDrawable != null) {
-            mDrawable.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
-        }
-    }
-
-    @Override
-    public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
-        RecentsPagedOrientationHandler orientationHandler =
-                orientationState.getOrientationHandler();
-        boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
-        DeviceProfile deviceProfile =
-                ActivityContext.lookupContext(getContext()).getDeviceProfile();
-
-        FrameLayout.LayoutParams iconParams = (FrameLayout.LayoutParams) getLayoutParams();
-
-        int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
-        int taskIconHeight = deviceProfile.overviewTaskIconSizePx;
-        int taskMargin = deviceProfile.overviewTaskMarginPx;
-
-        orientationHandler.setTaskIconParams(iconParams, taskMargin, taskIconHeight,
-                thumbnailTopMargin, isRtl);
-        iconParams.width = iconParams.height = taskIconHeight;
-        setLayoutParams(iconParams);
-
-        setRotation(orientationHandler.getDegreesRotated());
-        int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
-                : deviceProfile.overviewTaskIconDrawableSizePx;
-        setDrawableSize(iconDrawableSize, iconDrawableSize);
-    }
-
-    @Override
-    public View asView() {
-        return this;
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/views/IconView.kt b/quickstep/src/com/android/quickstep/views/IconView.kt
new file mode 100644
index 0000000..583207f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/IconView.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.View
+import android.widget.FrameLayout
+import androidx.core.view.updateLayoutParams
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.MultiValueAlpha
+import com.android.launcher3.views.ActivityContext
+import com.android.quickstep.util.RecentsOrientedState
+
+/**
+ * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
+ * when the drawable changes.
+ */
+class IconView : View, TaskViewIcon {
+    private val multiValueAlpha: MultiValueAlpha = MultiValueAlpha(this, NUM_ALPHA_CHANNELS)
+    private var drawable: Drawable? = null
+    private var drawableWidth = 0
+    private var drawableHeight = 0
+
+    constructor(context: Context) : super(context)
+
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+    constructor(
+        context: Context,
+        attrs: AttributeSet?,
+        defStyleAttr: Int,
+    ) : super(context, attrs, defStyleAttr)
+
+    init {
+        multiValueAlpha.setUpdateVisibility(true)
+    }
+
+    /** Sets a [Drawable] to be displayed. */
+    override fun setDrawable(d: Drawable?) {
+        drawable?.callback = null
+
+        drawable = d
+        drawable?.let {
+            it.callback = this
+            setDrawableSizeInternal(width, height)
+        }
+        invalidate()
+    }
+
+    /** Sets the size of the icon drawable. */
+    override fun setDrawableSize(iconWidth: Int, iconHeight: Int) {
+        drawableWidth = iconWidth
+        drawableHeight = iconHeight
+        drawable?.let { setDrawableSizeInternal(width, height) }
+    }
+
+    private fun setDrawableSizeInternal(selfWidth: Int, selfHeight: Int) {
+        val selfRect = Rect(0, 0, selfWidth, selfHeight)
+        val drawableRect = Rect()
+        Gravity.apply(Gravity.CENTER, drawableWidth, drawableHeight, selfRect, drawableRect)
+        drawable?.bounds = drawableRect
+    }
+
+    override fun getDrawable(): Drawable? = drawable
+
+    override fun getDrawableWidth(): Int = drawableWidth
+
+    override fun getDrawableHeight(): Int = drawableHeight
+
+    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+        super.onSizeChanged(w, h, oldw, oldh)
+        drawable?.let { setDrawableSizeInternal(w, h) }
+    }
+
+    override fun verifyDrawable(who: Drawable): Boolean =
+        super.verifyDrawable(who) || who === drawable
+
+    override fun drawableStateChanged() {
+        super.drawableStateChanged()
+        drawable?.let {
+            if (it.isStateful && it.setState(drawableState)) {
+                invalidateDrawable(it)
+            }
+        }
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        drawable?.draw(canvas)
+    }
+
+    override fun hasOverlappingRendering(): Boolean = false
+
+    override fun setContentAlpha(alpha: Float) {
+        multiValueAlpha[INDEX_CONTENT_ALPHA].setValue(alpha)
+    }
+
+    override fun setModalAlpha(alpha: Float) {
+        multiValueAlpha[INDEX_MODAL_ALPHA].setValue(alpha)
+    }
+
+    /**
+     * Set the tint color of the icon, useful for scrimming or dimming.
+     *
+     * @param color to blend in.
+     * @param amount [0,1] 0 no tint, 1 full tint
+     */
+    override fun setIconColorTint(color: Int, amount: Float) {
+        drawable?.colorFilter = Utilities.makeColorTintingColorFilter(color, amount)
+    }
+
+    override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) {
+        val orientationHandler = orientationState.orientationHandler
+        val deviceProfile: DeviceProfile =
+            (ActivityContext.lookupContext(context) as ActivityContext).getDeviceProfile()
+        orientationHandler.setTaskIconParams(
+            iconParams = getLayoutParams() as FrameLayout.LayoutParams,
+            taskIconMargin = deviceProfile.overviewTaskMarginPx,
+            taskIconHeight = deviceProfile.overviewTaskIconSizePx,
+            thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx,
+            isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
+        )
+        updateLayoutParams<FrameLayout.LayoutParams> {
+            height = deviceProfile.overviewTaskIconSizePx
+            width = height
+        }
+        setRotation(orientationHandler.degreesRotated)
+        val iconDrawableSize =
+            if (isGridTask) deviceProfile.overviewTaskIconDrawableSizeGridPx
+            else deviceProfile.overviewTaskIconDrawableSizePx
+        setDrawableSize(iconDrawableSize, iconDrawableSize)
+    }
+
+    override fun asView(): View = this
+
+    companion object {
+        private const val NUM_ALPHA_CHANNELS = 2
+        private const val INDEX_CONTENT_ALPHA = 0
+        private const val INDEX_MODAL_ALPHA = 1
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index e48a7c6..d20d0a5 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -26,7 +26,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
 
 import android.annotation.TargetApi;
 import android.content.Context;
@@ -54,6 +54,7 @@
 import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.AnimUtils;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.systemui.shared.recents.model.Task;
 
@@ -91,10 +92,12 @@
     protected void handleStartHome(boolean animated) {
         StateManager stateManager = getStateManager();
         animated &= stateManager.shouldAnimateStateChange();
-        stateManager.goToState(NORMAL, animated);
-        if (FeatureFlags.enableSplitContextually()) {
-            mSplitSelectStateController.getSplitAnimationController()
-                    .playPlaceholderDismissAnim(mContainer, LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
+        if (mSplitSelectStateController.isSplitSelectActive()) {
+            AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer,
+                    LAUNCHER_SPLIT_SELECTION_EXIT_HOME,
+                    mSplitSelectStateController.getSplitAnimationController());
+        } else {
+            stateManager.goToState(NORMAL, animated);
         }
         AbstractFloatingView.closeAllOpenViews(mContainer, animated);
     }
@@ -265,7 +268,7 @@
         super.onGestureAnimationStart(runningTasks, rotationTouchHelper);
         DesktopVisibilityController desktopVisibilityController =
                 mContainer.getDesktopVisibilityController();
-        if (!enableDesktopWindowingWallpaperActivity() && desktopVisibilityController != null) {
+        if (!WALLPAPER_ACTIVITY.isEnabled(mContext) && desktopVisibilityController != null) {
             // TODO: b/333533253 - Remove after flag rollout
             desktopVisibilityController.setRecentsGestureStart();
         }
@@ -288,7 +291,7 @@
             }
         }
         super.onGestureAnimationEnd();
-        if (!enableDesktopWindowingWallpaperActivity() && desktopVisibilityController != null) {
+        if (!WALLPAPER_ACTIVITY.isEnabled(mContext) && desktopVisibilityController != null) {
             // TODO: b/333533253 - Remove after flag rollout
             desktopVisibilityController.setRecentsGestureEnd(endTarget);
         }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 3273809..8e232ee 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -191,8 +191,14 @@
 import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.ViewUtils;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.recents.data.TasksRepository;
+import com.android.quickstep.recents.data.RecentTasksRepository;
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepository;
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepositoryImpl;
+import com.android.quickstep.recents.data.RecentsRotationStateRepository;
+import com.android.quickstep.recents.data.RecentsRotationStateRepositoryImpl;
+import com.android.quickstep.recents.di.RecentsDependencies;
 import com.android.quickstep.recents.viewmodel.RecentsViewData;
+import com.android.quickstep.recents.viewmodel.RecentsViewModel;
 import com.android.quickstep.util.ActiveGestureErrorDetector;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.AnimUtils;
@@ -239,8 +245,9 @@
 
 /**
  * A list of recent tasks.
+ *
  * @param <CONTAINER_TYPE> : the container that should host recents view
- * @param <STATE_TYPE> : the type of base state that will be used
+ * @param <STATE_TYPE>     : the type of base state that will be used
  */
 
 public abstract class RecentsView<CONTAINER_TYPE extends Context & RecentsViewContainer,
@@ -389,7 +396,7 @@
                     view.setScaleX(scale);
                     view.setScaleY(scale);
                     if (enableRefactorTaskThumbnail()) {
-                        view.mRecentsViewData.getScale().setValue(scale);
+                        view.mRecentsViewModel.updateScale(scale);
                     }
                     view.mLastComputedTaskStartPushOutDistance = null;
                     view.mLastComputedTaskEndPushOutDistance = null;
@@ -461,11 +468,6 @@
 
     private static final float FOREGROUND_SCRIM_TINT = 0.32f;
 
-    @Nullable
-    public final RecentsViewData mRecentsViewData = new RecentsViewData();
-    @Nullable
-    public final TasksRepository mTasksRepository;
-
     protected final RecentsOrientedState mOrientationState;
     protected final BaseContainerInterface<STATE_TYPE, CONTAINER_TYPE> mSizeStrategy;
     @Nullable
@@ -720,10 +722,12 @@
 
     private final SplitSelectionListener mSplitSelectionListener = new SplitSelectionListener() {
         @Override
-        public void onSplitSelectionConfirmed() { }
+        public void onSplitSelectionConfirmed() {
+        }
 
         @Override
-        public void onSplitSelectionActive() { }
+        public void onSplitSelectionActive() {
+        }
 
         @Override
         public void onSplitSelectionExit(boolean launchedSplit) {
@@ -802,10 +806,13 @@
      */
     private boolean mAnyTaskHasBeenDismissed;
 
+    private final RecentsViewModel mRecentsViewModel;
+
     public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
             BaseContainerInterface sizeStrategy) {
         super(context, attrs, defStyleAttr);
         setEnableFreeScroll(true);
+
         mSizeStrategy = sizeStrategy;
         mContainer = RecentsViewContainer.containerFromContext(context);
         mOrientationState = new RecentsOrientedState(
@@ -813,18 +820,30 @@
         final int rotation = mContainer.getDisplay().getRotation();
         mOrientationState.setRecentsRotation(rotation);
 
+        // Start Recents Dependency graph
+        if (enableRefactorTaskThumbnail()) {
+            RecentsDependencies recentsDependencies = RecentsDependencies.Companion.initialize(
+                    this);
+            mRecentsViewModel = new RecentsViewModel(
+                    recentsDependencies.inject(RecentTasksRepository.class),
+                    recentsDependencies.inject(RecentsViewData.class)
+            );
+
+            recentsDependencies.provide(RecentsRotationStateRepository.class,
+                    () -> new RecentsRotationStateRepositoryImpl(mOrientationState));
+
+            recentsDependencies.provide(RecentsDeviceProfileRepository.class,
+                    () -> new RecentsDeviceProfileRepositoryImpl(mContainer));
+        } else {
+            mRecentsViewModel = null;
+        }
+
         mScrollHapticMinGapMillis = getResources()
                 .getInteger(R.integer.recentsScrollHapticMinGapMillis);
         mFastFlingVelocity = getResources()
                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
         mModel = RecentsModel.INSTANCE.get(context);
         mIdp = InvariantDeviceProfile.INSTANCE.get(context);
-        if (enableRefactorTaskThumbnail()) {
-            mTasksRepository = new TasksRepository(
-                    mModel, mModel.getThumbnailCache(), mModel.getIconCache());
-        } else {
-            mTasksRepository = null;
-        }
 
         mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
                 .inflate(R.layout.overview_clear_all_button, this, false);
@@ -1067,6 +1086,7 @@
 
     /**
      * Update the thumbnail(s) of the relevant TaskView.
+     *
      * @param refreshNow Refresh immediately if it's true.
      */
     @Nullable
@@ -1127,6 +1147,7 @@
 
     /**
      * See overridden implementations
+     *
      * @return {@code true} if child TaskViews can be launched when user taps on them
      */
     protected boolean canLaunchFullscreenTask() {
@@ -2051,7 +2072,7 @@
     public void setFullscreenProgress(float fullscreenProgress) {
         mFullscreenProgress = fullscreenProgress;
         if (enableRefactorTaskThumbnail()) {
-            mRecentsViewData.getFullscreenProgress().setValue(mFullscreenProgress);
+            mRecentsViewModel.updateFullscreenProgress(mFullscreenProgress);
         }
         int taskCount = getTaskViewCount();
         for (int i = 0; i < taskCount; i++) {
@@ -2449,7 +2470,7 @@
             }
         }
         if (enableRefactorTaskThumbnail()) {
-            mTasksRepository.setVisibleTasks(visibleTaskIds);
+            mRecentsViewModel.updateVisibleTasks(visibleTaskIds);
         }
     }
 
@@ -2516,6 +2537,13 @@
         mFocusedTaskViewId = -1;
         mAnyTaskHasBeenDismissed = false;
 
+
+        if (enableRefactorTaskThumbnail()) {
+            // TODO(b/353917593): RecentsView is never destroyed, so its dependencies need to
+            //  be cleaned up during the reset, but re-created when RecentsView is "resumed".
+            // RecentsDependencies.Companion.destroy();
+        }
+
         Log.d(TAG, "reset - mEnableDrawingLiveTile: " + mEnableDrawingLiveTile
                 + ", mRecentsAnimationController: " + mRecentsAnimationController);
         if (mEnableDrawingLiveTile) {
@@ -2626,6 +2654,7 @@
 
     /**
      * Get the index of the task view whose id matches {@param taskId}.
+     *
      * @return -1 if there is no task view for the task id, else the index of the task view.
      */
     public int getTaskIndexForId(int taskId) {
@@ -2641,7 +2670,7 @@
             mTaskListChangeId = mModel.getTasks(this::applyLoadPlan, RecentsFilterState
                     .getFilter(mFilterState.getPackageNameToFilter()));
             if (enableRefactorTaskThumbnail()) {
-                mTasksRepository.getAllTaskData(/* forceRefresh = */ true);
+                mRecentsViewModel.refreshAllTaskData();
             }
         }
     }
@@ -2810,7 +2839,7 @@
         int[] runningTaskIds = Arrays.stream(runningTasks).mapToInt(task -> task.key.id).toArray();
         TaskView matchingTaskView = null;
         if (hasDesktopTask(runningTasks) && runningTaskIds.length == 1) {
-            // TODO(b/249371338): Unsure if it's expected, desktop runningTasks only have a single
+            // TODO(b/342635213): Unsure if it's expected, desktop runningTasks only have a single
             // taskId, therefore we match any DesktopTaskView that contains the runningTaskId.
             TaskView taskview = getTaskViewByTaskId(runningTaskIds[0]);
             if (taskview instanceof DesktopTaskView) {
@@ -2851,7 +2880,7 @@
                 // When we create a placeholder task view mSplitBoundsConfig will be null, but with
                 // the actual app running we won't need to show the thumbnail until all the tasks
                 // load later anyways
-                ((GroupedTaskView)taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1],
+                ((GroupedTaskView) taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1],
                         mOrientationState, mTaskOverlayFactory, mSplitBoundsConfig);
             } else {
                 taskView = getTaskViewFromPool(TaskViewType.SINGLE);
@@ -3179,7 +3208,7 @@
                     (mIsRtl
                             ? mLastComputedTaskSize.right
                             : deviceProfile.widthPx - mLastComputedTaskSize.left)
-                    - longRowWidth - deviceProfile.overviewGridSideMargin;
+                            - longRowWidth - deviceProfile.overviewGridSideMargin;
             clearAllShortTotalWidthTranslation = mIsRtl
                     ? -mClearAllShortTotalWidthTranslation : mClearAllShortTotalWidthTranslation;
             if (snappedTaskRowWidth == longRowWidth) {
@@ -3209,9 +3238,9 @@
                     (mIsRtl
                             ? mLastComputedTaskSize.left
                             : deviceProfile.widthPx - mLastComputedTaskSize.right)
-                    - deviceProfile.overviewGridSideMargin - mPageSpacing
-                    + (mTaskWidth - snappedTaskView.getLayoutParams().width)
-                    - mClearAllShortTotalWidthTranslation;
+                            - deviceProfile.overviewGridSideMargin - mPageSpacing
+                            + (mTaskWidth - snappedTaskView.getLayoutParams().width)
+                            - mClearAllShortTotalWidthTranslation;
             if (distanceFromClearAll < minimumDistance) {
                 int distanceDifference = minimumDistance - distanceFromClearAll;
                 snappedTaskGridTranslationX += mIsRtl ? distanceDifference : -distanceDifference;
@@ -3270,6 +3299,10 @@
     }
 
     private void setTaskThumbnailSplashAlpha(float taskThumbnailSplashAlpha) {
+        if (enableRefactorTaskThumbnail()) {
+            mRecentsViewModel.updateThumbnailSplashProgress(taskThumbnailSplashAlpha);
+            return;
+        }
         int taskCount = getTaskViewCount();
         if (taskCount == 0) {
             return;
@@ -3291,12 +3324,12 @@
             mLayoutTransition.addTransitionListener(new TransitionListener() {
                 @Override
                 public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
-                    View view, int i) {
+                        View view, int i) {
                 }
 
                 @Override
                 public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
-                    View view, int i) {
+                        View view, int i) {
                     // When the unpinned task is added, snap to first page and disable transitions
                     if (view instanceof TaskView) {
                         snapToPage(0);
@@ -3448,11 +3481,13 @@
 
     /**
      * Creates a {@link PendingAnimation} for dismissing the specified {@link TaskView}.
-     * @param dismissedTaskView the {@link TaskView} to be dismissed
-     * @param animateTaskView whether the {@link TaskView} to be dismissed should be animated
-     * @param shouldRemoveTask whether the associated {@link Task} should be removed from
-     *                         ActivityManager after dismissal
-     * @param duration duration of the animation
+     *
+     * @param dismissedTaskView           the {@link TaskView} to be dismissed
+     * @param animateTaskView             whether the {@link TaskView} to be dismissed should be
+     *                                    animated
+     * @param shouldRemoveTask            whether the associated {@link Task} should be removed from
+     *                                    ActivityManager after dismissal
+     * @param duration                    duration of the animation
      * @param dismissingForSplitSelection task dismiss animation is used for entering split
      *                                    selection state from app icon
      */
@@ -3717,7 +3752,7 @@
 
                     float animationEndProgress = isSplitSelectionActive()
                             ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset()
-                                            + splitTimings.getGridSlideDurationOffset(), 0f, 1f)
+                            + splitTimings.getGridSlideDurationOffset(), 0f, 1f)
                             : 1f;
 
                     // Slide tiles in horizontally to fill dismissed area
@@ -3761,16 +3796,16 @@
                 // dismissed index or next focused index. Offset successive task dismissal
                 // durations for a staggered effect.
                 distanceFromDismissedTask++;
-                int staggerColumn =  isStagingFocusedTask
+                int staggerColumn = isStagingFocusedTask
                         ? (int) Math.ceil(distanceFromDismissedTask / 2f)
                         : distanceFromDismissedTask;
                 // Set timings based on if user is initiating splitscreen on the focused task,
                 // or splitting/dismissing some other task.
                 float animationStartProgress = isStagingFocusedTask
                         ? Utilities.boundToRange(
-                                splitTimings.getGridSlideStartOffset()
-                                        + (splitTimings.getGridSlideStaggerOffset()
-                                        * staggerColumn),
+                        splitTimings.getGridSlideStartOffset()
+                                + (splitTimings.getGridSlideStaggerOffset()
+                                * staggerColumn),
                         0f,
                         dismissTranslationInterpolationEnd)
                         : Utilities.boundToRange(
@@ -3779,9 +3814,9 @@
                                         * staggerColumn, 0f, dismissTranslationInterpolationEnd);
                 float animationEndProgress = isStagingFocusedTask
                         ? Utilities.boundToRange(
-                                splitTimings.getGridSlideStartOffset()
-                                        + (splitTimings.getGridSlideStaggerOffset() * staggerColumn)
-                                        + splitTimings.getGridSlideDurationOffset(),
+                        splitTimings.getGridSlideStartOffset()
+                                + (splitTimings.getGridSlideStaggerOffset() * staggerColumn)
+                                + splitTimings.getGridSlideDurationOffset(),
                         0f,
                         dismissTranslationInterpolationEnd)
                         : dismissTranslationInterpolationEnd;
@@ -3997,8 +4032,9 @@
                                             RecentsView.this);
                                     int taskSize = (int) (
                                             getPagedOrientationHandler().getMeasuredSize(
-                                            taskView) * taskView
-                                            .getSizeAdjustment(/*fullscreenEnabled=*/false));
+                                                    taskView) * taskView
+                                                    .getSizeAdjustment(/*fullscreenEnabled=*/
+                                                            false));
                                     int taskEnd = taskStart + taskSize;
 
                                     shouldRebalance = taskEnd >= screenEnd - mPageSpacing;
@@ -4641,6 +4677,7 @@
     /**
      * Computes the distance to offset the given child such that it is completely offscreen when
      * translating away from the given midpoint.
+     *
      * @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen.
      */
     private float getHorizontalOffsetSize(int childIndex, int midpointIndex, float offsetProgress) {
@@ -4862,8 +4899,7 @@
                             mContainer.getDeviceProfile(),
                             mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(),
                             primaryTaskSelected);
-            builder.addOnFrameCallback(() ->{
-                // TODO(b/334826842): Handle splash icon for new TTV.
+            builder.addOnFrameCallback(() -> {
                 if (!enableRefactorTaskThumbnail()) {
                     taskContainer.getThumbnailViewDeprecated().refreshSplashView();
                 }
@@ -4886,17 +4922,21 @@
      * @param containerTaskView If our second selected app is currently running in Recents, this is
      *                          the "container" TaskView from Recents. If we are starting a fresh
      *                          instance of the app from an Intent, this will be null.
-     * @param task The Task corresponding to our second selected app. If we are starting a fresh
-     *             instance of the app from an Intent, this will be null.
-     * @param drawable The Drawable corresponding to our second selected app's icon.
-     * @param secondView The View representing the current space on the screen where the second app
-     *                   is (either the ThumbnailView or the tapped icon).
-     * @param intent If we are launching a fresh instance of the app, this is the Intent for it. If
-     *               the second app is already running in Recents, this will be null.
-     * @param user If we are launching a fresh instance of the app, this is the UserHandle for it.
-     *             If the second app is already running in Recents, this will be null.
+     * @param task              The Task corresponding to our second selected app. If we are
+     *                          starting a fresh
+     *                          instance of the app from an Intent, this will be null.
+     * @param drawable          The Drawable corresponding to our second selected app's icon.
+     * @param secondView        The View representing the current space on the screen where the
+     *                          second app
+     *                          is (either the ThumbnailView or the tapped icon).
+     * @param intent            If we are launching a fresh instance of the app, this is the Intent
+     *                          for it. If
+     *                          the second app is already running in Recents, this will be null.
+     * @param user              If we are launching a fresh instance of the app, this is the
+     *                          UserHandle for it.
+     *                          If the second app is already running in Recents, this will be null.
      * @return true if waiting for confirmation of second app or if split animations are running,
-     *          false otherwise
+     * false otherwise
      */
     public boolean confirmSplitSelect(TaskView containerTaskView, Task task, Drawable drawable,
             View secondView, @Nullable Bitmap thumbnail, Intent intent, UserHandle user,
@@ -4984,7 +5024,7 @@
                 "Second tile selected");
 
         // Fade out all other views underneath placeholders
-        ObjectAnimator tvFade = ObjectAnimator.ofFloat(this, RecentsView.CONTENT_ALPHA,1, 0);
+        ObjectAnimator tvFade = ObjectAnimator.ofFloat(this, RecentsView.CONTENT_ALPHA, 1, 0);
         pendingAnimation.add(tvFade, DECELERATE_2, SpringProperty.DEFAULT);
         pendingAnimation.buildAnim().start();
         return true;
@@ -5132,7 +5172,7 @@
         if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) {
             int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding;
             mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(),
-                    mEmptyMessagePaint, availableWidth)
+                            mEmptyMessagePaint, availableWidth)
                     .setAlignment(Layout.Alignment.ALIGN_CENTER)
                     .build();
             int totalHeight = mEmptyTextLayout.getHeight()
@@ -5259,7 +5299,7 @@
         updateScrollSynchronously();
 
         int targetSysUiFlags = tv.getTaskContainers().getFirst().getSysUiStatusNavFlags();
-        final boolean[] passedOverviewThreshold = new boolean[] {false};
+        final boolean[] passedOverviewThreshold = new boolean[]{false};
         ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
         progressAnim.addUpdateListener(animator -> {
             // Once we pass a certain threshold, update the sysui flags to match the target
@@ -5380,7 +5420,7 @@
                     fullyVisibleTaskIds.addAll(taskIds);
                 }
             }
-            mRecentsViewData.getSettledFullyVisibleTaskIds().setValue(fullyVisibleTaskIds);
+            mRecentsViewModel.updateTasksFullyVisible(fullyVisibleTaskIds);
         }
     }
 
@@ -5463,7 +5503,7 @@
         }
 
         RemoteTargetGluer gluer;
-        if (recentsAnimationTargets.hasDesktopTasks()) {
+        if (recentsAnimationTargets.hasDesktopTasks(mContext)) {
             gluer = new RemoteTargetGluer(getContext(), getSizeStrategy(), recentsAnimationTargets,
                     true /* forDesktop */);
             mRemoteTargetHandles = gluer.assignTargetsForDesktop(recentsAnimationTargets);
@@ -5779,6 +5819,7 @@
      * Sets whether or not we should clamp the scroll offset.
      * This is used to avoid x-axis movement when swiping up transient taskbar.
      * Should only be set at the beginning and end of the gesture, otherwise a jump may occur.
+     *
      * @param clampScrollOffset When true, we clamp the scroll to 0 before the clamp threshold is
      *                          met.
      */
@@ -5811,11 +5852,11 @@
         // Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so that
         // the page can move freely given there's no visual indication why it shouldn't.
         int overScrollShift = mAdjacentPageHorizontalOffset > 0
-                        ? (int) Utilities.mapRange(
-                                mAdjacentPageHorizontalOffset,
-                                getOverScrollShift(),
-                                getUndampedOverScrollShift())
-                        : getOverScrollShift();
+                ? (int) Utilities.mapRange(
+                mAdjacentPageHorizontalOffset,
+                getOverScrollShift(),
+                getUndampedOverScrollShift())
+                : getOverScrollShift();
         return getScrollForPage(pageIndex) - getPagedOrientationHandler().getPrimaryScroll(this)
                 + overScrollShift + getOffsetFromScrollPosition(pageIndex);
     }
@@ -5932,7 +5973,10 @@
         if (mOverlayEnabled != overlayEnabled) {
             mOverlayEnabled = overlayEnabled;
             updateEnabledOverlays();
-            mRecentsViewData.getOverlayEnabled().setValue(overlayEnabled);
+
+            if (enableRefactorTaskThumbnail()) {
+                mRecentsViewModel.setOverlayEnabled(overlayEnabled);
+            }
         }
     }
 
@@ -6077,7 +6121,6 @@
      * tasks to be dimmed while other elements in the recents view are left alone.
      */
     public void showForegroundScrim(boolean show) {
-        // TODO(b/349601769) Add scrim response into new TTV - this is called from overlay
         if (!show && mColorTint == 0) {
             if (mTintingAnimator != null) {
                 mTintingAnimator.cancel();
@@ -6096,6 +6139,10 @@
     private void setColorTint(float tintAmount) {
         mColorTint = tintAmount;
 
+        if (enableRefactorTaskThumbnail()) {
+            mRecentsViewModel.setTintAmount(tintAmount);
+        }
+
         for (int i = 0; i < getTaskViewCount(); i++) {
             requireTaskViewAt(i).setColorTint(mColorTint, mTintingColor);
         }
@@ -6120,7 +6167,7 @@
     public boolean showAsGrid() {
         return mOverviewGridEnabled || (mCurrentGestureEndTarget != null
                 && mSizeStrategy.stateFromGestureEndTarget(mCurrentGestureEndTarget)
-                    .displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
+                .displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
     }
 
     private boolean showAsFullscreen() {
@@ -6161,7 +6208,7 @@
 
     /**
      * @return Corner radius in pixel value for PiP window, which is updated via
-     *         {@link #mIPipAnimationListener}
+     * {@link #mIPipAnimationListener}
      */
     public int getPipCornerRadius() {
         return mPipCornerRadius;
@@ -6169,7 +6216,7 @@
 
     /**
      * @return Shadow radius in pixel value for PiP window, which is updated via
-     *         {@link #mIPipAnimationListener}
+     * {@link #mIPipAnimationListener}
      */
     public int getPipShadowRadius() {
         return mPipShadowRadius;
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index 3d994e8..f6393e4 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -16,13 +16,11 @@
 
 package com.android.quickstep.views;
 
-import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON;
 import static com.android.settingslib.widget.theme.R.dimen.settingslib_preferred_minimum_touch_target;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
@@ -42,9 +40,8 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.util.AnimUtils;
 import com.android.quickstep.util.SplitSelectStateController;
 
 /**
@@ -57,7 +54,6 @@
 public class SplitInstructionsView extends LinearLayout {
     private static final int BOUNCE_DURATION = 250;
     private static final float BOUNCE_HEIGHT = 20;
-    private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350;
 
     private final RecentsViewContainer mContainer;
     public boolean mIsCurrentlyAnimating = false;
@@ -165,25 +161,11 @@
     private void exitSplitSelection() {
         RecentsView recentsView = mContainer.getOverviewPanel();
         SplitSelectStateController splitSelectController = recentsView.getSplitSelectController();
-
         StateManager stateManager = recentsView.getStateManager();
-        BaseState startState = stateManager.getState();
-        long duration = startState.getTransitionDuration(mContainer.asContext(), false);
-        if (duration == 0) {
-            // Case where we're in contextual on workspace (NORMAL), which by default has 0
-            // transition duration
-            duration = DURATION_DEFAULT_SPLIT_DISMISS;
-        }
-        StateAnimationConfig config = new StateAnimationConfig();
-        config.duration = duration;
-        AnimatorSet stateAnim = stateManager.createAtomicAnimation(
-                startState, NORMAL, config);
-        AnimatorSet dismissAnim = splitSelectController.getSplitAnimationController()
-                .createPlaceholderDismissAnim(mContainer,
-                        LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON, duration);
-        stateAnim.play(dismissAnim);
-        stateManager.setCurrentAnimation(stateAnim, NORMAL);
-        stateAnim.start();
+
+        AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer,
+                LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON,
+                splitSelectController.getSplitAnimationController());
     }
 
     void ensureProperRotation() {
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index 74d120f..6202bba 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -29,10 +29,15 @@
 import com.android.launcher3.util.TransformingTouchDelegate
 import com.android.quickstep.TaskOverlayFactory
 import com.android.quickstep.TaskUtils
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.get
+import com.android.quickstep.recents.di.getScope
+import com.android.quickstep.recents.di.inject
+import com.android.quickstep.recents.viewmodel.TaskContainerViewModel
 import com.android.quickstep.task.thumbnail.TaskThumbnail
 import com.android.quickstep.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.util.GetThumbnailUseCase
 import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
 import com.android.systemui.shared.recents.model.Task
 
 /** Holder for all Task dependent information. */
@@ -55,21 +60,31 @@
     taskOverlayFactory: TaskOverlayFactory
 ) {
     val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
-    val taskContainerData = TaskContainerData()
+    lateinit var taskContainerData: TaskContainerData
 
-    private val getThumbnailUseCase by lazy {
-        // TODO(b/335649589): Ideally create and obtain this from DI.
-        val recentsView =
-            RecentsViewContainer.containerFromContext<RecentsViewContainer>(
-                    overlay.taskView.context
-                )
-                .getOverviewPanel<RecentsView<*, *>>()
-        GetThumbnailUseCase(recentsView.mTasksRepository!!)
+    private val taskThumbnailViewModel: TaskThumbnailViewModel by
+        RecentsDependencies.inject(snapshotView)
+
+    // TODO(b/335649589): Ideally create and obtain this from DI.
+    private val taskContainerViewModel: TaskContainerViewModel by lazy {
+        TaskContainerViewModel(
+            sysUiStatusNavFlagsUseCase = RecentsDependencies.get(),
+            getThumbnailUseCase = RecentsDependencies.get(),
+            splashAlphaUseCase = RecentsDependencies.get(),
+        )
     }
 
     init {
         if (enableRefactorTaskThumbnail()) {
             require(snapshotView is TaskThumbnailView)
+            taskContainerData = RecentsDependencies.get(this)
+            RecentsDependencies.getScope(snapshotView).apply {
+                val taskViewScope = RecentsDependencies.getScope(taskView)
+                linkTo(taskViewScope)
+
+                val taskContainerScope = RecentsDependencies.getScope(this@TaskContainer)
+                linkTo(taskContainerScope)
+            }
         } else {
             require(snapshotView is TaskThumbnailViewDeprecated)
         }
@@ -78,7 +93,7 @@
     val splitAnimationThumbnail: Bitmap?
         get() =
             if (enableRefactorTaskThumbnail()) {
-                getThumbnailUseCase.run(task.key.id)
+                taskContainerViewModel.getThumbnail(task.key.id)
             } else {
                 thumbnailViewDeprecated.thumbnail
             }
@@ -98,13 +113,15 @@
     // TODO(b/334826842): Support shouldShowSplashView for new TTV.
     val shouldShowSplashView: Boolean
         get() =
-            if (enableRefactorTaskThumbnail()) false
+            if (enableRefactorTaskThumbnail())
+                taskContainerViewModel.shouldShowThumbnailSplash(task.key.id)
             else thumbnailViewDeprecated.shouldShowSplashView()
 
-    // TODO(b/350743460) Support sysUiStatusNavFlags for new TTV.
     val sysUiStatusNavFlags: Int
         get() =
-            if (enableRefactorTaskThumbnail()) 0 else thumbnailViewDeprecated.sysUiStatusNavFlags
+            if (enableRefactorTaskThumbnail())
+                taskContainerViewModel.getSysUiStatusNavFlags(task.key.id)
+            else thumbnailViewDeprecated.sysUiStatusNavFlags
 
     /** Builds proto for logging */
     val itemInfo: WorkspaceItemInfo
@@ -146,12 +163,10 @@
         overlay.destroy()
     }
 
-    // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
-    //  so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
     fun bindThumbnailView() {
         // TODO(b/343364498): Existing view has shouldShowScreenshot as an override as well but
         //  this should be decided inside TaskThumbnailViewModel.
-        thumbnailView.viewModel.bind(TaskThumbnail(task.key.id, taskView.isRunningTask))
+        taskThumbnailViewModel.bind(TaskThumbnail(task.key.id, taskView.isRunningTask))
     }
 
     fun setOverlayEnabled(enabled: Boolean) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 004003c..b2abe69 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -80,8 +80,10 @@
 import com.android.quickstep.TaskOverlayFactory
 import com.android.quickstep.TaskViewUtils
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.get
 import com.android.quickstep.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.viewmodel.TaskViewData
+import com.android.quickstep.task.viewmodel.TaskViewModel
 import com.android.quickstep.util.ActiveGestureErrorDetector
 import com.android.quickstep.util.ActiveGestureLog
 import com.android.quickstep.util.BorderAnimator
@@ -115,7 +117,8 @@
     @IntDef(FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS)
     annotation class TaskDataChanges
 
-    val taskViewData = TaskViewData(type)
+    private lateinit var taskViewModel: TaskViewModel
+
     val taskIds: IntArray
         /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */
         get() = taskContainers.map { it.task.key.id }.toIntArray()
@@ -282,6 +285,9 @@
         set(value) {
             field = value
             applyScale()
+            if (enableRefactorTaskThumbnail()) {
+                taskViewModel.updateNonGridScale(value)
+            }
         }
 
     private var dismissScale = 1f
@@ -441,6 +447,11 @@
 
     init {
         setOnClickListener { _ -> onClick() }
+
+        if (enableRefactorTaskThumbnail()) {
+            taskViewModel = RecentsDependencies.get(this, "TaskViewType" to type)
+        }
+
         val keyboardFocusHighlightEnabled =
             (ENABLE_KEYBOARD_QUICK_SWITCH.get() || enableFocusOutline())
         val cursorHoverStatesEnabled = enableCursorHoverStates()
@@ -638,6 +649,7 @@
         orientedState: RecentsOrientedState,
         taskOverlayFactory: TaskOverlayFactory
     ) {
+
         cancelPendingLoadTasks()
         taskContainers =
             listOf(
@@ -1042,11 +1054,9 @@
                     if (isQuickSwitch) {
                         setFreezeRecentTasksReordering()
                     }
-                    // TODO(b/334826842) add splash functionality to new TTV
-                    if (!enableRefactorTaskThumbnail()) {
-                        disableStartingWindow =
-                            firstContainer.thumbnailViewDeprecated.shouldShowSplashView()
-                    }
+                    // TODO(b/334826842) no work required - add splash functionality to new TTV -
+                    // cold start e.g. restart device. Small splash moving to bigger splash
+                    disableStartingWindow = firstContainer.shouldShowSplashView
                 }
         Executors.UI_HELPER_EXECUTOR.execute {
             if (
@@ -1387,7 +1397,7 @@
 
     protected open fun refreshTaskThumbnailSplash() {
         if (!enableRefactorTaskThumbnail()) {
-            // TODO(b/334826842) add splash functionality to new TTV
+            // TODO(b/342560598) handle onTaskIconChanged
             taskContainers.forEach { it.thumbnailViewDeprecated.refreshSplashView() }
         }
     }
@@ -1404,14 +1414,13 @@
         scaleX = scale
         scaleY = scale
         if (enableRefactorTaskThumbnail()) {
-            taskViewData.scale.value = scale
+            taskViewModel.updateScale(scale)
         }
         updateSnapshotRadius()
     }
 
     protected open fun applyThumbnailSplashAlpha() {
         if (!enableRefactorTaskThumbnail()) {
-            // TODO(b/334826842) add splash functionality to new TTV
             taskContainers.forEach {
                 it.thumbnailViewDeprecated.setSplashAlpha(taskThumbnailSplashAlpha)
             }
@@ -1483,8 +1492,6 @@
 
     /** Updates [TaskThumbnailView] to reflect the latest [Task] state (i.e., task isRunning). */
     fun notifyIsRunningTaskUpdated() {
-        // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
-        //  so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
         taskContainers.forEach { it.bindThumbnailView() }
     }
 
diff --git a/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
similarity index 79%
rename from quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
index a532762..0005df6 100644
--- a/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.launcher3.model
 
 import android.app.prediction.AppPredictor
@@ -19,7 +34,7 @@
 import org.mockito.Mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.MockitoAnnotations
 
 /** Unit tests for [QuickstepModelDelegate]. */
@@ -57,25 +72,25 @@
         underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_PREDICTION)
 
         verify(allAppsPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
-        verifyZeroInteractions(hotseatPredictor)
-        verifyZeroInteractions(widgetRecommendationPredictor)
+        verifyNoMoreInteractions(hotseatPredictor)
+        verifyNoMoreInteractions(widgetRecommendationPredictor)
     }
 
     @Test
     fun onWidgetPrediction_notifyWidgetRecommendationPredictor() {
         underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_WIDGETS_PREDICTION)
 
-        verifyZeroInteractions(allAppsPredictor)
+        verifyNoMoreInteractions(allAppsPredictor)
         verify(widgetRecommendationPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
-        verifyZeroInteractions(hotseatPredictor)
+        verifyNoMoreInteractions(hotseatPredictor)
     }
 
     @Test
     fun onHotseatPrediction_notifyHotseatPredictor() {
         underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_HOTSEAT_PREDICTION)
 
-        verifyZeroInteractions(allAppsPredictor)
-        verifyZeroInteractions(widgetRecommendationPredictor)
+        verifyNoMoreInteractions(allAppsPredictor)
+        verifyNoMoreInteractions(widgetRecommendationPredictor)
         verify(hotseatPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
     }
 
@@ -83,8 +98,8 @@
     fun onOtherClient_notifyHotseatPredictor() {
         underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_WALLPAPERS)
 
-        verifyZeroInteractions(allAppsPredictor)
-        verifyZeroInteractions(widgetRecommendationPredictor)
+        verifyNoMoreInteractions(allAppsPredictor)
+        verifyNoMoreInteractions(widgetRecommendationPredictor)
         verify(hotseatPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt
new file mode 100644
index 0000000..8bad3b9
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.Path
+import android.graphics.drawable.ColorDrawable
+import android.view.LayoutInflater
+import androidx.core.graphics.drawable.toBitmap
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.R
+import com.android.wm.shell.common.bubbles.BubbleInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleViewTest {
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var bubbleView: BubbleView
+    private lateinit var overflowView: BubbleView
+    private lateinit var bubble: BubbleBarBubble
+
+    @Test
+    fun hasUnseenContent_bubble() {
+        setupBubbleViews()
+        assertThat(bubbleView.hasUnseenContent()).isTrue()
+
+        bubbleView.markSeen()
+        assertThat(bubbleView.hasUnseenContent()).isFalse()
+    }
+
+    @Test
+    fun hasUnseenContent_overflow() {
+        setupBubbleViews()
+        assertThat(overflowView.hasUnseenContent()).isFalse()
+    }
+
+    private fun setupBubbleViews() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            val inflater = LayoutInflater.from(context)
+
+            val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
+            overflowView = inflater.inflate(R.layout.bubblebar_item_view, null, false) as BubbleView
+            overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
+
+            val bubbleInfo =
+                BubbleInfo("key", 0, null, null, 0, context.packageName, null, null, false)
+            bubbleView = inflater.inflate(R.layout.bubblebar_item_view, null, false) as BubbleView
+            bubble =
+                BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
+            bubbleView.setBubble(bubble)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
new file mode 100644
index 0000000..c0a5dfa
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.stashing
+
+import android.animation.AnimatorTestRule
+import android.content.Context
+import android.widget.FrameLayout
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.anim.AnimatedFloat
+import com.android.launcher3.taskbar.TaskbarInsetsController
+import com.android.launcher3.taskbar.bubbles.BubbleBarView
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.util.MultiValueAlpha
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/** Unit tests for [PersistentBubbleStashController]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PersistentBubbleStashControllerTest {
+
+    companion object {
+        const val BUBBLE_BAR_HEIGHT = 100f
+        const val HOTSEAT_TRANSLATION_Y = -45f
+        const val TASK_BAR_TRANSLATION_Y = -5f
+    }
+
+    @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this)
+
+    @get:Rule val rule: MockitoRule = MockitoJUnit.rule()
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var bubbleBarView: BubbleBarView
+
+    @Mock lateinit var bubbleBarViewController: BubbleBarViewController
+
+    @Mock lateinit var taskbarInsetsController: TaskbarInsetsController
+
+    private lateinit var persistentTaskBarStashController: PersistentBubbleStashController
+    private lateinit var translationY: AnimatedFloat
+    private lateinit var scale: AnimatedFloat
+    private lateinit var alpha: MultiValueAlpha
+
+    @Before
+    fun setUp() {
+        persistentTaskBarStashController =
+            PersistentBubbleStashController(DefaultDimensionsProvider())
+        setUpBubbleBarView()
+        setUpBubbleBarController()
+        persistentTaskBarStashController.init(
+            taskbarInsetsController,
+            bubbleBarViewController,
+            null,
+            ImmediateAction()
+        )
+    }
+
+    @Test
+    fun setBubblesShowingOnHomeUpdatedToFalse_barPositionYUpdated_controllersNotified() {
+        // Given bubble bar is on home and has bubbles
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        persistentTaskBarStashController.isBubblesShowingOnHome = true
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+        // When switch out of the home screen
+        getInstrumentation().runOnMainSync {
+            persistentTaskBarStashController.isBubblesShowingOnHome = false
+        }
+
+        // Then translation Y is animating and the bubble bar controller is notified
+        assertThat(translationY.isAnimating).isTrue()
+        verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+        // Wait until animation ends
+        advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION)
+        // Check translation Y is correct and the insets controller is notified
+        assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y)
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    @Test
+    fun setBubblesShowingOnHomeUpdatedToTrue_barPositionYUpdated_controllersNotified() {
+        // Given bubble bar has bubbles
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+        // When switch to home screen
+        getInstrumentation().runOnMainSync {
+            persistentTaskBarStashController.isBubblesShowingOnHome = true
+        }
+
+        // Then translation Y is animating and the bubble bar controller is notified
+        assertThat(translationY.isAnimating).isTrue()
+        verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+        // Wait until animation ends
+        advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION)
+
+        // Check translation Y is correct and the insets controller is notified
+        assertThat(bubbleBarView.translationY).isEqualTo(HOTSEAT_TRANSLATION_Y)
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    @Test
+    fun setBubblesShowingOnOverviewUpdatedToFalse_controllersNotified() {
+        // Given bubble bar is on overview
+        persistentTaskBarStashController.isBubblesShowingOnOverview = true
+        clearInvocations(bubbleBarViewController)
+
+        // When switch out of the overview screen
+        persistentTaskBarStashController.isBubblesShowingOnOverview = false
+
+        // Then bubble bar controller is notified
+        verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+    }
+
+    @Test
+    fun setBubblesShowingOnOverviewUpdatedToTrue_controllersNotified() {
+        // When switch to the overview screen
+        persistentTaskBarStashController.isBubblesShowingOnOverview = true
+
+        // Then bubble bar controller is notified
+        verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+    }
+
+    @Test
+    fun isSysuiLockedSwitchedToFalseForOverview_unlockAnimationIsShown() {
+        // Given screen is locked and bubble bar has bubbles
+        persistentTaskBarStashController.isSysuiLocked = true
+        persistentTaskBarStashController.isBubblesShowingOnOverview = true
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+        // When switch to the overview screen
+        getInstrumentation().runOnMainSync {
+            persistentTaskBarStashController.isSysuiLocked = false
+        }
+
+        // Then
+        assertThat(translationY.isAnimating).isTrue()
+        assertThat(scale.isAnimating).isTrue()
+        // Wait until animation ends
+        advanceTimeBy(BubbleStashController.BAR_STASH_DURATION)
+
+        // Then bubble bar is fully visible at the correct location
+        assertThat(bubbleBarView.scaleX).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1f)
+        assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y)
+        assertThat(bubbleBarView.alpha).isEqualTo(1f)
+        // Insets controller is notified
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    @Test
+    fun showBubbleBarImmediateToY() {
+        // Given bubble bar is fully transparent and scaled to 0 at 0 y position
+        val targetY = 341f
+        bubbleBarView.alpha = 0f
+        bubbleBarView.scaleX = 0f
+        bubbleBarView.scaleY = 0f
+        bubbleBarView.translationY = 0f
+
+        // When
+        persistentTaskBarStashController.showBubbleBarImmediate(targetY)
+
+        // Then all property values are updated
+        assertThat(bubbleBarView.translationY).isEqualTo(targetY)
+        assertThat(bubbleBarView.alpha).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1f)
+    }
+
+    @Test
+    fun isTransientTaskbar_false() {
+        assertThat(persistentTaskBarStashController.isTransientTaskBar).isFalse()
+    }
+
+    @Test
+    fun hasHandleView_false() {
+        assertThat(persistentTaskBarStashController.hasHandleView).isFalse()
+    }
+
+    @Test
+    fun isStashed_false() {
+        assertThat(persistentTaskBarStashController.isStashed).isFalse()
+    }
+
+    @Test
+    fun bubbleBarTranslationYForTaskbar() {
+        // Give bubble bar is on home
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        persistentTaskBarStashController.isBubblesShowingOnHome = true
+
+        // Then bubbleBarTranslationY would be HOTSEAT_TRANSLATION_Y
+        assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
+            .isEqualTo(HOTSEAT_TRANSLATION_Y)
+
+        // Give bubble bar is not on home
+        persistentTaskBarStashController.isBubblesShowingOnHome = false
+
+        // Then bubbleBarTranslationY would be TASK_BAR_TRANSLATION_Y
+        assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
+            .isEqualTo(TASK_BAR_TRANSLATION_Y)
+    }
+
+    private fun advanceTimeBy(advanceMs: Long) {
+        // Advance animator for on-device tests
+        getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(advanceMs) }
+    }
+
+    private fun setUpBubbleBarView() {
+        getInstrumentation().runOnMainSync {
+            bubbleBarView = BubbleBarView(context)
+            bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
+        }
+    }
+
+    private fun setUpBubbleBarController() {
+        translationY = AnimatedFloat(Runnable { bubbleBarView.translationY = translationY.value })
+        scale =
+            AnimatedFloat(
+                Runnable {
+                    val scale: Float = scale.value
+                    bubbleBarView.scaleX = scale
+                    bubbleBarView.scaleY = scale
+                }
+            )
+        alpha = MultiValueAlpha(bubbleBarView, 1 /* num alpha channels */)
+
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+        whenever(bubbleBarViewController.bubbleBarTranslationY).thenReturn(translationY)
+        whenever(bubbleBarViewController.bubbleBarScale).thenReturn(scale)
+        whenever(bubbleBarViewController.bubbleBarAlpha).thenReturn(alpha)
+        whenever(bubbleBarViewController.bubbleBarCollapsedHeight).thenReturn(BUBBLE_BAR_HEIGHT)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt
new file mode 100644
index 0000000..00ad3b7
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.stashing
+
+class ImmediateAction : BubbleStashController.ControllersAfterInitAction {
+    override fun runAfterInit(action: () -> Unit) = action.invoke()
+}
+
+class DefaultDimensionsProvider(
+    private val taskBarBottomSpace: Int = TASKBAR_BOTTOM_SPACE,
+    private val taskBarHeight: Int = TASKBAR_HEIGHT,
+    private val hotseatBottomSpace: Int = HOTSEAT_BOTTOM_SPACE,
+    private val hotseatHeight: Int = HOTSEAT_HEIGHT
+) : BubbleStashController.TaskbarHotseatDimensionsProvider {
+    override fun getTaskbarBottomSpace(): Int = taskBarBottomSpace
+
+    override fun getTaskbarHeight(): Int = taskBarHeight
+
+    override fun getHotseatBottomSpace(): Int = hotseatBottomSpace
+
+    override fun getHotseatHeight(): Int = hotseatHeight
+
+    companion object {
+        const val TASKBAR_BOTTOM_SPACE = 0
+        const val TASKBAR_HEIGHT = 110
+        const val HOTSEAT_BOTTOM_SPACE = 20
+        const val HOTSEAT_HEIGHT = 150
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
new file mode 100644
index 0000000..b5809c2
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.stashing
+
+import android.animation.AnimatorTestRule
+import android.content.Context
+import android.view.View
+import android.widget.FrameLayout
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.anim.AnimatedFloat
+import com.android.launcher3.taskbar.StashedHandleView
+import com.android.launcher3.taskbar.TaskbarInsetsController
+import com.android.launcher3.taskbar.bubbles.BubbleBarView
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.STASHED_BAR_SCALE
+import com.android.launcher3.util.MultiValueAlpha
+import com.android.wm.shell.shared.animation.PhysicsAnimator
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/** Unit tests for [TransientBubbleStashController]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TransientBubbleStashControllerTest {
+
+    companion object {
+        const val TASKBAR_BOTTOM_SPACE = 5
+        const val BUBBLE_BAR_HEIGHT = 100f
+        const val HOTSEAT_TRANSLATION_Y = -45f
+        const val TASK_BAR_TRANSLATION_Y = -TASKBAR_BOTTOM_SPACE
+        const val HANDLE_VIEW_HEIGHT = 4
+        const val BUBBLE_BAR_STASHED_TRANSLATION_Y = 48
+    }
+
+    @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this)
+
+    @get:Rule val rule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock lateinit var bubbleStashedHandleViewController: BubbleStashedHandleViewController
+
+    @Mock lateinit var bubbleBarViewController: BubbleBarViewController
+
+    @Mock lateinit var taskbarInsetsController: TaskbarInsetsController
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var bubbleBarView: BubbleBarView
+    private lateinit var stashedHandleView: StashedHandleView
+    private lateinit var barTranslationY: AnimatedFloat
+    private lateinit var barScale: AnimatedFloat
+    private lateinit var barAlpha: MultiValueAlpha
+    private lateinit var stashedHandleAlpha: MultiValueAlpha
+    private lateinit var stashedHandleScale: AnimatedFloat
+    private lateinit var stashedHandleTranslationY: AnimatedFloat
+    private lateinit var stashPhysicsAnimator: PhysicsAnimator<View>
+
+    private lateinit var mTransientBubbleStashController: TransientBubbleStashController
+
+    @Before
+    fun setUp() {
+        val taskbarHotseatDimensionsProvider =
+            DefaultDimensionsProvider(taskBarBottomSpace = TASKBAR_BOTTOM_SPACE)
+        mTransientBubbleStashController =
+            TransientBubbleStashController(taskbarHotseatDimensionsProvider, context.resources)
+        setUpBubbleBarView()
+        setUpBubbleBarController()
+        setUpStashedHandleView()
+        setUpBubbleStashedHandleViewController()
+        PhysicsAnimatorTestUtils.prepareForTest()
+        mTransientBubbleStashController.init(
+            taskbarInsetsController,
+            bubbleBarViewController,
+            bubbleStashedHandleViewController,
+            ImmediateAction()
+        )
+    }
+
+    @Test
+    fun setBubblesShowingOnHomeUpdatedToTrue_barPositionYUpdated_controllersNotified() {
+        // Given bubble bar is on home and has bubbles
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+        // When switch out of the home screen
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.isBubblesShowingOnHome = true
+        }
+
+        // Then BubbleBarView is animating, BubbleBarViewController controller is notified
+        assertThat(barTranslationY.isAnimating).isTrue()
+        verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+
+        // Wait until animation ends
+        advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION)
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+        // Then translation Y is correct and the insets controller is notified
+        assertThat(barTranslationY.isAnimating).isFalse()
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+        assertThat(bubbleBarView.translationY).isEqualTo(HOTSEAT_TRANSLATION_Y)
+    }
+
+    @Test
+    fun setBubblesShowingOnOverviewUpdatedToTrue_barPositionYUpdated_controllersNotified() {
+        // Given bubble bar is on home and has bubbles
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+        // When switch out of the home screen
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.isBubblesShowingOnOverview = true
+        }
+
+        // Then BubbleBarView is animating, BubbleBarViewController controller is notified
+        assertThat(barTranslationY.isAnimating).isTrue()
+        verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
+
+        // Wait until animation ends
+        advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION)
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+        // Then translation Y is correct and the insets controller is notified
+        assertThat(barTranslationY.isAnimating).isFalse()
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+        assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y)
+    }
+
+    @Test
+    fun updateStashedAndExpandedState_stashAndCollapse_bubbleBarHidden_stashedHandleShown() {
+        // Given bubble bar has bubbles and not stashed
+        mTransientBubbleStashController.isStashed = false
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+
+        // When stash
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.updateStashedAndExpandedState(
+                stash = true,
+                expand = false
+            )
+        }
+
+        // Wait until animations ends
+        advanceTimeBy(BubbleStashController.BAR_STASH_DURATION)
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // Then check BubbleBarController is notified
+        verify(bubbleBarViewController).onStashStateChanging()
+        // Bubble bar is stashed
+        assertThat(mTransientBubbleStashController.isStashed).isTrue()
+        assertThat(bubbleBarView.translationY).isEqualTo(BUBBLE_BAR_STASHED_TRANSLATION_Y)
+        assertThat(bubbleBarView.alpha).isEqualTo(0f)
+        assertThat(bubbleBarView.scaleX).isEqualTo(STASHED_BAR_SCALE)
+        assertThat(bubbleBarView.scaleY).isEqualTo(STASHED_BAR_SCALE)
+        // Handle view is visible
+        assertThat(stashedHandleView.translationY).isEqualTo(0)
+        assertThat(stashedHandleView.alpha).isEqualTo(1)
+    }
+
+    @Test
+    fun isSysuiLockedSwitchedToFalseForOverview_unlockAnimationIsShown() {
+        // Given screen is locked and bubble bar has bubbles
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.isSysuiLocked = true
+            mTransientBubbleStashController.isBubblesShowingOnOverview = true
+            whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+        }
+        advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION)
+
+        // When switch to the overview screen
+        getInstrumentation().runOnMainSync { mTransientBubbleStashController.isSysuiLocked = false }
+
+        // Then
+        assertThat(barTranslationY.isAnimating).isTrue()
+        assertThat(barScale.isAnimating).isTrue()
+        // Wait until animation ends
+        advanceTimeBy(BubbleStashController.BAR_STASH_DURATION)
+
+        // Then bubble bar is fully visible at the correct location
+        assertThat(bubbleBarView.scaleX).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1f)
+        assertThat(bubbleBarView.translationY)
+            .isEqualTo(PersistentBubbleStashControllerTest.TASK_BAR_TRANSLATION_Y)
+        assertThat(bubbleBarView.alpha).isEqualTo(1f)
+        // Insets controller is notified
+        verify(taskbarInsetsController, atLeastOnce())
+            .onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    @Test
+    fun showBubbleBarImmediateToY() {
+        // Given bubble bar is fully transparent and scaled to 0 at 0 y position
+        val targetY = 341f
+        bubbleBarView.alpha = 0f
+        bubbleBarView.scaleX = 0f
+        bubbleBarView.scaleY = 0f
+        bubbleBarView.translationY = 0f
+        stashedHandleView.translationY = targetY
+
+        // When
+        mTransientBubbleStashController.showBubbleBarImmediate(targetY)
+
+        // Then all property values are updated
+        assertThat(bubbleBarView.translationY).isEqualTo(targetY)
+        assertThat(bubbleBarView.alpha).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1f)
+        // Handle is transparent
+        assertThat(stashedHandleView.alpha).isEqualTo(0)
+        // Insets controller is notified
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    @Test
+    fun stashBubbleBarImmediate() {
+        // When
+        mTransientBubbleStashController.stashBubbleBarImmediate()
+
+        // Then all property values are updated
+        assertThat(bubbleBarView.translationY).isEqualTo(BUBBLE_BAR_STASHED_TRANSLATION_Y)
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(bubbleBarView.scaleX).isEqualTo(STASHED_BAR_SCALE)
+        assertThat(bubbleBarView.scaleY).isEqualTo(STASHED_BAR_SCALE)
+        // Handle is visible at correct Y position
+        assertThat(stashedHandleView.alpha).isEqualTo(1)
+        assertThat(stashedHandleView.translationY).isEqualTo(0)
+        // Insets controller is notified
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    @Test
+    fun getTouchableHeight_stashed_stashHeightReturned() {
+        // When
+        mTransientBubbleStashController.isStashed = true
+        val height = mTransientBubbleStashController.getTouchableHeight()
+
+        // Then
+        assertThat(height).isEqualTo(HANDLE_VIEW_HEIGHT)
+    }
+
+    @Test
+    fun getTouchableHeight_unstashed_barHeightReturned() {
+        // When BubbleBar is not stashed
+        mTransientBubbleStashController.isStashed = false
+        val height = mTransientBubbleStashController.getTouchableHeight()
+
+        // Then bubble bar height is returned
+        assertThat(height).isEqualTo(BUBBLE_BAR_HEIGHT.toInt())
+    }
+
+    private fun advanceTimeBy(advanceMs: Long) {
+        // Advance animator for on-device tests
+        getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(advanceMs) }
+    }
+
+    private fun setUpBubbleBarView() {
+        getInstrumentation().runOnMainSync {
+            bubbleBarView = BubbleBarView(context)
+            bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
+        }
+    }
+
+    private fun setUpStashedHandleView() {
+        getInstrumentation().runOnMainSync {
+            stashedHandleView = StashedHandleView(context)
+            stashedHandleView.layoutParams = FrameLayout.LayoutParams(0, 0)
+        }
+    }
+
+    private fun setUpBubbleBarController() {
+        barTranslationY =
+            AnimatedFloat(Runnable { bubbleBarView.translationY = barTranslationY.value })
+        barScale =
+            AnimatedFloat(
+                Runnable {
+                    val scale: Float = barScale.value
+                    bubbleBarView.scaleX = scale
+                    bubbleBarView.scaleY = scale
+                }
+            )
+        barAlpha = MultiValueAlpha(bubbleBarView, 1 /* num alpha channels */)
+
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+        whenever(bubbleBarViewController.bubbleBarTranslationY).thenReturn(barTranslationY)
+        whenever(bubbleBarViewController.bubbleBarScale).thenReturn(barScale)
+        whenever(bubbleBarViewController.bubbleBarAlpha).thenReturn(barAlpha)
+        whenever(bubbleBarViewController.bubbleBarCollapsedHeight).thenReturn(BUBBLE_BAR_HEIGHT)
+    }
+
+    private fun setUpBubbleStashedHandleViewController() {
+        stashedHandleTranslationY =
+            AnimatedFloat(Runnable { stashedHandleView.translationY = barTranslationY.value })
+        stashedHandleScale =
+            AnimatedFloat(
+                Runnable {
+                    val scale: Float = barScale.value
+                    bubbleBarView.scaleX = scale
+                    bubbleBarView.scaleY = scale
+                }
+            )
+        stashedHandleAlpha = MultiValueAlpha(stashedHandleView, 1 /* num alpha channels */)
+        stashPhysicsAnimator = PhysicsAnimator.getInstance(stashedHandleView)
+        whenever(bubbleStashedHandleViewController.stashedHandleAlpha)
+            .thenReturn(stashedHandleAlpha)
+        whenever(bubbleStashedHandleViewController.physicsAnimator).thenReturn(stashPhysicsAnimator)
+        whenever(bubbleStashedHandleViewController.stashedHeight).thenReturn(HANDLE_VIEW_HEIGHT)
+        whenever(bubbleStashedHandleViewController.setTranslationYForSwipe(any())).thenAnswer {
+            invocation ->
+            (invocation.arguments[0] as Float).also { stashedHandleView.translationY = it }
+        }
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
index a966d2a..bbcf566 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -29,10 +29,10 @@
 import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.launcher3.taskbar.TaskbarManager
 import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks
+import com.android.launcher3.taskbar.TaskbarViewController
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
 import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
 import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric
-import com.android.launcher3.util.ModelTestExtensions.loadModelSync
 import com.android.launcher3.util.TestUtil
 import com.android.quickstep.AllAppsActionManager
 import com.android.quickstep.TouchInteractionService
@@ -152,7 +152,7 @@
                     }
 
                 try {
-                    LauncherAppState.getInstance(context).model.loadModelSync()
+                    TaskbarViewController.enableModelLoadingForTests(false)
 
                     // Replace Launcher Taskbar window with test instance.
                     instrumentation.runOnMainSync {
@@ -167,6 +167,8 @@
                         taskbarManager.destroy()
                         launcherTaskbarManager?.setSuspended(false)
                     }
+
+                    TaskbarViewController.enableModelLoadingForTests(true)
                 }
             }
         }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt
new file mode 100644
index 0000000..24f9696
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt
@@ -0,0 +1,139 @@
+package com.android.quickstep
+
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags
+import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(Flags.FLAG_ENABLE_LAUNCHER_BR_METRICS_FIXED)
+class LauncherRestoreEventLoggerImplTest {
+
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
+    private val mLauncherModelHelper = LauncherModelHelper()
+    private val mSandboxContext: SandboxModelContext = mLauncherModelHelper.sandboxContext
+    private lateinit var loggerUnderTest: LauncherRestoreEventLoggerImpl
+
+    @Before
+    fun setup() {
+        loggerUnderTest = LauncherRestoreEventLoggerImpl(mSandboxContext)
+    }
+
+    @After
+    fun teardown() {
+        loggerUnderTest.restoreEventLogger.clearData()
+        mLauncherModelHelper.destroy()
+    }
+
+    @Test
+    fun `logLauncherItemsRestoreFailed logs multiple items as failing restore`() {
+        // Given
+        val expectedDataType = "application"
+        val expectedError = "test_failure"
+        // When
+        loggerUnderTest.logLauncherItemsRestoreFailed(
+            dataType = expectedDataType,
+            count = 5,
+            error = expectedError
+        )
+        // Then
+        val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+        assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+        assertThat(actualResult.successCount).isEqualTo(0)
+        assertThat(actualResult.failCount).isEqualTo(5)
+        assertThat(actualResult.errors.keys).containsExactly(expectedError)
+    }
+
+    @Test
+    fun `logLauncherItemsRestored logs multiple items as restored`() {
+        // Given
+        val expectedDataType = "application"
+        // When
+        loggerUnderTest.logLauncherItemsRestored(dataType = expectedDataType, count = 5)
+        // Then
+        val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+        assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+        assertThat(actualResult.successCount).isEqualTo(5)
+        assertThat(actualResult.failCount).isEqualTo(0)
+        assertThat(actualResult.errors.keys).isEmpty()
+    }
+
+    @Test
+    fun `logSingleFavoritesItemRestored logs a single Favorites Item as restored`() {
+        // Given
+        val expectedDataType = "widget"
+        // When
+        loggerUnderTest.logSingleFavoritesItemRestored(favoritesId = Favorites.ITEM_TYPE_APPWIDGET)
+        // Then
+        val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+        assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+        assertThat(actualResult.successCount).isEqualTo(1)
+        assertThat(actualResult.failCount).isEqualTo(0)
+        assertThat(actualResult.errors.keys).isEmpty()
+    }
+
+    @Test
+    fun `logSingleFavoritesItemRestoreFailed logs a single Favorites Item as failing restore`() {
+        // Given
+        val expectedDataType = "widget"
+        val expectedError = "test_failure"
+        // When
+        loggerUnderTest.logSingleFavoritesItemRestoreFailed(
+            favoritesId = Favorites.ITEM_TYPE_APPWIDGET,
+            error = expectedError
+        )
+        // Then
+        val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+        assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+        assertThat(actualResult.successCount).isEqualTo(0)
+        assertThat(actualResult.failCount).isEqualTo(1)
+        assertThat(actualResult.errors.keys).containsExactly(expectedError)
+    }
+
+    @Test
+    fun `logFavoritesItemsRestoreFailed logs multiple Favorites Items as failing restore`() {
+        // Given
+        val expectedDataType = "deep_shortcut"
+        val expectedError = "test_failure"
+        // When
+        loggerUnderTest.logFavoritesItemsRestoreFailed(
+            favoritesId = Favorites.ITEM_TYPE_DEEP_SHORTCUT,
+            count = 5,
+            error = expectedError
+        )
+        // Then
+        val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+        assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+        assertThat(actualResult.successCount).isEqualTo(0)
+        assertThat(actualResult.failCount).isEqualTo(5)
+        assertThat(actualResult.errors.keys).containsExactly(expectedError)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/MultiStateCallbackTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/MultiStateCallbackTest.java
new file mode 100644
index 0000000..0ff142a
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/MultiStateCallbackTest.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.launcher3.util.LauncherMultivalentJUnit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit.class)
+public class MultiStateCallbackTest {
+
+    private int mFlagCount = 0;
+    private int getNextStateFlag() {
+        int index = 1 << mFlagCount;
+        mFlagCount++;
+        return index;
+    }
+
+    private final MultiStateCallback mMultiStateCallback = new MultiStateCallback(new String[0]);
+    private final Runnable mCallback = spy(new Runnable() {
+        @Override
+        public void run() {}
+    });
+    private final Consumer<Boolean> mListener = spy(new Consumer<Boolean>() {
+        @Override
+        public void accept(Boolean isOn) {}
+    });
+
+    @Test
+    public void testSetState_trackedProperly() {
+        int watchedAnime = getNextStateFlag();
+
+        assertThat(mMultiStateCallback.getState()).isEqualTo(0);
+        assertThat(mMultiStateCallback.hasStates(watchedAnime)).isFalse();
+
+        mMultiStateCallback.setState(watchedAnime);
+
+        assertThat(mMultiStateCallback.getState()).isEqualTo(watchedAnime);
+        assertThat(mMultiStateCallback.hasStates(watchedAnime)).isTrue();
+    }
+
+    @Test
+    public void testSetState_withMultipleStates_trackedProperly() {
+        int watchedAnime = getNextStateFlag();
+        int sharedMemes = getNextStateFlag();
+
+        mMultiStateCallback.setState(watchedAnime);
+        mMultiStateCallback.setState(sharedMemes);
+
+        assertThat(mMultiStateCallback.getState()).isEqualTo(watchedAnime | sharedMemes);
+        assertThat(mMultiStateCallback.hasStates(watchedAnime)).isTrue();
+        assertThat(mMultiStateCallback.hasStates(sharedMemes)).isTrue();
+        assertThat(mMultiStateCallback.hasStates(watchedAnime | sharedMemes)).isTrue();
+    }
+
+    @Test
+    public void testClearState_trackedProperly() {
+        int lovedAnime = getNextStateFlag();
+
+        mMultiStateCallback.setState(lovedAnime);
+        mMultiStateCallback.clearState(lovedAnime);
+
+        assertThat(mMultiStateCallback.getState()).isEqualTo(0);
+        assertThat(mMultiStateCallback.hasStates(lovedAnime)).isFalse();
+    }
+
+    @Test
+    public void testClearState_withMultipleState_trackedProperly() {
+        int lovedAnime = getNextStateFlag();
+        int talkedAboutAnime = getNextStateFlag();
+
+        mMultiStateCallback.setState(lovedAnime);
+        mMultiStateCallback.setState(talkedAboutAnime);
+        mMultiStateCallback.clearState(talkedAboutAnime);
+
+        assertThat(mMultiStateCallback.getState()).isEqualTo(lovedAnime);
+        assertThat(mMultiStateCallback.hasStates(lovedAnime)).isTrue();
+        assertThat(mMultiStateCallback.hasStates(talkedAboutAnime)).isFalse();
+        assertThat(mMultiStateCallback.hasStates(lovedAnime | talkedAboutAnime)).isFalse();
+    }
+
+    @Test
+    public void testCallbackDoesNotRun_withoutState() {
+        int watchedOnePiece = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(watchedOnePiece, mCallback);
+
+        verify(mCallback, never()).run();
+    }
+
+    @Test
+    public void testCallbackDoesNotRun_whenNotTracked() {
+        int watchedJujutsuKaisen = getNextStateFlag();
+
+        mMultiStateCallback.setState(watchedJujutsuKaisen);
+
+        verify(mCallback, never()).run();
+    }
+
+    @Test
+    public void testCallbackRuns_afterTrackedAndStateSet() {
+        int watchedHunterXHunter = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(watchedHunterXHunter, mCallback);
+        mMultiStateCallback.setState(watchedHunterXHunter);
+
+        verify(mCallback, times(1)).run();
+    }
+
+    @Test
+    public void testCallbackRuns_onUiThread() {
+        int watchedHunterXHunter = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(watchedHunterXHunter, mCallback);
+        mMultiStateCallback.setStateOnUiThread(watchedHunterXHunter);
+
+        runOnMainSync(() -> verify(mCallback, times(1)).run());
+    }
+
+    @Test
+    public void testCallbackRuns_agnosticallyToCallOrder() {
+        int watchedFullMetalAlchemist = getNextStateFlag();
+
+        mMultiStateCallback.setState(watchedFullMetalAlchemist);
+        mMultiStateCallback.runOnceAtState(watchedFullMetalAlchemist, mCallback);
+
+        verify(mCallback, times(1)).run();
+    }
+
+    @Test
+    public void testCallbackRuns_onlyOnceAfterStateSet() {
+        int watchedBleach = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(watchedBleach, mCallback);
+        mMultiStateCallback.setState(watchedBleach);
+        mMultiStateCallback.setState(watchedBleach);
+
+        verify(mCallback, times(1)).run();
+    }
+
+    @Test
+    public void testCallbackRuns_onlyOnceAfterClearState() {
+        int rememberedGreatShow = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(rememberedGreatShow, mCallback);
+        mMultiStateCallback.setState(rememberedGreatShow);
+        mMultiStateCallback.clearState(rememberedGreatShow);
+        mMultiStateCallback.setState(rememberedGreatShow);
+
+        verify(mCallback, times(1)).run();
+    }
+
+    @Test
+    public void testCallbackDoesNotRun_withoutFullStateSet() {
+        int watchedMobPsycho = getNextStateFlag();
+        int watchedVinlandSaga = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(watchedMobPsycho | watchedVinlandSaga, mCallback);
+        mMultiStateCallback.setState(watchedMobPsycho);
+
+        verify(mCallback, times(0)).run();
+    }
+
+    @Test
+    public void testCallbackRuns_withFullStateSet_agnosticallyToCallOrder() {
+        int watchedReZero = getNextStateFlag();
+        int watchedJojosBizareAdventure = getNextStateFlag();
+
+        mMultiStateCallback.setState(watchedJojosBizareAdventure);
+        mMultiStateCallback.runOnceAtState(watchedReZero | watchedJojosBizareAdventure, mCallback);
+        mMultiStateCallback.setState(watchedReZero);
+
+        verify(mCallback, times(1)).run();
+    }
+
+    @Test
+    public void testCallbackRuns_withFullStateSet_asIntegerMask() {
+        int watchedPokemon = getNextStateFlag();
+        int watchedDigimon = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(watchedPokemon | watchedDigimon, mCallback);
+        mMultiStateCallback.setState(watchedPokemon | watchedDigimon);
+
+        verify(mCallback, times(1)).run();
+    }
+
+    @Test
+    public void testCallbackDoesNotRun_afterClearState() {
+        int watchedMonster = getNextStateFlag();
+        int watchedPingPong = getNextStateFlag();
+
+        mMultiStateCallback.runOnceAtState(watchedMonster | watchedPingPong, mCallback);
+        mMultiStateCallback.setState(watchedMonster);
+        mMultiStateCallback.clearState(watchedMonster);
+        mMultiStateCallback.setState(watchedPingPong);
+
+        verify(mCallback, times(0)).run();
+    }
+
+    @Test
+    public void testlistenerRuns_multipleTimes() {
+        int watchedSteinsGate = getNextStateFlag();
+
+        mMultiStateCallback.addChangeListener(watchedSteinsGate, mListener);
+        mMultiStateCallback.setState(watchedSteinsGate);
+
+        // Called exactly one
+        verify(mListener, times(1)).accept(anyBoolean());
+        // Called exactly once with isOn = true
+        verify(mListener, times(1)).accept(eq(true));
+        // Never called with isOn = false
+        verify(mListener, times(0)).accept(eq(false));
+
+        mMultiStateCallback.clearState(watchedSteinsGate);
+
+        // Called exactly twice
+        verify(mListener, times(2)).accept(anyBoolean());
+        // Called exactly once with isOn = true
+        verify(mListener, times(1)).accept(eq(true));
+        // Called exactly once with isOn = false
+        verify(mListener, times(1)).accept(eq(false));
+    }
+
+    @Test
+    public void testlistenerDoesNotRun_forUnchangedState() {
+        int watchedSteinsGate = getNextStateFlag();
+
+        mMultiStateCallback.addChangeListener(watchedSteinsGate, mListener);
+        mMultiStateCallback.setState(watchedSteinsGate);
+        mMultiStateCallback.setState(watchedSteinsGate);
+
+        // State remained unchanged
+        verify(mListener, times(1)).accept(anyBoolean());
+        // Called exactly once with isOn = true
+        verify(mListener, times(1)).accept(eq(true));
+    }
+
+    private static void runOnMainSync(Runnable runnable) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
new file mode 100644
index 0000000..80b9489
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
@@ -0,0 +1,452 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.inputconsumers;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_HOVER_ENTER;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
+import com.android.quickstep.DeviceConfigWrapper;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.NavHandle;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.TopTaskTracker;
+import com.android.quickstep.util.TestExtensions;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NavHandleLongPressInputConsumerTest {
+
+    private static final float TOUCH_SLOP = 10;
+    private static final float SQUARED_TOUCH_SLOP = 100;
+
+    private final AtomicBoolean mLongPressTriggered = new AtomicBoolean();
+    private final Runnable mLongPressRunnable = () -> mLongPressTriggered.set(true);
+    private NavHandleLongPressInputConsumer mUnderTest;
+    private SandboxContext mContext;
+    private float mScreenWidth;
+    @Mock InputConsumer mDelegate;
+    @Mock InputMonitorCompat mInputMonitor;
+    @Mock RecentsAnimationDeviceState mDeviceState;
+    @Mock NavHandle mNavHandle;
+    @Mock GestureState mGestureState;
+    @Mock NavHandleLongPressHandler mNavHandleLongPressHandler;
+    @Mock TopTaskTracker mTopTaskTracker;
+    @Mock TopTaskTracker.CachedTaskInfo mTaskInfo;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mTopTaskTracker.getCachedTopTask(anyBoolean())).thenReturn(mTaskInfo);
+        when(mDeviceState.getSquaredTouchSlop()).thenReturn(SQUARED_TOUCH_SLOP);
+        when(mDelegate.allowInterceptByParent()).thenReturn(true);
+        MAIN_EXECUTOR.getHandler().removeCallbacks(mLongPressRunnable);
+        mLongPressTriggered.set(false);
+        when(mNavHandleLongPressHandler.getLongPressRunnable(any())).thenReturn(mLongPressRunnable);
+        initializeObjectUnderTest();
+    }
+
+    @After
+    public void tearDown() {
+        mContext.onDestroy();
+    }
+
+    @Test
+    public void testGetType() {
+        assertThat(mUnderTest.getType() & InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS).isNotEqualTo(0);
+    }
+
+    @Test
+    public void testDelegateDisallowsTouchIntercept() {
+        when(mDelegate.allowInterceptByParent()).thenReturn(false);
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+
+        verify(mDelegate).onMotionEvent(any());
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        verify(mNavHandleLongPressHandler, never()).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testDelegateDisallowsTouchInterceptAfterTouchDown() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+
+        // Delegate should still get touches unless long press is triggered.
+        verify(mDelegate).onMotionEvent(any());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+
+        when(mDelegate.allowInterceptByParent()).thenReturn(false);
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_MOVE));
+
+        // Delegate should still get motion events unless long press is triggered.
+        verify(mDelegate, times(2)).onMotionEvent(any());
+        // But our handler should be cancelled.
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressTriggered() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
+        assertTrue(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressTriggeredWithSlightVerticalMovement() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
+                -(TOUCH_SLOP - 1)));
+        SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
+        assertTrue(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressTriggeredWithSlightHorizontalMovement() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
+                mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
+        SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
+        assertTrue(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressTriggeredWithExtendedTwoStageDuration() {
+        try (AutoCloseable flag = overrideTwoStageFlag(true)) {
+            // Reinitialize to pick up updated flag state.
+            initializeObjectUnderTest();
+
+            mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+            mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
+                    mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
+            // We have entered the second stage, so the normal timeout shouldn't trigger.
+            SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+            assertFalse(mLongPressTriggered.get());
+            verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+            verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+
+            // After an extended time, the long press should trigger.
+            float extendedDurationMultiplier =
+                    (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
+            SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout()
+                    * (extendedDurationMultiplier - 1)));  // -1 because we already waited 1x
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
+            assertTrue(mLongPressTriggered.get());
+            verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+            verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void testLongPressTriggeredWithNormalDurationInFirstStage() {
+        try (AutoCloseable flag = overrideTwoStageFlag(true)) {
+            // Reinitialize to pick up updated flag state.
+            initializeObjectUnderTest();
+
+            mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+            // We have not entered the second stage, so the normal timeout should trigger.
+            SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
+            assertTrue(mLongPressTriggered.get());
+            verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+            verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void testLongPressAbortedByTouchUp() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        SystemClock.sleep(ViewConfiguration.getLongPressTimeout() - 10);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP));
+        // Wait past the long press timeout, to be extra sure it wouldn't have triggered.
+        SystemClock.sleep(20);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressAbortedByTouchCancel() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        SystemClock.sleep(ViewConfiguration.getLongPressTimeout() - 10);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_CANCEL));
+        // Wait past the long press timeout, to be extra sure it wouldn't have triggered.
+        SystemClock.sleep(20);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressAbortedByTouchSlopPassedVertically() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        SystemClock.sleep(ViewConfiguration.getLongPressTimeout() - 10);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+
+        mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
+                -(TOUCH_SLOP + 1)));
+        // Wait past the long press timeout, to be extra sure it wouldn't have triggered.
+        SystemClock.sleep(20);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressAbortedByTouchSlopPassedHorizontally() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        SystemClock.sleep(ViewConfiguration.getLongPressTimeout() - 10);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+
+        mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
+                mScreenWidth / 2f - (TOUCH_SLOP + 1), 0));
+        // Wait past the long press timeout, to be extra sure it wouldn't have triggered.
+        SystemClock.sleep(20);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testLongPressAbortedByTouchSlopPassedVertically_twoStageEnabled() {
+        try (AutoCloseable flag = overrideTwoStageFlag(true)) {
+            // Reinitialize to pick up updated flag state.
+            initializeObjectUnderTest();
+
+            mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+            // Enter the second stage.
+            mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
+                    -(TOUCH_SLOP - 1)));
+            // Normal duration shouldn't trigger.
+            SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+            assertFalse(mLongPressTriggered.get());
+
+            // Move out of the second stage.
+            mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
+                    -(TOUCH_SLOP + 1)));
+            // Wait past the extended long press timeout, to be sure it wouldn't have triggered.
+            float extendedDurationMultiplier =
+                    (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
+            SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout()
+                    * (extendedDurationMultiplier - 1)));  // -1 because we already waited 1x
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+            assertFalse(mLongPressTriggered.get());
+            verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+            // Touch cancelled.
+            verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void testLongPressAbortedByTouchSlopPassedHorizontally_twoStageEnabled() {
+        try (AutoCloseable flag = overrideTwoStageFlag(true)) {
+            // Reinitialize to pick up updated flag state.
+            initializeObjectUnderTest();
+
+            mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+            // Enter the second stage.
+            mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
+                    mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
+            // Normal duration shouldn't trigger.
+            SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+            assertFalse(mLongPressTriggered.get());
+
+            // Move out of the second stage.
+            mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
+                    mScreenWidth / 2f - (TOUCH_SLOP + 1), 0));
+            // Wait past the extended long press timeout, to be sure it wouldn't have triggered.
+            float extendedDurationMultiplier =
+                    (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
+            SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout()
+                    * (extendedDurationMultiplier - 1)));  // -1 because we already waited 1x
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+            assertFalse(mLongPressTriggered.get());
+            verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
+            // Touch cancelled.
+            verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void testTouchOutsideNavHandleIgnored() {
+        // Touch the far left side of the screen. (y=0 is top of navbar region, picked arbitrarily)
+        mUnderTest.onMotionEvent(generateMotionEvent(ACTION_DOWN, 0, 0));
+        SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        // Should be ignored because the x position was not centered in the navbar region.
+        assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+        assertFalse(mLongPressTriggered.get());
+        verify(mNavHandleLongPressHandler, never()).onTouchStarted(any());
+        verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+    }
+
+    @Test
+    public void testHoverPassedToDelegate() {
+        // Regardless of whether the delegate wants us to intercept, we tell it about hover events.
+        when(mDelegate.allowInterceptByParent()).thenReturn(false);
+        mUnderTest.onHoverEvent(generateCenteredMotionEvent(ACTION_HOVER_ENTER));
+
+        verify(mDelegate).onHoverEvent(any());
+
+        when(mDelegate.allowInterceptByParent()).thenReturn(true);
+        mUnderTest.onHoverEvent(generateCenteredMotionEvent(ACTION_HOVER_ENTER));
+
+        verify(mDelegate, times(2)).onHoverEvent(any());
+    }
+
+    private void initializeObjectUnderTest() {
+        if (mContext != null) {
+            mContext.onDestroy();
+        }
+        mContext = new SandboxContext(getApplicationContext());
+        mContext.putObject(TopTaskTracker.INSTANCE, mTopTaskTracker);
+        mScreenWidth = DisplayController.INSTANCE.get(mContext).getInfo().currentSize.x;
+        mUnderTest = new NavHandleLongPressInputConsumer(mContext, mDelegate, mInputMonitor,
+                mDeviceState, mNavHandle, mGestureState);
+        mUnderTest.setNavHandleLongPressHandler(mNavHandleLongPressHandler);
+    }
+
+    /** Generate a motion event centered horizontally in the screen. */
+    private MotionEvent generateCenteredMotionEvent(int motionAction) {
+        return generateCenteredMotionEventWithYOffset(motionAction, 0);
+    }
+
+    /** Generate a motion event centered horizontally in the screen, with y offset. */
+    private MotionEvent generateCenteredMotionEventWithYOffset(int motionAction, float y) {
+        return generateMotionEvent(motionAction, mScreenWidth / 2f, y);
+    }
+
+    private static MotionEvent generateMotionEvent(int motionAction, float x, float y) {
+        return MotionEvent.obtain(0, 0, motionAction, x, y, 0);
+    }
+
+    private static AutoCloseable overrideTwoStageFlag(boolean value) {
+        return TestExtensions.overrideNavConfigFlag(
+                "ENABLE_LPNH_TWO_STAGES",
+                value,
+                () -> DeviceConfigWrapper.get().getEnableLpnhTwoStages());
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
new file mode 100644
index 0000000..ea2e484
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.logging
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.THEMED_ICONS
+import com.android.launcher3.LauncherPrefs.Companion.backedUpItem
+import com.android.launcher3.logging.InstanceId
+import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALL_APPS_SUGGESTIONS_ENABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_ROTATION_DISABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_ROTATION_ENABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_DISABLED
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class SettingsChangeLoggerTest {
+    private val mContext: Context = ApplicationProvider.getApplicationContext()
+
+    private val mInstanceId = InstanceId.fakeInstanceId(1)
+
+    private lateinit var mSystemUnderTest: SettingsChangeLogger
+
+    @Mock private lateinit var mStatsLogManager: StatsLogManager
+
+    @Mock private lateinit var mMockLogger: StatsLogManager.StatsLogger
+
+    @Captor private lateinit var mEventCaptor: ArgumentCaptor<StatsLogManager.EventEnum>
+
+    private var mDefaultThemedIcons = false
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(mStatsLogManager.logger()).doReturn(mMockLogger)
+        whenever(mStatsLogManager.logger().withInstanceId(any())).doReturn(mMockLogger)
+        mDefaultThemedIcons = LauncherPrefs.get(mContext).get(THEMED_ICONS)
+        // To match the default value of THEMED_ICONS
+        LauncherPrefs.get(mContext).put(THEMED_ICONS, false)
+
+        mSystemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager)
+    }
+
+    @After
+    fun tearDown() {
+        LauncherPrefs.get(mContext).put(THEMED_ICONS, mDefaultThemedIcons)
+        mSystemUnderTest.close()
+    }
+
+    @Test
+    fun loggingPrefs_correctDefaultValue() {
+        assertThat(mSystemUnderTest.loggingPrefs["pref_allowRotation"]!!.defaultValue).isFalse()
+        assertThat(mSystemUnderTest.loggingPrefs["pref_add_icon_to_home"]!!.defaultValue).isTrue()
+        assertThat(mSystemUnderTest.loggingPrefs["pref_overview_action_suggestions"]!!.defaultValue)
+            .isTrue()
+        assertThat(mSystemUnderTest.loggingPrefs["pref_smartspace_home_screen"]!!.defaultValue)
+            .isTrue()
+        assertThat(mSystemUnderTest.loggingPrefs["pref_enable_minus_one"]!!.defaultValue).isTrue()
+    }
+
+    @Test
+    fun logSnapshot_defaultValue() {
+        mSystemUnderTest.logSnapshot(mInstanceId)
+
+        verify(mMockLogger, atLeastOnce()).log(mEventCaptor.capture())
+        val capturedEvents = mEventCaptor.allValues
+        assertThat(capturedEvents.isNotEmpty()).isTrue()
+        verifyDefaultEvent(capturedEvents)
+        // pref_allowRotation false
+        assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_ROTATION_DISABLED.id })
+            .isTrue()
+    }
+
+    @Test
+    fun logSnapshot_updateValue() {
+        LauncherPrefs.get(mContext)
+            .put(
+                item =
+                    backedUpItem(
+                        sharedPrefKey = "pref_allowRotation",
+                        defaultValue = false,
+                    ),
+                value = true
+            )
+
+        mSystemUnderTest.logSnapshot(mInstanceId)
+
+        verify(mMockLogger, atLeastOnce()).log(mEventCaptor.capture())
+        val capturedEvents = mEventCaptor.allValues
+        assertThat(capturedEvents.isNotEmpty()).isTrue()
+        verifyDefaultEvent(capturedEvents)
+        assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_ROTATION_ENABLED.id })
+            .isTrue()
+    }
+
+    private fun verifyDefaultEvent(capturedEvents: MutableList<StatsLogManager.EventEnum>) {
+        assertThat(capturedEvents.any { it.id == LAUNCHER_NOTIFICATION_DOT_ENABLED.id }).isTrue()
+        assertThat(capturedEvents.any { it.id == LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON.id })
+            .isTrue()
+        assertThat(capturedEvents.any { it.id == LAUNCHER_THEMED_ICON_DISABLED.id }).isTrue()
+        assertThat(capturedEvents.any { it.id == LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED.id })
+            .isTrue()
+        assertThat(capturedEvents.any { it.id == LAUNCHER_ALL_APPS_SUGGESTIONS_ENABLED.id })
+            .isTrue()
+        assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED.id })
+            .isTrue()
+        // LAUNCHER_GOOGLE_APP_SWIPE_LEFT_ENABLED
+        assertThat(capturedEvents.any { it.id == 617 }).isTrue()
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
new file mode 100644
index 0000000..cdfbd16
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+class FakeRecentsDeviceProfileRepository : RecentsDeviceProfileRepository {
+    private var recentsDeviceProfile =
+        RecentsDeviceProfile(
+            isLargeScreen = false,
+            widthPx = 1080,
+            heightPx = 1920,
+        )
+
+    override fun getRecentsDeviceProfile() = recentsDeviceProfile
+
+    fun setRecentsDeviceProfile(newValue: RecentsDeviceProfile) {
+        recentsDeviceProfile = newValue
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.kt
new file mode 100644
index 0000000..c328672
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import android.view.Surface
+
+class FakeRecentsRotationStateRepository : RecentsRotationStateRepository {
+    private var recentsRotationState =
+        RecentsRotationState(
+            activityRotation = Surface.ROTATION_0,
+            orientationHandlerRotation = Surface.ROTATION_0
+        )
+
+    override fun getRecentsRotationState() = recentsRotationState
+
+    fun setRecentsRotationState(newValue: RecentsRotationState) {
+        recentsRotationState = newValue
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
index 242bc73..fee4979 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
@@ -23,10 +23,12 @@
 import com.android.systemui.shared.recents.model.Task
 import com.google.common.truth.Truth.assertThat
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 class FakeTaskIconDataSource : TaskIconDataSource {
 
-    val taskIdToDrawable: Map<Int, Drawable> = (0..10).associateWith { mock() }
+    val taskIdToDrawable: Map<Int, Drawable> = (0..10).associateWith { mockCopyableDrawable() }
+
     val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
     var shouldLoadSynchronously: Boolean = true
 
@@ -49,6 +51,17 @@
         }
         return null
     }
+
+    private fun mockCopyableDrawable(): Drawable {
+        val mutableDrawable = mock<Drawable>()
+        val immutableDrawable =
+            mock<Drawable>().apply { whenever(mutate()).thenReturn(mutableDrawable) }
+        val constantState =
+            mock<Drawable.ConstantState>().apply {
+                whenever(newDrawable()).thenReturn(immutableDrawable)
+            }
+        return mutableDrawable.apply { whenever(this.constantState).thenReturn(constantState) }
+    }
 }
 
 fun Task.assertHasIconDataFromSource(fakeTaskIconDataSource: FakeTaskIconDataSource) {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
index 19990a8..ec1da5a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
@@ -24,6 +24,7 @@
 
 class FakeTasksRepository : RecentTasksRepository {
     private var thumbnailDataMap: Map<Int, ThumbnailData> = emptyMap()
+    private var taskIconDataMap: Map<Int, TaskIconQueryResponse> = emptyMap()
     private var tasks: MutableStateFlow<List<Task>> = MutableStateFlow(emptyList())
     private var visibleTasks: MutableStateFlow<List<Int>> = MutableStateFlow(emptyList())
 
@@ -37,7 +38,17 @@
 
     override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
         visibleTasks.value = visibleTaskIdList
-        tasks.value = tasks.value.map { it.apply { thumbnail = thumbnailDataMap[it.key.id] } }
+        tasks.value =
+            tasks.value.map {
+                it.apply {
+                    thumbnail = thumbnailDataMap[it.key.id]
+                    taskIconDataMap[it.key.id].let { taskIconData ->
+                        icon = taskIconData?.icon
+                        titleDescription = taskIconData?.contentDescription
+                        title = taskIconData?.title
+                    }
+                }
+            }
     }
 
     fun seedTasks(tasks: List<Task>) {
@@ -47,4 +58,8 @@
     fun seedThumbnailData(thumbnailDataMap: Map<Int, ThumbnailData>) {
         this.thumbnailDataMap = thumbnailDataMap
     }
+
+    fun seedIconData(iconDataMap: Map<Int, TaskIconQueryResponse>) {
+        this.taskIconDataMap = iconDataMap
+    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt
new file mode 100644
index 0000000..e74fe4b
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.FakeInvariantDeviceProfileTest
+import com.android.quickstep.views.RecentsViewContainer
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Test for [RecentsDeviceProfileRepositoryImpl] */
+@RunWith(AndroidJUnit4::class)
+class RecentsDeviceProfileRepositoryImplTest : FakeInvariantDeviceProfileTest() {
+    private val recentsViewContainer = mock<RecentsViewContainer>()
+
+    private val systemUnderTest = RecentsDeviceProfileRepositoryImpl(recentsViewContainer)
+
+    @Test
+    fun deviceProfileMappedCorrectly() {
+        initializeVarsForTablet()
+        val tabletDeviceProfile = newDP()
+        whenever(recentsViewContainer.deviceProfile).thenReturn(tabletDeviceProfile)
+
+        assertThat(systemUnderTest.getRecentsDeviceProfile())
+            .isEqualTo(RecentsDeviceProfile(isLargeScreen = true, widthPx = 1600, heightPx = 2560))
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.kt
new file mode 100644
index 0000000..017f037
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import android.view.Surface.ROTATION_270
+import android.view.Surface.ROTATION_90
+import com.android.quickstep.orientation.SeascapePagedViewHandler
+import com.android.quickstep.util.RecentsOrientedState
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Test for [RecentsRotationStateRepositoryImpl] */
+class RecentsRotationStateRepositoryImplTest {
+    private val recentsOrientedState = mock<RecentsOrientedState>()
+
+    private val systemUnderTest = RecentsRotationStateRepositoryImpl(recentsOrientedState)
+
+    @Test
+    fun orientedStateMappedCorrectly() {
+        whenever(recentsOrientedState.recentsActivityRotation).thenReturn(ROTATION_90)
+        whenever(recentsOrientedState.orientationHandler).thenReturn(SeascapePagedViewHandler())
+
+        assertThat(systemUnderTest.getRecentsRotationState())
+            .isEqualTo(
+                RecentsRotationState(
+                    activityRotation = ROTATION_90,
+                    orientationHandlerRotation = ROTATION_270
+                )
+            )
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
new file mode 100644
index 0000000..02f1d11
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.usecase
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.view.Surface.ROTATION_90
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository
+import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/** Test for [GetThumbnailPositionUseCase] */
+@RunWith(AndroidJUnit4::class)
+class GetThumbnailPositionUseCaseTest {
+    private val task =
+        Task(Task.TaskKey(TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+            colorBackground = Color.BLACK
+        }
+    private val thumbnailData =
+        ThumbnailData(
+            thumbnail =
+                mock<Bitmap>().apply {
+                    whenever(width).thenReturn(THUMBNAIL_WIDTH)
+                    whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+                }
+        )
+
+    private val deviceProfileRepository = FakeRecentsDeviceProfileRepository()
+    private val rotationStateRepository = FakeRecentsRotationStateRepository()
+    private val tasksRepository = FakeTasksRepository()
+    private val previewPositionHelper = mock<PreviewPositionHelper>()
+
+    private val systemUnderTest =
+        GetThumbnailPositionUseCase(
+            deviceProfileRepository,
+            rotationStateRepository,
+            tasksRepository,
+            previewPositionHelper
+        )
+
+    @Test
+    fun invisibleTask_returnsIdentityMatrix() = runTest {
+        tasksRepository.seedTasks(listOf(task))
+
+        assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
+            .isInstanceOf(MissingThumbnail::class.java)
+    }
+
+    @Test
+    fun visibleTaskWithoutThumbnailData_returnsIdentityMatrix() = runTest {
+        tasksRepository.seedTasks(listOf(task))
+        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+
+        assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
+            .isInstanceOf(MissingThumbnail::class.java)
+    }
+
+    @Test
+    fun visibleTaskWithThumbnailData_returnsTransformedMatrix() = runTest {
+        tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+        tasksRepository.seedTasks(listOf(task))
+        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+
+        val isLargeScreen = true
+        deviceProfileRepository.setRecentsDeviceProfile(
+            deviceProfileRepository.getRecentsDeviceProfile().copy(isLargeScreen = isLargeScreen)
+        )
+        val activityRotation = ROTATION_90
+        rotationStateRepository.setRecentsRotationState(
+            rotationStateRepository
+                .getRecentsRotationState()
+                .copy(activityRotation = activityRotation)
+        )
+        val isRtl = true
+        val isRotated = true
+
+        whenever(previewPositionHelper.matrix).thenReturn(MATRIX)
+        whenever(previewPositionHelper.isOrientationChanged).thenReturn(isRotated)
+
+        assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+            .isEqualTo(MatrixScaling(MATRIX, isRotated))
+
+        verify(previewPositionHelper)
+            .updateThumbnailMatrix(
+                Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT),
+                thumbnailData,
+                CANVAS_WIDTH,
+                CANVAS_HEIGHT,
+                isLargeScreen,
+                activityRotation,
+                isRtl
+            )
+    }
+
+    companion object {
+        const val TASK_ID = 2
+        const val THUMBNAIL_WIDTH = 100
+        const val THUMBNAIL_HEIGHT = 200
+        const val CANVAS_WIDTH = 300
+        const val CANVAS_HEIGHT = 600
+        val MATRIX =
+            Matrix().apply {
+                setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
+            }
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/util/GetThumbnailUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
similarity index 98%
rename from quickstep/tests/multivalentTests/src/com/android/quickstep/task/util/GetThumbnailUseCaseTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
index 414f8ca..12a94cf 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/util/GetThumbnailUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.quickstep.task.util
+package com.android.quickstep.recents.usecase
 
 import android.content.ComponentName
 import android.content.Intent
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
new file mode 100644
index 0000000..ba4e206
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.usecase
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Test for [SysUiStatusNavFlagsUseCase] */
+class SysUiStatusNavFlagsUseCaseTest {
+    private lateinit var tasksRepository: FakeTasksRepository
+    private lateinit var sysUiStatusNavFlagsUseCase: SysUiStatusNavFlagsUseCase
+
+    @Before
+    fun setup() {
+        tasksRepository = FakeTasksRepository()
+        sysUiStatusNavFlagsUseCase = SysUiStatusNavFlagsUseCase(tasksRepository)
+        initTaskRepository()
+    }
+
+    @Test
+    fun onLightAppearanceReturnExpectedFlags() {
+        assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(FIRST_TASK_ID))
+            .isEqualTo(FLAGS_APPEARANCE_LIGHT_THEME)
+    }
+
+    @Test
+    fun onDarkAppearanceReturnExpectedFlags() {
+        assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(SECOND_TASK_ID))
+            .isEqualTo(FLAGS_APPEARANCE_DARK_THEME)
+    }
+
+    @Test
+    fun whenThumbnailIsNullReturnDefault() {
+        assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(UNKNOWN_TASK_ID))
+            .isEqualTo(FLAGS_DEFAULT)
+    }
+
+    private fun initTaskRepository() {
+        val firstTask =
+            Task(Task.TaskKey(FIRST_TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+                colorBackground = Color.BLACK
+            }
+        val firstThumbnailData =
+            ThumbnailData(
+                thumbnail =
+                    mock<Bitmap>().apply {
+                        whenever(width).thenReturn(THUMBNAIL_WIDTH)
+                        whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+                    },
+                appearance = APPEARANCE_LIGHT_THEME
+            )
+
+        val secondTask =
+            Task(Task.TaskKey(SECOND_TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2005)).apply {
+                colorBackground = Color.BLACK
+            }
+        val secondThumbnailData =
+            ThumbnailData(
+                thumbnail =
+                    mock<Bitmap>().apply {
+                        whenever(width).thenReturn(THUMBNAIL_WIDTH)
+                        whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+                    },
+                appearance = APPEARANCE_DARK_THEME
+            )
+
+        tasksRepository.seedTasks(listOf(firstTask, secondTask))
+        tasksRepository.seedThumbnailData(
+            mapOf(FIRST_TASK_ID to firstThumbnailData, SECOND_TASK_ID to secondThumbnailData)
+        )
+        tasksRepository.setVisibleTasks(listOf(FIRST_TASK_ID, SECOND_TASK_ID))
+    }
+
+    companion object {
+        const val FIRST_TASK_ID = 0
+        const val SECOND_TASK_ID = 100
+        const val UNKNOWN_TASK_ID = 404
+        const val THUMBNAIL_WIDTH = 100
+        const val THUMBNAIL_HEIGHT = 200
+        const val APPEARANCE_LIGHT_THEME = 24
+        const val FLAGS_APPEARANCE_LIGHT_THEME = 5
+        const val APPEARANCE_DARK_THEME = 0
+        const val FLAGS_APPEARANCE_DARK_THEME = 10
+        const val FLAGS_DEFAULT = 0
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCaseTest.kt
new file mode 100644
index 0000000..13e8b09
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCaseTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.graphics.Point
+import android.graphics.drawable.Drawable
+import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository
+import com.android.quickstep.task.viewmodel.TaskViewData
+import com.android.quickstep.views.TaskViewType
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class GetSplashSizeUseCaseTest {
+    private val taskThumbnailViewData = TaskThumbnailViewData()
+    private val taskViewData = TaskViewData(TaskViewType.SINGLE)
+    private val recentsDeviceProfileRepository = FakeRecentsDeviceProfileRepository()
+    private val systemUnderTest =
+        GetSplashSizeUseCase(taskThumbnailViewData, taskViewData, recentsDeviceProfileRepository)
+
+    @Test
+    fun execute_whenNoScaleRequired_returnsIntrinsicSize() {
+        taskThumbnailViewData.width.value =
+            recentsDeviceProfileRepository.getRecentsDeviceProfile().widthPx
+        taskThumbnailViewData.height.value =
+            recentsDeviceProfileRepository.getRecentsDeviceProfile().heightPx
+
+        assertThat(systemUnderTest.execute(createIcon(100, 100))).isEqualTo(Point(100, 100))
+    }
+
+    @Test
+    fun execute_whenThumbnailViewIsSmallerThanScreen_returnsScaledSize() {
+        taskThumbnailViewData.width.value =
+            recentsDeviceProfileRepository.getRecentsDeviceProfile().widthPx / 2
+        taskThumbnailViewData.height.value =
+            recentsDeviceProfileRepository.getRecentsDeviceProfile().heightPx / 2
+
+        assertThat(systemUnderTest.execute(createIcon(100, 100))).isEqualTo(Point(50, 50))
+    }
+
+    @Test
+    fun execute_whenThumbnailViewIsSmallerThanScreen_withNonGridScale_returnsScaledSize() {
+        taskThumbnailViewData.width.value =
+            recentsDeviceProfileRepository.getRecentsDeviceProfile().widthPx / 2
+        taskThumbnailViewData.height.value =
+            recentsDeviceProfileRepository.getRecentsDeviceProfile().heightPx / 2
+        taskViewData.nonGridScale.value = 2f
+
+        assertThat(systemUnderTest.execute(createIcon(100, 100))).isEqualTo(Point(25, 25))
+    }
+
+    @Test
+    fun execute_whenThumbnailViewIsSmallerThanScreen_withThumbnailViewScale_returnsScaledSize() {
+        taskThumbnailViewData.width.value =
+            recentsDeviceProfileRepository.getRecentsDeviceProfile().widthPx / 2
+        taskThumbnailViewData.height.value =
+            recentsDeviceProfileRepository.getRecentsDeviceProfile().heightPx / 2
+        taskThumbnailViewData.scaleX.value = 2f
+        taskThumbnailViewData.scaleY.value = 2f
+
+        assertThat(systemUnderTest.execute(createIcon(100, 100))).isEqualTo(Point(25, 25))
+    }
+
+    private fun createIcon(width: Int, height: Int): Drawable =
+        mock<Drawable>().apply {
+            whenever(intrinsicWidth).thenReturn(width)
+            whenever(intrinsicHeight).thenReturn(height)
+        }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
new file mode 100644
index 0000000..e083046
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.view.Surface
+import android.view.Surface.ROTATION_90
+import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.data.TaskIconQueryResponse
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class SplashAlphaUseCaseTest {
+    private val recentsViewData = RecentsViewData()
+    private val taskContainerData = TaskContainerData()
+    private val taskThumbnailViewData = TaskThumbnailViewData()
+    private val recentTasksRepository = FakeTasksRepository()
+    private val recentsRotationStateRepository = FakeRecentsRotationStateRepository()
+    private val systemUnderTest =
+        SplashAlphaUseCase(
+            recentsViewData,
+            taskContainerData,
+            taskThumbnailViewData,
+            recentTasksRepository,
+            recentsRotationStateRepository
+        )
+
+    @Test
+    fun execute_withNullThumbnail_showsSplash() = runTest {
+        assertThat(systemUnderTest.execute(0).first()).isEqualTo(SPLASH_SHOWN)
+    }
+
+    @Test
+    fun execute_withTaskSpecificSplashAlpha_showsSplash() = runTest {
+        setupTask(2)
+        taskContainerData.thumbnailSplashProgress.value = 0.7f
+
+        assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.7f)
+    }
+
+    @Test
+    fun execute_withNoGlobalSplashEnabled_doesntShowSplash() = runTest {
+        setupTask(2)
+
+        assertThat(systemUnderTest.execute(2).first()).isEqualTo(SPLASH_HIDDEN)
+    }
+
+    @Test
+    fun execute_withSameAspectRatioAndRotation_withGlobalSplashEnabled_doesntShowSplash() =
+        runTest {
+            setupTask(2)
+            recentsViewData.thumbnailSplashProgress.value = 0.5f
+            taskThumbnailViewData.width.value = THUMBNAIL_WIDTH * 2
+            taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
+
+            assertThat(systemUnderTest.execute(2).first()).isEqualTo(SPLASH_HIDDEN)
+        }
+
+    @Test
+    fun execute_withDifferentAspectRatioAndSameRotation_showsSplash() = runTest {
+        setupTask(2)
+        recentsViewData.thumbnailSplashProgress.value = 0.5f
+        taskThumbnailViewData.width.value = THUMBNAIL_WIDTH
+        taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
+
+        assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
+    }
+
+    @Test
+    fun execute_withSameAspectRatioAndDifferentRotation_showsSplash() = runTest {
+        setupTask(2, createThumbnailData(rotation = ROTATION_90))
+        recentsViewData.thumbnailSplashProgress.value = 0.5f
+        taskThumbnailViewData.width.value = THUMBNAIL_WIDTH * 2
+        taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
+
+        assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
+    }
+
+    @Test
+    fun execute_withDifferentAspectRatioAndRotation_showsSplash() = runTest {
+        setupTask(2, createThumbnailData(rotation = ROTATION_90))
+        recentsViewData.thumbnailSplashProgress.value = 0.5f
+        taskThumbnailViewData.width.value = THUMBNAIL_WIDTH
+        taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
+
+        assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
+    }
+
+    private val tasks = (0..5).map(::createTaskWithId)
+
+    private fun setupTask(taskId: Int, thumbnailData: ThumbnailData = createThumbnailData()) {
+        recentTasksRepository.seedThumbnailData(mapOf(taskId to thumbnailData))
+        val expectedIconData = createIconData("Task $taskId")
+        recentTasksRepository.seedIconData(mapOf(taskId to expectedIconData))
+        recentTasksRepository.seedTasks(tasks)
+        recentTasksRepository.setVisibleTasks(listOf(taskId))
+    }
+
+    private fun createThumbnailData(
+        rotation: Int = Surface.ROTATION_0,
+        width: Int = THUMBNAIL_WIDTH,
+        height: Int = THUMBNAIL_HEIGHT
+    ): ThumbnailData {
+        val bitmap = mock<Bitmap>()
+        whenever(bitmap.width).thenReturn(width)
+        whenever(bitmap.height).thenReturn(height)
+
+        return ThumbnailData(thumbnail = bitmap, rotation = rotation)
+    }
+
+    private fun createIconData(title: String) = TaskIconQueryResponse(mock<Drawable>(), "", title)
+
+    private fun createTaskWithId(taskId: Int) =
+        Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+            colorBackground = Color.argb(taskId, taskId, taskId, taskId)
+        }
+
+    companion object {
+        const val THUMBNAIL_WIDTH = 100
+        const val THUMBNAIL_HEIGHT = 200
+
+        const val SPLASH_HIDDEN = 0f
+        const val SPLASH_SHOWN = 1f
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
index b78f871..877528e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
@@ -20,15 +20,25 @@
 import android.content.Intent
 import android.graphics.Bitmap
 import android.graphics.Color
-import android.graphics.Rect
+import android.graphics.Matrix
+import android.graphics.Point
+import android.graphics.drawable.Drawable
+import android.view.Surface
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.data.TaskIconQueryResponse
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Splash
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
 import com.android.quickstep.task.viewmodel.TaskViewData
 import com.android.quickstep.views.TaskViewType
 import com.android.systemui.shared.recents.model.Task
@@ -36,8 +46,10 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
@@ -48,12 +60,28 @@
     private val taskViewData by lazy { TaskViewData(taskViewType) }
     private val taskContainerData = TaskContainerData()
     private val tasksRepository = FakeTasksRepository()
+    private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
+    private val splashAlphaUseCase: SplashAlphaUseCase = mock()
+    private val getSplashSizeUseCase: GetSplashSizeUseCase = mock()
     private val systemUnderTest by lazy {
-        TaskThumbnailViewModel(recentsViewData, taskViewData, taskContainerData, tasksRepository)
+        TaskThumbnailViewModel(
+            recentsViewData,
+            taskViewData,
+            taskContainerData,
+            tasksRepository,
+            mGetThumbnailPositionUseCase,
+            splashAlphaUseCase,
+            getSplashSizeUseCase,
+        )
     }
 
     private val tasks = (0..5).map(::createTaskWithId)
 
+    @Before
+    fun setUp() {
+        whenever(getSplashSizeUseCase.execute(any())).thenReturn(Point())
+    }
+
     @Test
     fun initialStateIsUninitialized() = runTest {
         assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
@@ -136,9 +164,11 @@
     }
 
     @Test
-    fun bindStoppedTaskWithThumbnail_thenStateIs_Snapshot_withAlphaRemoved() = runTest {
-        val expectedThumbnailData = createThumbnailData()
+    fun bindStoppedTaskWithThumbnail_thenStateIs_SnapshotSplash_withAlphaRemoved() = runTest {
+        val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270)
         tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData))
+        val expectedIconData = createIconData("Task 2")
+        tasksRepository.seedIconData(mapOf(2 to expectedIconData))
         tasksRepository.seedTasks(tasks)
         tasksRepository.setVisibleTasks(listOf(2))
         val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
@@ -146,18 +176,23 @@
         systemUnderTest.bind(recentTask)
         assertThat(systemUnderTest.uiState.first())
             .isEqualTo(
-                Snapshot(
-                    backgroundColor = Color.rgb(2, 2, 2),
-                    bitmap = expectedThumbnailData.thumbnail!!,
-                    drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
+                SnapshotSplash(
+                    Snapshot(
+                        backgroundColor = Color.rgb(2, 2, 2),
+                        bitmap = expectedThumbnailData.thumbnail!!,
+                        thumbnailRotation = Surface.ROTATION_270,
+                    ),
+                    Splash(expectedIconData.icon, Point())
                 )
             )
     }
 
     @Test
-    fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshot() = runTest {
+    fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshotSplash() = runTest {
         val expectedThumbnailData = createThumbnailData()
         tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData))
+        val expectedIconData = createIconData("Task 2")
+        tasksRepository.seedIconData(mapOf(2 to expectedIconData))
         tasksRepository.seedTasks(tasks)
         val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
 
@@ -167,29 +202,106 @@
         tasksRepository.setVisibleTasks(listOf(2))
         assertThat(systemUnderTest.uiState.first())
             .isEqualTo(
-                Snapshot(
-                    backgroundColor = Color.rgb(2, 2, 2),
-                    bitmap = expectedThumbnailData.thumbnail!!,
-                    drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
+                SnapshotSplash(
+                    Snapshot(
+                        backgroundColor = Color.rgb(2, 2, 2),
+                        bitmap = expectedThumbnailData.thumbnail!!,
+                        thumbnailRotation = Surface.ROTATION_0,
+                    ),
+                    Splash(expectedIconData.icon, Point())
                 )
             )
     }
 
+    @Test
+    fun bindStoppedTask_thenStateContainsSplashSizeFromUseCase() = runTest {
+        val expectedSplashSize = Point(100, 150)
+        whenever(getSplashSizeUseCase.execute(any())).thenReturn(expectedSplashSize)
+        val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270)
+        tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData))
+        val expectedIconData = createIconData("Task 2")
+        tasksRepository.seedIconData(mapOf(2 to expectedIconData))
+        tasksRepository.seedTasks(tasks)
+        tasksRepository.setVisibleTasks(listOf(2))
+        val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
+
+        systemUnderTest.bind(recentTask)
+        val uiState = systemUnderTest.uiState.first() as SnapshotSplash
+        assertThat(uiState.splash.size).isEqualTo(expectedSplashSize)
+    }
+
+    @Test
+    fun getSnapshotMatrix_MissingThumbnail() = runTest {
+        val taskId = 2
+        val recentTask = TaskThumbnail(taskId = taskId, isRunning = false)
+        val isRtl = true
+
+        whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+            .thenReturn(MissingThumbnail)
+
+        systemUnderTest.bind(recentTask)
+        assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+            .isEqualTo(Matrix.IDENTITY_MATRIX)
+    }
+
+    @Test
+    fun getSnapshotMatrix_MatrixScaling() = runTest {
+        val taskId = 2
+        val recentTask = TaskThumbnail(taskId = taskId, isRunning = false)
+        val isRtl = true
+
+        whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+            .thenReturn(MatrixScaling(MATRIX, isRotated = false))
+
+        systemUnderTest.bind(recentTask)
+        assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+            .isEqualTo(MATRIX)
+    }
+
+    @Test
+    fun getForegroundScrimDimProgress_returnsForegroundMaxScrim() = runTest {
+        recentsViewData.tintAmount.value = 0.32f
+        taskContainerData.taskMenuOpenProgress.value = 0f
+        assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.32f)
+    }
+
+    @Test
+    fun getTaskMenuScrimDimProgress_returnsTaskMenuScrim() = runTest {
+        recentsViewData.tintAmount.value = 0f
+        taskContainerData.taskMenuOpenProgress.value = 1f
+        assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.4f)
+    }
+
+    @Test
+    fun getForegroundScrimDimProgress_returnsNoScrim() = runTest {
+        recentsViewData.tintAmount.value = 0f
+        taskContainerData.taskMenuOpenProgress.value = 0f
+        assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0f)
+    }
+
     private fun createTaskWithId(taskId: Int) =
         Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
             colorBackground = Color.argb(taskId, taskId, taskId, taskId)
         }
 
-    private fun createThumbnailData(): ThumbnailData {
+    private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData {
         val bitmap = mock<Bitmap>()
         whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH)
         whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT)
 
-        return ThumbnailData(thumbnail = bitmap)
+        return ThumbnailData(thumbnail = bitmap, rotation = rotation)
     }
 
+    private fun createIconData(title: String) = TaskIconQueryResponse(mock<Drawable>(), "", title)
+
     companion object {
         const val THUMBNAIL_WIDTH = 100
         const val THUMBNAIL_HEIGHT = 200
+        const val CANVAS_WIDTH = 300
+        const val CANVAS_HEIGHT = 600
+        val MATRIX =
+            Matrix().apply {
+                setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
+            }
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
index 40482c4..d0887df 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
@@ -23,9 +23,13 @@
 import android.graphics.Matrix
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
+import com.android.quickstep.task.viewmodel.TaskOverlayViewModel.ThumbnailPositionState
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.google.common.truth.Truth.assertThat
@@ -53,7 +57,9 @@
         )
     private val recentsViewData = RecentsViewData()
     private val tasksRepository = FakeTasksRepository()
-    private val systemUnderTest = TaskOverlayViewModel(task, recentsViewData, tasksRepository)
+    private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
+    private val systemUnderTest =
+        TaskOverlayViewModel(task, recentsViewData, mGetThumbnailPositionUseCase, tasksRepository)
 
     @Test
     fun initialStateIsDisabled() = runTest {
@@ -87,7 +93,6 @@
                 Enabled(
                     isRealSnapshot = false,
                     thumbnail = null,
-                    thumbnailMatrix = Matrix.IDENTITY_MATRIX
                 )
             )
     }
@@ -107,7 +112,6 @@
                 Enabled(
                     isRealSnapshot = true,
                     thumbnail = thumbnailData.thumbnail,
-                    thumbnailMatrix = Matrix.IDENTITY_MATRIX
                 )
             )
     }
@@ -127,7 +131,6 @@
                 Enabled(
                     isRealSnapshot = false,
                     thumbnail = thumbnailData.thumbnail,
-                    thumbnailMatrix = Matrix.IDENTITY_MATRIX
                 )
             )
     }
@@ -147,14 +150,42 @@
                 Enabled(
                     isRealSnapshot = false,
                     thumbnail = thumbnailData.thumbnail,
-                    thumbnailMatrix = Matrix.IDENTITY_MATRIX
                 )
             )
     }
 
+    @Test
+    fun getThumbnailMatrix_MissingThumbnail() = runTest {
+        val isRtl = true
+
+        whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+            .thenReturn(MissingThumbnail)
+
+        assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+            .isEqualTo(ThumbnailPositionState(Matrix.IDENTITY_MATRIX, isRotated = false))
+    }
+
+    @Test
+    fun getThumbnailMatrix_MatrixScaling() = runTest {
+        val isRtl = true
+        val isRotated = true
+
+        whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+            .thenReturn(MatrixScaling(MATRIX, isRotated))
+
+        assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+            .isEqualTo(ThumbnailPositionState(MATRIX, isRotated))
+    }
+
     companion object {
         const val TASK_ID = 0
         const val THUMBNAIL_WIDTH = 100
         const val THUMBNAIL_HEIGHT = 200
+        const val CANVAS_WIDTH = 300
+        const val CANVAS_HEIGHT = 600
+        val MATRIX =
+            Matrix().apply {
+                setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
+            }
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt
new file mode 100644
index 0000000..6c526a4
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import com.android.launcher3.BuildConfig
+import com.android.launcher3.util.SafeCloseable
+import com.android.quickstep.DeviceConfigWrapper.Companion.configHelper
+import com.android.quickstep.util.DeviceConfigHelper.Companion.prefs
+import java.util.concurrent.CountDownLatch
+import java.util.function.BooleanSupplier
+import org.junit.Assert
+import org.junit.Assume
+
+/** Helper methods for testing */
+object TestExtensions {
+
+    @JvmStatic
+    fun overrideNavConfigFlag(
+        key: String,
+        value: Boolean,
+        targetValue: BooleanSupplier
+    ): AutoCloseable {
+        Assume.assumeTrue(BuildConfig.IS_DEBUG_DEVICE)
+        if (targetValue.asBoolean == value) {
+            return AutoCloseable {}
+        }
+
+        navConfigEditWatcher().let {
+            prefs.edit().putBoolean(key, value).commit()
+            it.close()
+        }
+        Assert.assertEquals(value, targetValue.asBoolean)
+
+        val watcher = navConfigEditWatcher()
+        return AutoCloseable {
+            prefs.edit().remove(key).commit()
+            watcher.close()
+        }
+    }
+
+    private fun navConfigEditWatcher(): SafeCloseable {
+        val wait = CountDownLatch(1)
+        val listener = Runnable { wait.countDown() }
+        configHelper.addChangeListener(listener)
+
+        return SafeCloseable {
+            wait.await()
+            configHelper.removeChangeListener(listener)
+        }
+    }
+}
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 27e761a..c0ff189 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -18,9 +18,11 @@
 
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.content.ComponentName
+import android.content.Context
 import android.content.Intent
 import android.os.Process
 import android.os.UserHandle
+import android.platform.test.rule.TestWatcher
 import android.testing.AndroidTestingRunner
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION
@@ -39,12 +41,14 @@
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.junit.runner.Description
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.Mock
 import org.mockito.junit.MockitoJUnit
 import org.mockito.kotlin.any
 import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.never
 import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
@@ -53,32 +57,60 @@
 class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() {
 
     @get:Rule val mockitoRule = MockitoJUnit.rule()
+    @get:Rule
+    val disableControllerForCertainTestsWatcher =
+        object : TestWatcher() {
+            override fun starting(description: Description) {
+                // Update canShowRunningAndRecentAppsAtInit before setUp() is called for each test.
+                canShowRunningAndRecentAppsAtInit =
+                    description.methodName !in
+                        listOf(
+                            "canShowRunningAndRecentAppsAtInitIsFalse_getTasksNeverCalled",
+                        )
+            }
+        }
 
     @Mock private lateinit var mockIconCache: TaskIconCache
     @Mock private lateinit var mockRecentsModel: RecentsModel
+    @Mock private lateinit var mockContext: Context
     @Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController
 
     private var taskListChangeId: Int = 1
 
     private lateinit var recentAppsController: TaskbarRecentAppsController
-    private lateinit var recentTasksChangedListener: RecentTasksChangedListener
     private lateinit var userHandle: UserHandle
 
+    private var canShowRunningAndRecentAppsAtInit = true
+    private var recentTasksChangedListener: RecentTasksChangedListener? = null
+
     @Before
     fun setUp() {
         super.setup()
         userHandle = Process.myUserHandle()
 
         whenever(mockRecentsModel.iconCache).thenReturn(mockIconCache)
+        whenever(mockRecentsModel.unregisterRecentTasksChangedListener()).then {
+            recentTasksChangedListener = null
+            it
+        }
         recentAppsController =
-            TaskbarRecentAppsController(mockRecentsModel) { mockDesktopVisibilityController }
+            TaskbarRecentAppsController(mockContext, mockRecentsModel) {
+                mockDesktopVisibilityController
+            }
+        recentAppsController.canShowRunningApps = canShowRunningAndRecentAppsAtInit
+        recentAppsController.canShowRecentApps = canShowRunningAndRecentAppsAtInit
         recentAppsController.init(taskbarControllers)
-        recentAppsController.canShowRunningApps = true
-        recentAppsController.canShowRecentApps = true
 
-        val listenerCaptor = ArgumentCaptor.forClass(RecentTasksChangedListener::class.java)
-        verify(mockRecentsModel).registerRecentTasksChangedListener(listenerCaptor.capture())
-        recentTasksChangedListener = listenerCaptor.value
+        recentTasksChangedListener =
+            if (canShowRunningAndRecentAppsAtInit) {
+                val listenerCaptor = ArgumentCaptor.forClass(RecentTasksChangedListener::class.java)
+                verify(mockRecentsModel)
+                    .registerRecentTasksChangedListener(listenerCaptor.capture())
+                listenerCaptor.value
+            } else {
+                verify(mockRecentsModel, never()).registerRecentTasksChangedListener(any())
+                null
+            }
 
         // Make sure updateHotseatItemInfos() is called after commitRunningAppsToUI()
         whenever(taskbarViewController.commitRunningAppsToUI()).then {
@@ -88,6 +120,32 @@
         }
     }
 
+    // See the TestWatcher rule at the top which sets canShowRunningAndRecentAppsAtInit = false.
+    @Test
+    fun canShowRunningAndRecentAppsAtInitIsFalse_getTasksNeverCalled() {
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+            runningTasks = listOf(createTask(1, RUNNING_APP_PACKAGE_1)),
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+        )
+        verify(mockRecentsModel, never()).getTasks(any<Consumer<List<GroupTask>>>())
+    }
+
+    @Test
+    fun canShowRunningAndRecentAppsIsFalseAfterInit_getTasksOnlyCalledInInit() {
+        // getTasks() should have been called once from init().
+        verify(mockRecentsModel, times(1)).getTasks(any<Consumer<List<GroupTask>>>())
+        recentAppsController.canShowRunningApps = false
+        recentAppsController.canShowRecentApps = false
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+            runningTasks = listOf(createTask(1, RUNNING_APP_PACKAGE_1)),
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+        )
+        // Verify that getTasks() was not called again after the init().
+        verify(mockRecentsModel, times(1)).getTasks(any<Consumer<List<GroupTask>>>())
+    }
+
     @Test
     fun updateHotseatItemInfos_cantShowRunning_inDesktopMode_returnsAllHotseatItems() {
         recentAppsController.canShowRunningApps = false
@@ -518,7 +576,7 @@
         )
 
         setInDesktopMode(true)
-        recentTasksChangedListener.onRecentTasksChanged()
+        recentTasksChangedListener!!.onRecentTasksChanged()
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
         assertThat(shownPackages).containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
     }
@@ -535,7 +593,7 @@
             recentTaskPackages = recentTaskPackages
         )
         setInDesktopMode(false)
-        recentTasksChangedListener.onRecentTasksChanged()
+        recentTasksChangedListener!!.onRecentTasksChanged()
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
         // Don't expect RECENT_PACKAGE_3 because it is currently running.
         val expectedPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
@@ -705,7 +763,7 @@
             }
             .whenever(mockRecentsModel)
             .getTasks(any<Consumer<List<GroupTask>>>())
-        recentTasksChangedListener.onRecentTasksChanged()
+        recentTasksChangedListener?.onRecentTasksChanged()
     }
 
     private fun createHotseatItemsFromPackageNames(packageNames: List<String>): List<ItemInfo> {
diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
index 5d00255..c213dbb 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
@@ -16,6 +16,8 @@
 
 package com.android.quickstep;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.TestCase.assertNull;
 
 import static org.junit.Assert.assertEquals;
@@ -27,6 +29,7 @@
 
 import android.app.ActivityManager;
 import android.app.KeyguardManager;
+import android.content.Context;
 
 import androidx.test.filters.SmallTest;
 
@@ -52,7 +55,9 @@
 public class RecentTasksListTest {
 
     @Mock
-    private SystemUiProxy mockSystemUiProxy;
+    private Context mContext;
+    @Mock
+    private SystemUiProxy mSystemUiProxy;
     @Mock
     private TopTaskTracker mTopTaskTracker;
 
@@ -64,22 +69,22 @@
         MockitoAnnotations.initMocks(this);
         LooperExecutor mockMainThreadExecutor = mock(LooperExecutor.class);
         KeyguardManager mockKeyguardManager = mock(KeyguardManager.class);
-        mRecentTasksList = new RecentTasksList(mockMainThreadExecutor, mockKeyguardManager,
-                mockSystemUiProxy, mTopTaskTracker);
+        mRecentTasksList = new RecentTasksList(mContext, mockMainThreadExecutor,
+                mockKeyguardManager, mSystemUiProxy, mTopTaskTracker);
     }
 
     @Test
-    public void onRecentTasksChanged_doesNotFetchTasks() {
+    public void onRecentTasksChanged_doesNotFetchTasks() throws Exception {
         mRecentTasksList.onRecentTasksChanged();
-        verify(mockSystemUiProxy, times(0))
+        verify(mSystemUiProxy, times(0))
                 .getRecentTasks(anyInt(), anyInt());
     }
 
     @Test
-    public void loadTasksInBackground_onlyKeys_noValidTaskDescription() {
+    public void loadTasksInBackground_onlyKeys_noValidTaskDescription() throws Exception  {
         GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(
                 new ActivityManager.RecentTaskInfo(), new ActivityManager.RecentTaskInfo(), null);
-        when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+        when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
         List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1,
@@ -91,7 +96,19 @@
     }
 
     @Test
-    public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() {
+    public void loadTasksInBackground_GetRecentTasksException() throws Exception  {
+        when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+                .thenThrow(new SystemUiProxy.GetRecentTasksException("task load failed"));
+
+        RecentTasksList.TaskLoadResult taskList = mRecentTasksList.loadTasksInBackground(
+                Integer.MAX_VALUE, -1, false);
+
+        assertThat(taskList.mRequestId).isEqualTo(-1);
+        assertThat(taskList).isEmpty();
+    }
+
+    @Test
+    public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() throws Exception  {
         String taskDescription = "Wheeee!";
         ActivityManager.RecentTaskInfo task1 = new ActivityManager.RecentTaskInfo();
         task1.taskDescription = new ActivityManager.TaskDescription(taskDescription);
@@ -99,7 +116,7 @@
         task2.taskDescription = new ActivityManager.TaskDescription();
         GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(task1, task2,
                 null);
-        when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+        when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
         List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1,
@@ -111,14 +128,14 @@
     }
 
     @Test
-    public void loadTasksInBackground_freeformTask_createsDesktopTask() {
+    public void loadTasksInBackground_freeformTask_createsDesktopTask() throws Exception  {
         ActivityManager.RecentTaskInfo[] tasks = {
                 createRecentTaskInfo(1 /* taskId */),
                 createRecentTaskInfo(4 /* taskId */),
                 createRecentTaskInfo(5 /* taskId */)};
         GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forFreeformTasks(
                 tasks, Collections.emptySet() /* minimizedTaskIds */);
-        when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+        when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
         List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(
@@ -134,7 +151,8 @@
     }
 
     @Test
-    public void loadTasksInBackground_freeformTask_onlyMinimizedTasks_doesNotCreateDesktopTask() {
+    public void loadTasksInBackground_freeformTask_onlyMinimizedTasks_doesNotCreateDesktopTask()
+            throws Exception {
         ActivityManager.RecentTaskInfo[] tasks = {
                 createRecentTaskInfo(1 /* taskId */),
                 createRecentTaskInfo(4 /* taskId */),
@@ -143,7 +161,7 @@
                 Arrays.stream(new Integer[]{1, 4, 5}).collect(Collectors.toSet());
         GroupedRecentTaskInfo recentTaskInfos =
                 GroupedRecentTaskInfo.forFreeformTasks(tasks, minimizedTaskIds);
-        when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+        when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
         List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
index ec245ee..c24e974 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
@@ -24,6 +24,7 @@
 import androidx.test.filters.LargeTest;
 
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
+import com.android.launcher3.util.rule.ScreenRecordRule;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -133,6 +134,7 @@
 
     @Test
     @PortraitLandscape
+    @ScreenRecordRule.ScreenRecord // b/349439239
     public void testLaunchAppInSplitscreen_fromTaskbarAllApps() {
         getTaskbar().openAllApps()
                 .getAppIcon(TEST_APP_NAME)
diff --git a/res/color-night-v31/material_color_surface.xml b/res/color-night-v31/material_color_surface.xml
deleted file mode 100644
index a645f24..0000000
--- a/res/color-night-v31/material_color_surface.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="6" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_bright.xml b/res/color-night-v31/material_color_surface_bright.xml
deleted file mode 100644
index f34ed6c..0000000
--- a/res/color-night-v31/material_color_surface_bright.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="24" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container.xml b/res/color-night-v31/material_color_surface_container.xml
deleted file mode 100644
index 002b88e..0000000
--- a/res/color-night-v31/material_color_surface_container.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="12" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_high.xml b/res/color-night-v31/material_color_surface_container_high.xml
deleted file mode 100644
index edd36fc..0000000
--- a/res/color-night-v31/material_color_surface_container_high.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="17" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_highest.xml b/res/color-night-v31/material_color_surface_container_highest.xml
deleted file mode 100644
index e54f953..0000000
--- a/res/color-night-v31/material_color_surface_container_highest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="22" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_low.xml b/res/color-night-v31/material_color_surface_container_low.xml
deleted file mode 100644
index 40f0d4c..0000000
--- a/res/color-night-v31/material_color_surface_container_low.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="10" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_lowest.xml b/res/color-night-v31/material_color_surface_container_lowest.xml
deleted file mode 100644
index 24f559b..0000000
--- a/res/color-night-v31/material_color_surface_container_lowest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="4" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_dim.xml b/res/color-night-v31/material_color_surface_dim.xml
deleted file mode 100644
index a645f24..0000000
--- a/res/color-night-v31/material_color_surface_dim.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="6" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_inverse.xml b/res/color-night-v31/material_color_surface_inverse.xml
deleted file mode 100644
index ac63072..0000000
--- a/res/color-night-v31/material_color_surface_inverse.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="98" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_variant.xml b/res/color-night-v31/material_color_surface_variant.xml
deleted file mode 100644
index a645f24..0000000
--- a/res/color-night-v31/material_color_surface_variant.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="6" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/popup_shade_first.xml b/res/color-night-v31/popup_shade_first.xml
index 83822a6..28995e3 100644
--- a/res/color-night-v31/popup_shade_first.xml
+++ b/res/color-night-v31/popup_shade_first.xml
@@ -12,7 +12,6 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <item android:color="?androidprv:attr/materialColorSurfaceContainer"/>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?attr/materialColorSurfaceContainer"/>
 </selector>
diff --git a/res/color-v31/material_color_surface.xml b/res/color-v31/material_color_surface.xml
deleted file mode 100644
index b049851..0000000
--- a/res/color-v31/material_color_surface.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="98" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_bright.xml b/res/color-v31/material_color_surface_bright.xml
deleted file mode 100644
index b049851..0000000
--- a/res/color-v31/material_color_surface_bright.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="98" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container.xml b/res/color-v31/material_color_surface_container.xml
deleted file mode 100644
index b031c08..0000000
--- a/res/color-v31/material_color_surface_container.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="94" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_high.xml b/res/color-v31/material_color_surface_container_high.xml
deleted file mode 100644
index a996d51..0000000
--- a/res/color-v31/material_color_surface_container_high.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="92" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_highest.xml b/res/color-v31/material_color_surface_container_highest.xml
deleted file mode 100644
index e7a535a..0000000
--- a/res/color-v31/material_color_surface_container_highest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="90" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_low.xml b/res/color-v31/material_color_surface_container_low.xml
deleted file mode 100644
index b8fe01e..0000000
--- a/res/color-v31/material_color_surface_container_low.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="96" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_lowest.xml b/res/color-v31/material_color_surface_container_lowest.xml
deleted file mode 100644
index 25e8666..0000000
--- a/res/color-v31/material_color_surface_container_lowest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="100" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_dim.xml b/res/color-v31/material_color_surface_dim.xml
deleted file mode 100644
index e2d226f..0000000
--- a/res/color-v31/material_color_surface_dim.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="87" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_inverse.xml b/res/color-v31/material_color_surface_inverse.xml
deleted file mode 100644
index e189862..0000000
--- a/res/color-v31/material_color_surface_inverse.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="6" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_variant.xml b/res/color-v31/material_color_surface_variant.xml
deleted file mode 100644
index e2d226f..0000000
--- a/res/color-v31/material_color_surface_variant.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@android:color/system_neutral1_500" android:lStar="87" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/popup_shade_first.xml b/res/color-v31/popup_shade_first.xml
index 1278bb4..be73698 100644
--- a/res/color-v31/popup_shade_first.xml
+++ b/res/color-v31/popup_shade_first.xml
@@ -13,7 +13,6 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <item android:color="?androidprv:attr/materialColorSurfaceContainer"/>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?attr/materialColorSurfaceContainer"/>
 </selector>
diff --git a/res/color/overview_button.xml b/res/color/overview_button.xml
index 1dd8da6..0b317bd 100644
--- a/res/color/overview_button.xml
+++ b/res/color/overview_button.xml
@@ -1,12 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
     <item
         android:alpha="1"
-        android:color="?androidprv:attr/materialColorOnSurface"
+        android:color="?attr/materialColorOnSurface"
         android:state_enabled="true" />
     <item
         android:alpha="?android:disabledAlpha"
-        android:color="?androidprv:attr/materialColorOnSurface"
+        android:color="?attr/materialColorOnSurface"
         android:state_enabled="false" />
 </selector>
\ No newline at end of file
diff --git a/res/color/popup_shade_first.xml b/res/color/popup_shade_first.xml
index 1278bb4..be73698 100644
--- a/res/color/popup_shade_first.xml
+++ b/res/color/popup_shade_first.xml
@@ -13,7 +13,6 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <item android:color="?androidprv:attr/materialColorSurfaceContainer"/>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?attr/materialColorSurfaceContainer"/>
 </selector>
diff --git a/res/drawable/add_item_dialog_background.xml b/res/drawable/add_item_dialog_background.xml
index e279fa0..be4765a 100644
--- a/res/drawable/add_item_dialog_background.xml
+++ b/res/drawable/add_item_dialog_background.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle" >
-    <solid android:color="@color/material_color_surface_container_highest" />
+    <solid android:color="?attr/materialColorSurfaceContainerHighest" />
     <corners
         android:topLeftRadius="?android:attr/dialogCornerRadius"
         android:topRightRadius="?android:attr/dialogCornerRadius" />
diff --git a/res/drawable/all_apps_tabs_background.xml b/res/drawable/all_apps_tabs_background.xml
index 1e7cff2..62927af 100644
--- a/res/drawable/all_apps_tabs_background.xml
+++ b/res/drawable/all_apps_tabs_background.xml
@@ -30,7 +30,7 @@
                 android:state_selected="false">
                 <shape android:shape="rectangle">
                     <corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
-                    <solid android:color="@color/material_color_surface_bright" />
+                    <solid android:color="?attr/materialColorSurfaceBright" />
                 </shape>
             </item>
 
@@ -39,7 +39,7 @@
                 android:state_selected="true">
                 <shape android:shape="rectangle">
                     <corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
-                    <solid android:color="@color/material_color_primary" />
+                    <solid android:color="?attr/materialColorPrimary" />
                 </shape>
             </item>
         </selector>
diff --git a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
index 379e0e5..a19465d 100644
--- a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
+++ b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
@@ -15,8 +15,7 @@
 -->
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:shape="rectangle" >
-    <solid android:color="?androidprv:attr/materialColorOutlineVariant"/>
+    <solid android:color="?attr/materialColorOutlineVariant"/>
     <corners android:radius="@dimen/bottom_sheet_handle_corner_radius" />
 </shape>
diff --git a/res/drawable/button_top_rounded_bordered_ripple.xml b/res/drawable/button_top_rounded_bordered_ripple.xml
index f5b6886..13959f6 100644
--- a/res/drawable/button_top_rounded_bordered_ripple.xml
+++ b/res/drawable/button_top_rounded_bordered_ripple.xml
@@ -25,7 +25,7 @@
                     android:topRightRadius="12dp"
                     android:bottomLeftRadius="4dp"
                     android:bottomRightRadius="4dp"  />
-                <solid android:color="@color/material_color_surface_container_highest"/>
+                <solid android:color="?attr/materialColorSurfaceContainerHighest"/>
                 <stroke
                     android:width="2dp"
                     android:color="@color/button_bg"/>
diff --git a/res/drawable/cloud_download_24px.xml b/res/drawable/cloud_download_24px.xml
new file mode 100644
index 0000000..6f7c95a
--- /dev/null
+++ b/res/drawable/cloud_download_24px.xml
@@ -0,0 +1,11 @@
+<!-- GM3 icon cloud_download:vd_theme_24 -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M260,800Q169,800 104.5,737Q40,674 40,583Q40,505 87,444Q134,383 210,366Q227,294 295,229Q363,164 440,164Q473,164 496.5,187.5Q520,211 520,244L520,486L584,424L640,480L480,640L320,480L376,424L440,486L440,244Q364,258 322,317.5Q280,377 280,440L260,440Q202,440 161,481Q120,522 120,580Q120,638 161,679Q202,720 260,720L740,720Q782,720 811,691Q840,662 840,620Q840,578 811,549Q782,520 740,520L680,520L680,440Q680,392 658,350.5Q636,309 600,280L600,187Q674,222 717,290.5Q760,359 760,440L760,440L760,440Q829,448 874.5,499.5Q920,551 920,620Q920,695 867.5,747.5Q815,800 740,800L260,800ZM480,442Q480,442 480,442Q480,442 480,442L480,442Q480,442 480,442Q480,442 480,442L480,442Q480,442 480,442Q480,442 480,442L480,442Q480,442 480,442Q480,442 480,442Q480,442 480,442Q480,442 480,442L480,442Q480,442 480,442Q480,442 480,442Q480,442 480,442Q480,442 480,442L480,442L480,442Q480,442 480,442Q480,442 480,442Z"/>
+</vector>
diff --git a/res/drawable/ic_close_work_edu.xml b/res/drawable/ic_close_work_edu.xml
index f336eea..e4053e3 100644
--- a/res/drawable/ic_close_work_edu.xml
+++ b/res/drawable/ic_close_work_edu.xml
@@ -20,6 +20,6 @@
     android:viewportWidth="960"
     android:viewportHeight="960">
     <path
-        android:fillColor="@color/material_color_on_surface"
+        android:fillColor="?attr/materialColorOnSurface"
         android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z"/>
 </vector>
diff --git a/res/drawable/ic_more_vert_dots.xml b/res/drawable/ic_more_vert_dots.xml
new file mode 100644
index 0000000..c4659f8
--- /dev/null
+++ b/res/drawable/ic_more_vert_dots.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_private_space_with_background.xml b/res/drawable/ic_private_space_with_background.xml
index cb37c9a..cc73f6d 100644
--- a/res/drawable/ic_private_space_with_background.xml
+++ b/res/drawable/ic_private_space_with_background.xml
@@ -13,14 +13,13 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:viewportWidth="48"
     android:viewportHeight="48"
     android:width="48dp"
     android:height="48dp">
     <path
         android:pathData="M48 24A24 24 0 0 1 0 24A24 24 0 0 1 48 24Z"
-        android:fillColor="?androidprv:attr/materialColorSurfaceContainerLowest" />
+        android:fillColor="?attr/materialColorSurfaceContainerLowest" />
     <path
         android:pathData="M24.0021 10.6641L13.3354 14.6641V22.7841C13.3354 29.5174 17.8821 35.7974 24.0021 37.3307C30.1221 35.7974 34.6688 29.5174 34.6688 22.7841V14.6641L24.0021 10.6641ZM32.0021 22.7841C32.0021 28.1174 28.6021 33.0507 24.0021 34.5574C19.4021 33.0507 16.0021 28.1307 16.0021 22.7841V16.5174L24.0021 13.5174L32.0021 16.5174V22.7841Z"
         android:fillType="evenOdd"
diff --git a/res/drawable/icon_menu_arrow_background.xml b/res/drawable/icon_menu_arrow_background.xml
index 2eb1dfc..6345c2b 100644
--- a/res/drawable/icon_menu_arrow_background.xml
+++ b/res/drawable/icon_menu_arrow_background.xml
@@ -15,14 +15,13 @@
      limitations under the License.
 -->
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:autoMirrored="true">
     <gradient
         android:type="linear"
         android:angle="0"
         android:startColor="#00000000"
         android:centerX="0.25"
-        android:centerColor="?androidprv:attr/materialColorSurfaceBright"
-        android:endColor="?androidprv:attr/materialColorSurfaceBright" />
+        android:centerColor="?attr/materialColorSurfaceBright"
+        android:endColor="?attr/materialColorSurfaceBright" />
     <corners android:radius="@dimen/dialogCornerRadius" />
 </shape>
\ No newline at end of file
diff --git a/res/drawable/private_space_install_app_icon.xml b/res/drawable/private_space_install_app_icon.xml
index cfec2b1..12c4a82 100644
--- a/res/drawable/private_space_install_app_icon.xml
+++ b/res/drawable/private_space_install_app_icon.xml
@@ -23,9 +23,9 @@
             android:pathData="M30 0H30A30 30 0 0 1 60 30V30A30 30 0 0 1 30 60H30A30 30 0 0 1 0 30V30A30 30 0 0 1 30 0Z" />
         <path
             android:pathData="M30 0H30A30 30 0 0 1 60 30V30A30 30 0 0 1 30 60H30A30 30 0 0 1 0 30V30A30 30 0 0 1 30 0Z"
-            android:fillColor="@color/material_color_surface_container_lowest" />
+            android:fillColor="?attr/materialColorSurfaceContainerLowest" />
         <path
             android:pathData="M29 31h-6v-2h6v-6h2v6h6v2h-6v6h-2v-6Z"
-            android:fillColor="@color/material_color_on_surface" />
+            android:fillColor="?attr/materialColorOnSurface" />
     </group>
 </vector>
diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml
index e283d3f..ebfa996 100644
--- a/res/drawable/rounded_action_button.xml
+++ b/res/drawable/rounded_action_button.xml
@@ -7,7 +7,7 @@
   ~
   ~      http://www.apache.org/licenses/LICENSE-2.0
   ~
-  ~ Unless required by applicable law or agreed to in writing, software
+  ~ Unless required by applicable law or agreed to in writing, soft]ware
   ~ distributed under the License is distributed on an "AS IS" BASIS,
   ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   ~ See the License for the specific language governing permissions and
@@ -16,15 +16,11 @@
 
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:shape="rectangle">
-    <solid android:color="@color/material_color_surface_container_low" />
+    <solid android:color="?attr/materialColorSurfaceContainerLow" />
     <corners android:radius="@dimen/rounded_button_radius" />
     <stroke
         android:width="1dp"
-        android:color="@color/material_color_surface_container_low" />
-    <padding
-        android:left="@dimen/rounded_button_padding"
-        android:right="@dimen/rounded_button_padding" />
+        android:color="?attr/materialColorSurfaceContainerLow" />
 </shape>
 
diff --git a/res/drawable/work_card.xml b/res/drawable/work_card.xml
index 9bf2b8d..01ec947 100644
--- a/res/drawable/work_card.xml
+++ b/res/drawable/work_card.xml
@@ -17,7 +17,7 @@
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle">
-    <solid android:color="@color/material_color_surface_container_highest" />
+    <solid android:color="?attr/materialColorSurfaceContainerHighest" />
     <corners android:radius="@dimen/work_edu_card_radius" />
 </shape>
 
diff --git a/res/layout/private_space_header.xml b/res/layout/private_space_header.xml
index 9c0f129..e68f0f3 100644
--- a/res/layout/private_space_header.xml
+++ b/res/layout/private_space_header.xml
@@ -43,6 +43,7 @@
             android:layout_height="@dimen/ps_header_image_height"
             android:background="@drawable/ps_settings_background"
             android:src="@drawable/ic_ps_settings"
+            android:visibility="gone"
             android:contentDescription="@string/ps_container_settings" />
         <LinearLayout
             android:id="@+id/ps_lock_unlock_button"
@@ -60,7 +61,7 @@
                 android:layout_marginBottom="@dimen/ps_lock_icon_margin_bottom"
                 android:importantForAccessibility="no"
                 android:src="@drawable/ic_lock"
-                app:tint="@color/material_color_primary_fixed_dim"
+                app:tint="?attr/materialColorPrimaryFixedDim"
                 android:scaleType="center"/>
             <TextView
                 android:id="@+id/lock_text"
@@ -68,10 +69,12 @@
                 android:layout_height="wrap_content"
                 android:layout_marginStart="@dimen/ps_lock_icon_text_margin_start_expanded"
                 android:layout_marginEnd="@dimen/ps_lock_icon_text_margin_end_expanded"
-                android:textColor="@color/material_color_on_primary_fixed"
+                android:textColor="?attr/materialColorOnPrimaryFixed"
                 android:textSize="14sp"
                 android:text="@string/ps_container_lock_title"
+                android:maxLines="1"
                 android:visibility="gone"
+                android:alpha="0"
                 style="@style/TextHeadline"/>
         </LinearLayout>
     </LinearLayout>
diff --git a/res/layout/widgets_two_pane_sheet_paged_view.xml b/res/layout/widgets_two_pane_sheet_paged_view.xml
index 887efb8..1f41680 100644
--- a/res/layout/widgets_two_pane_sheet_paged_view.xml
+++ b/res/layout/widgets_two_pane_sheet_paged_view.xml
@@ -55,18 +55,39 @@
             android:clipToOutline="true"
             android:orientation="vertical">
 
-            <FrameLayout
+            <LinearLayout
                 android:id="@+id/search_bar_container"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
+                android:orientation="horizontal"
                 android:background="?attr/widgetPickerPrimarySurfaceColor"
-                android:clipToPadding="false"
-                android:elevation="0.1dp"
-                android:paddingBottom="8dp"
+                android:gravity="center_vertical"
                 launcher:layout_sticky="true">
+                <FrameLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:clipToPadding="false"
+                    android:elevation="0.1dp"
+                    android:paddingBottom="8dp">
 
-                <include layout="@layout/widgets_search_bar" />
-            </FrameLayout>
+                    <include layout="@layout/widgets_search_bar" />
+                </FrameLayout>
+
+                <ImageButton
+                    android:id="@+id/widget_picker_widget_options_menu"
+                    android:layout_width="48dp"
+                    android:layout_height="48dp"
+                    android:layout_marginStart="8dp"
+                    android:layout_marginBottom="8dp"
+                    android:layout_gravity="bottom"
+                    android:background="@drawable/full_rounded_transparent_ripple"
+                    android:contentDescription="@string/widget_picker_widget_options_button_description"
+                    android:padding="12dp"
+                    android:src="@drawable/ic_more_vert_dots"
+                    android:visibility="gone"
+                    android:tint="?attr/widgetPickerWidgetOptionsMenuColor" />
+            </LinearLayout>
 
             <FrameLayout
                 android:layout_width="match_parent"
diff --git a/res/layout/widgets_two_pane_sheet_recyclerview.xml b/res/layout/widgets_two_pane_sheet_recyclerview.xml
index f3d3b16..c6b3b74 100644
--- a/res/layout/widgets_two_pane_sheet_recyclerview.xml
+++ b/res/layout/widgets_two_pane_sheet_recyclerview.xml
@@ -39,19 +39,40 @@
             android:clipToOutline="true"
             android:orientation="vertical">
 
-            <FrameLayout
+            <LinearLayout
                 android:id="@+id/search_bar_container"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
+                android:gravity="center_vertical"
+                android:orientation="horizontal"
                 android:background="?attr/widgetPickerPrimarySurfaceColor"
-                android:clipToPadding="false"
-                android:elevation="0.1dp"
-                android:paddingBottom="16dp"
                 android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
                 launcher:layout_sticky="true">
+                <FrameLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:clipToPadding="false"
+                    android:elevation="0.1dp"
+                    android:paddingBottom="16dp">
 
-                <include layout="@layout/widgets_search_bar" />
-            </FrameLayout>
+                    <include layout="@layout/widgets_search_bar" />
+                </FrameLayout>
+
+                <ImageButton
+                    android:id="@+id/widget_picker_widget_options_menu"
+                    android:layout_width="48dp"
+                    android:layout_height="48dp"
+                    android:layout_marginStart="8dp"
+                    android:layout_marginBottom="16dp"
+                    android:layout_gravity="bottom"
+                    android:background="@drawable/full_rounded_transparent_ripple"
+                    android:contentDescription="@string/widget_picker_widget_options_button_description"
+                    android:padding="12dp"
+                    android:src="@drawable/ic_more_vert_dots"
+                    android:visibility="gone"
+                    android:tint="?attr/widgetPickerWidgetOptionsMenuColor" />
+            </LinearLayout>
 
             <FrameLayout
                 android:layout_width="match_parent"
diff --git a/res/layout/work_apps_edu.xml b/res/layout/work_apps_edu.xml
index c581ae3..a45d585 100644
--- a/res/layout/work_apps_edu.xml
+++ b/res/layout/work_apps_edu.xml
@@ -44,8 +44,7 @@
         <FrameLayout
             android:layout_width="@dimen/rounded_button_width"
             android:layout_height="@dimen/rounded_button_width"
-            android:background="@drawable/rounded_action_button"
-            android:padding="@dimen/rounded_button_padding">
+            android:background="@drawable/rounded_action_button">
             <ImageButton
                 android:id="@+id/action_btn"
                 android:layout_width="@dimen/x_icon_size"
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 1a8f8e2..0ef1f47 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Apppaar is nie beskikbaar nie"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Raak en hou om \'n legstuk te skuif."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dubbeltik en hou om \'n legstuk te skuif of gebruik gepasmaakte handelinge."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d breed by %2$d hoog"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>-legstuk"</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 80447c5..63cace9 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"የመተግበሪያ ጥምረት አይገኝም"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ምግብርን ለማንቀሳቀስ ይንኩ እና ይያዙ።"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ምግብርን ለማንቀሳቀስ ወይም ብጁ እርምጃዎችን ለመጠቀም ሁለቴ መታ ያድርጉ እና ይያዙ።"</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d ስፋት በ%2$d ከፍታ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"የ<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ምግብር"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 4eee121..d62400e 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ميزة \"استخدام تطبيقين في الوقت نفسه\" غير متوفّرة"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"انقر مع الاستمرار لنقل أداة."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"انقر مرتين مع تثبيت إصبعك لنقل أداة أو استخدام الإجراءات المخصّصة."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"‏العرض %1$d الطول %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"أداة <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index 52ec7ea..a3c0bc0 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"এপ্‌ পেয়াৰ কৰাৰ সুবিধাটো উপলব্ধ নহয়"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ৱিজেট স্থানান্তৰ কৰিবলৈ টিপি ধৰি ৰাখক।"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"কোনো ৱিজেট স্থানান্তৰ কৰিবলৈ দুবাৰ টিপি ধৰি ৰাখক অথবা কাষ্টম কাৰ্য ব্যৱহাৰ কৰক।"</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d বহল x %2$d ওখ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ৱিজেট"</string>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index 6c1cc46..cfdcbe0 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Tətbiq cütü əlçatan deyil"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Vidceti daşımaq üçün toxunub saxlayın."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Vidceti daşımaq üçün iki dəfə toxunub saxlayın və ya fərdi əməliyyatlardan istifadə edin."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%2$d hündürlük %1$d enində"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> vidceti"</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 24328cf..322a613 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Par aplikacija nije dostupan"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Dodirnite i zadržite radi pomeranja vidžeta."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dvaput dodirnite i zadržite da biste pomerali vidžet ili koristite prilagođene radnje."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d×%2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"širina od %1$d i visina od %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> vidžet"</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index ebbb378..6f423aa 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Спалучэнне праграм недаступнае"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Націсніце і ўтрымлівайце віджэт для перамяшчэння."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Дакраніцеся двойчы і ўтрымлівайце, каб перамясціць віджэт або выкарыстоўваць спецыяльныя дзеянні."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Шырына: %1$d, вышыня: %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Віджэт \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\""</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index d5d948e..2230012 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Двойката приложения не е налице"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Докоснете и задръжте за преместване на приспособление"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Докоснете двукратно и задръжте за преместване на приспособление или използвайте персонал. действия."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Ширина %1$d и височина %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> приспособление"</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index cf75fb58..09ebf2f 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -38,6 +38,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"অ্যাপ পেয়ার উপলভ্য নেই"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"কোনও উইজেট সরাতে সেটি টাচ করে ধরে রাখুন।"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"একটি উইজেট সরাতে বা কাস্টম অ্যাকশন ব্যবহার করতে ডবল ট্যাপ করে ধরে রাখুন।"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"আরও বিকল্প"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"সব উইজেট দেখুন"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%2$d উচ্চতা অনুযায়ী %1$d প্রস্থ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>টি উইজেট"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 1758c39..e6dffb2 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Par aplikacija nije dostupan"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Dodirnite i zadržite da pomjerite vidžet."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dvaput dodirnite i zadržite da pomjerite vidžet ili da koristite prilagođene radnje."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Širina %1$d, visina %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Vidžet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index bf578f5..014e481 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"La parella d\'aplicacions no està disponible"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Fes doble toc i mantén premut per moure un widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Fes doble toc i mantén premut per moure un widget o per utilitzar accions personalitzades."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d d\'amplada per %2$d d\'alçada"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget de <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 9a8fc6f..456b4da 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Dvojice aplikací není k dispozici"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Widget přesunete klepnutím a podržením."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dvojitým klepnutím a podržením přesunete widget, případně použijte vlastní akce."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"šířka %1$d, výška %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 208c2ef..b202828 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Appsammenknytning er ikke tilgængelig"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Hold en widget nede for at flytte den."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Tryk to gange, og hold en widget nede for at flytte den eller bruge tilpassede handlinger."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d i bredden og %2$d i højden"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widgetten <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 380030b..3e984cd 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"App-Paar nicht verfügbar"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Zum Verschieben des Widgets gedrückt halten"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Doppeltippen und halten, um ein Widget zu bewegen oder benutzerdefinierte Aktionen zu nutzen."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d breit und %2$d hoch"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget „<xliff:g id="WIDGET_NAME">%1$s</xliff:g>“"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index e86ebae..cda7670 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Το ζεύγος εφαρμογών δεν είναι διαθέσιμο"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Πατήστε παρατετ. για μετακίνηση γραφ. στοιχείου."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Πατήστε δύο φορές παρατεταμένα για μετακίνηση γραφικού στοιχείου ή χρήση προσαρμοσμένων ενεργειών."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Πλάτος %1$d επί ύψος %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Γραφικό στοιχείο <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 50c5976..80957b2 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"App pair isn\'t available"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Touch and hold to move a widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Double-tap &amp; hold to move a widget or use custom actions."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d wide by %2$d high"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index 08ff6e7..311b4b9 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -38,6 +38,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"App pair isn\'t available"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Touch and hold to move a widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Double-tap and hold to move a widget or use custom actions."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"More options"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Show all widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d wide by %2$d high"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 50c5976..80957b2 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"App pair isn\'t available"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Touch and hold to move a widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Double-tap &amp; hold to move a widget or use custom actions."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d wide by %2$d high"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 50c5976..80957b2 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"App pair isn\'t available"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Touch and hold to move a widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Double-tap &amp; hold to move a widget or use custom actions."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d wide by %2$d high"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index fa6d1f1..047a67f 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -38,6 +38,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‎‏‎‏‎‏‏‏‎‎‎‎‏‎‏‏‎‏‎‎‏‎‎‏‎‎‏‎‎‏‎‎‏‎‎‎‎‎‏‎‎‏‏‎‏‏‎‏‏‏‎‎‎‏‏‏‏‏‎App pair isn\'t available‎‏‎‎‏‎"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‏‎‎‏‎‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‏‎‎‏‏‎‎‎‏‎‎‎‎‏‎‏‎‎‏‎‎‎‏‏‎‎‎‎‎‏‎‏‎‎‏‎Touch &amp; hold to move a widget.‎‏‎‎‏‎"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‏‏‎‏‏‏‏‏‎‏‎‏‎‎‎‎‎‎‎‏‏‏‏‎‎‎‎‏‏‎‏‎‏‏‎‎‎‎‎‎‏‎‏‎‎‎‎‎‎‎‎‎‏‏‎‎Double-tap &amp; hold to move a widget or use custom actions.‎‏‎‎‏‎"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‎‎‏‏‎‎‏‎‏‏‎‎‏‏‎‎‎‎‎‎‎‎‏‎‎‎‎‎‏‎‏‎‎‏‏‏‏‏‏‎‏‎‏‏‎‎‎‎‏‎‎‎‏‎‏‏‎More options‎‏‎‎‏‎"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‏‎‎‏‏‎‎‏‏‏‎‎‏‎‎‏‏‏‏‏‏‎‏‏‏‎‎‎‏‎‎‎‏‏‏‎‏‎‎‎‎‏‏‏‎‏‎‏‏‏‎Show all widgets‎‏‎‎‏‎"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‏‎‎‏‏‎‏‎‎‏‏‏‏‏‏‎‎‏‏‎‎‏‏‎‏‎‎‎‎‎‎‏‎‏‎‎‎‏‎‎‎‎‎‏‎‎‎‏‎‏‏‏‏‏‎%1$d × %2$d‎‏‎‎‏‎"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‎‏‎‎‎‎‏‎‎‎‏‏‎‏‎‎‎‎‏‏‎‎‎‏‎‎‎‏‏‏‎‎‎‎‏‏‏‎‎‏‎‎‏‎‎‏‏‎‎‎‎‎‏‏‏‏‎‎%1$d wide by %2$d high‎‏‎‎‏‎"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‎‎‏‎‏‎‎‏‎‎‎‏‏‎‎‏‎‏‎‏‎‏‎‎‎‎‎‎‎‎‎‎‏‏‏‎‏‏‏‎‎‎‎‎‎‏‏‎‏‏‎‎‎‏‎‎‏‏‎<xliff:g id="WIDGET_NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎ widget‎‏‎‎‏‎"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 5879129..dc3e779 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"La vinculación de apps no está disponible"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Mantén presionado para mover un widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Presiona dos veces y mantén presionado para mover un widget o usar acciones personalizadas."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d de ancho por %2$d de alto"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 8d384ac..0f716dc 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"La aplicación emparejada no está disponible"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Mantén pulsado un widget para moverlo"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Toca dos veces y mantén pulsado un widget para moverlo o usar acciones personalizadas."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d de ancho por %2$d de alto"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget de <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 44448a6..e7e9c98 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Rakendusepaar ei ole saadaval"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Vidina teisaldamiseks puudutage ja hoidke all."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Vidina teisaldamiseks või kohandatud toimingute kasutamiseks topeltpuudutage ja hoidke all."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d lai ja %2$d kõrge"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Vidin <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index 6fc4cf4..9868cca 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Aplikazio parea ez dago erabilgarri"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Eduki sakatuta widget bat mugitzeko."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Sakatu birritan eta eduki sakatuta widget bat mugitzeko edo ekintza pertsonalizatuak erabiltzeko."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d zabal eta %2$d luze"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widgeta"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index fffce13..19689d3 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"جفت برنامه دردسترس نیست"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"برای جابه‌جا کردن ابزارک، لمس کنید و نگه دارید."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"برای جابه‌جا کردن ابزارک یا استفاده از کنش‌های سفارشی، دو تک‌ضرب بزنید و نگه دارید."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"‏%1$d عرض در %2$d طول"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"ابزارک <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 310053e..6bb0faf 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Sovelluspari ei ole saatavilla"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Kosketa pitkään, niin voit siirtää widgetiä."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Kaksoisnapauta ja paina pitkään, niin voit siirtää widgetiä tai käyttää muokattuja toimintoja."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Leveys: %1$d, korkeus: %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 11b2c01..6bf5d63 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"La Paire d\'applis n\'est pas offerte"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Maintenez le doigt sur un widget pour le déplacer."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Touchez 2x un widget et maintenez le doigt dessus pour le déplacer ou utiliser des actions personnalisées."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d de largeur sur %2$d de hauteur"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index b4c5ec6..c128343 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"La paire d\'applications n\'est pas disponible"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Appuyez de manière prolongée sur un widget pour le déplacer."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Appuyez deux fois et maintenez la pression pour déplacer widget ou utiliser actions personnalisées."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d x %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d de largeur et %2$d de hauteur"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index ac72802..e68d0e5 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -38,6 +38,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Non está dispoñible o emparellamento de aplicacións"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Mantén premido un widget para movelo."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Toca dúas veces un widget e manteno premido para movelo ou utiliza accións personalizadas."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Máis opcións"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Mostrar todos os widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d de largo por %2$d de alto"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 74747d0..c5e00ef 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ઍપની જોડી ઉપલબ્ધ નથી"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"વિજેટ ખસેડવા ટચ કરીને થોડી વાર દબાવી રાખો."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"વિજેટ ખસેડવા બે વાર ટૅપ કરીને દબાવી રાખો અથવા કસ્ટમ ક્રિયાઓનો ઉપયોગ કરો."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d પહોળાઈ X %2$d ઊંચાઈ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> વિજેટ"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 6071935..79fcc3f 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -38,6 +38,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"साथ में इस्तेमाल किए जा सकने वाले ऐप्लिकेशन की सुविधा उपलब्ध नहीं है"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"किसी विजेट को एक से दूसरी जगह ले जाने के लिए, उसे दबाकर रखें."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"किसी विजेट को एक से दूसरी जगह ले जाने के लिए, उस पर दो बार टैप करके दबाकर रखें या पसंद के मुताबिक कार्रवाइयां इस्तेमाल करें."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"ज़्यादा विकल्प"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"सभी विजेट दिखाएं"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d चौड़ाई गुणा %2$d ऊंचाई"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट"</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index cf7a91a..6c86d88 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Par aplikacija nije dostupan"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Dodirnite i zadržite da biste premjestili widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dvaput dodirnite i zadržite pritisak da biste premjestili widget ili upotrijebite prilagođene radnje"</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d širine i %2$d visine"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index f306110..2378a52 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Az alkalmazáspár nem áll rendelkezésre"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Tartsa lenyomva a modult az áthelyezéshez."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Modul áthelyezéséhez koppintson duplán, tartsa nyomva az ujját, vagy használjon egyéni műveleteket."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d széles és %2$d magas"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> modul"</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 2d345f2..67cc1d5 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Հավելվածների զույգը հասանելի չէ"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Հպեք և պահեք՝ վիջեթ տեղափոխելու համար։"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Կրկնակի հպեք և պահեք՝ վիջեթ տեղափոխելու համար, կամ օգտվեք հատուկ գործողություններից։"</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Լայնությունը՝ %1$d, բարձրությունը՝ %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> վիջեթ"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 9ced9f4..f112064 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Pasangan aplikasi tidak tersedia"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Sentuh lama untuk memindahkan widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Ketuk dua kali &amp; tahan untuk memindahkan widget atau gunakan tindakan khusus."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"lebar %1$d x tinggi %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 2ab7817..558af15 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Forritapar er ekki í boði"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Haltu fingri á græju til að færa hana."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Ýttu tvisvar og haltu fingri á græju til að færa hana eða notaðu sérsniðnar aðgerðir."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d á breidd og %2$d á hæð"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Græjan <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index a59de6c..fb05034 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"La coppia di app non è disponibile"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Tocca e tieni premuto per spostare un widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Tocca due volte e tieni premuto per spostare un widget o per usare le azioni personalizzate."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d di larghezza per %2$d di altezza"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 89a0c8a..09a876a 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"צמד האפליקציות לא זמין"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"להעברת ווידג\'ט למקום אחר לוחצים עליו לחיצה ארוכה."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"כדי להעביר ווידג\'ט למקום אחר או להשתמש בפעולות מותאמות אישית, יש ללחוץ פעמיים ולא להרפות."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"‏רוחב %1$d על גובה %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"ווידג\'ט <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index b48c7be..e0aa65f 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -38,6 +38,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"アプリのペア設定は利用できません"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"長押ししてウィジェットを移動させます。"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ウィジェットをダブルタップして長押ししながら移動するか、カスタム操作を使用してください。"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"その他のオプション"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"すべてのウィジェットを表示"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$dx%2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"幅 %1$d、高さ %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ウィジェット"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index 76b8b5d..369aa22 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"აპთა წყვილი მიუწვდომელია"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"შეხებით აირჩიეთ და გეჭიროთ ვიჯეტის გადასაადგილებლად."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ორმაგი შეხებით აირჩიეთ და გეჭიროთ ვიჯეტის გადასაადგილებლად ან მორგებული მოქმედებების გამოსაყენებლად."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"სიგრძე: %1$d, სიგანე: %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ვიჯეტი"</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 95d4420..8d4efa7 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Қолданбаларды жұптау функциясы қолжетімді емес."</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Виджетті жылжыту үшін басып тұрыңыз."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Виджетті жылжыту үшін екі рет түртіңіз де, ұстап тұрыңыз немесе арнаулы әрекеттерді пайдаланыңыз."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Ені: %1$d, биіктігі: %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> виджеті"</string>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index 5c71276..caa92bd 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"មិនអាចប្រើគូកម្មវិធីបានទេ"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ចុចឱ្យជាប់​ដើម្បីផ្លាស់ទី​ធាតុក្រាហ្វិក​។"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ចុចពីរដង រួចសង្កត់ឱ្យជាប់ ដើម្បីផ្លាស់ទី​ធាតុក្រាហ្វិក ឬប្រើ​សកម្មភាព​តាមបំណង​។"</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"ទទឺង %1$d គុណនឹងកម្ពស់ %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"ធាតុ​ក្រាហ្វិក <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 931ca30..04daaac 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -38,6 +38,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ಆ್ಯಪ್ ಜೋಡಿ ಲಭ್ಯವಿಲ್ಲ"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ವಿಜೆಟ್ ಸರಿಸಲು ಸ್ಪರ್ಶಿಸಿ ಮತ್ತು ಹಿಡಿದುಕೊಳ್ಳಿ."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ವಿಜೆಟ್ ಸರಿಸಲು ಅಥವಾ ಕಸ್ಟಮ್ ಕ್ರಿಯೆಗಳನ್ನು ಬಳಸಲು ಡಬಲ್-ಟ್ಯಾಪ್ ಮಾಡಿ ಮತ್ತು ಹಿಡಿದುಕೊಳ್ಳಿ."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"ಇನ್ನಷ್ಟು ಆಯ್ಕೆಗಳು"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"ಎಲ್ಲಾ ವಿಜೆಟ್‌ ತೋರಿಸಿ"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d ಅಗಲ ಮತ್ತು %2$d ಎತ್ತರ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ವಿಜೆಟ್"</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 9f64cfb..152745a 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"앱 페어링을 사용할 수 없습니다."</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"길게 터치하여 위젯을 이동하세요."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"두 번 탭한 다음 길게 터치하여 위젯을 이동하거나 맞춤 작업을 사용하세요."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d×%2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"너비 %1$d, 높이 %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"위젯 <xliff:g id="WIDGET_NAME">%1$s</xliff:g>개"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 3e67c36..6a9b4df 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Эки колдонмону бир маалда пайдаланууга болбойт"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Виджетти кое бербей басып туруп жылдырыңыз."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Виджетти жылдыруу үчүн эки жолу таптап, кармап туруңуз же ыңгайлаштырылган аракеттерди колдонуңуз."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Туурасы: %1$d, бийиктиги: %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> виджети"</string>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index e0cd3ee..9572f3d 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -38,6 +38,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ການຈັບຄູ່ແອັບບໍ່ມີໃຫ້"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ແຕະຄ້າງໄວ້ເພື່ອຍ້າຍວິດເຈັດ."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ແຕະສອງເທື່ອຄ້າງໄວ້ເພື່ອຍ້າຍວິດເຈັດ ຫຼື ໃຊ້ຄຳສັ່ງກຳນົດເອງ."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"ຕົວເລືອກເພີ່ມເຕີມ"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"ສະແດງວິດເຈັດທັງໝົດ"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"ກວ້າງ %1$d ຄູນສູງ %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"ວິດເຈັດ <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 9846525..6cd8872 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -38,6 +38,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Programų pora nepasiekiama"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Dukart pal. ir palaik., kad perkeltumėte valdiklį."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dukart palieskite ir palaikykite, kad perkeltumėte valdiklį ar naudotumėte tinkintus veiksmus."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Daugiau parinkčių"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Rodyti visus valdiklius"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d plotis ir %2$d aukštis"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> valdiklis"</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 49f7ffe..6fd8345 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Lietotņu pāris nav pieejams"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Lai pārvietotu logrīku, pieskarieties un turiet."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Lai pārvietotu logrīku, uz tā veiciet dubultskārienu un turiet. Varat arī veikt pielāgotas darbības."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d plats un %2$d augsts"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Logrīks <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 2bfa089..5440af6 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Парот апликации не е достапен"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Допрете и задржете за да преместите виџет."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Допрете двапати и задржете за да преместите виџет или користете приспособени дејства."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d широк на %2$d висок"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Виџет <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index a2babd5..8a860bf 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ആപ്പ് ജോടി ലഭ്യമല്ല"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"വിജറ്റ് നീക്കാൻ സ്‌പർശിച്ച് പിടിക്കുക."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"വിജറ്റ് നീക്കാൻ ഡബിൾ ടാപ്പ് ചെയ്യൂ, ഹോൾഡ് ചെയ്യൂ അല്ലെങ്കിൽ ഇഷ്‌ടാനുസൃത പ്രവർത്തനങ്ങൾ ഉപയോഗിക്കൂ."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d വീതിയും %2$d ഉയരവും"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> വിജറ്റ്"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 93a1c9c..e85a33c 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Апп хослуулалт боломжгүй байна"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Виджетийг зөөх бол хүрээд, удаан дарна уу."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Виджетийг зөөх эсвэл захиалгат үйлдлийг ашиглахын тулд хоёр товшоод, удаан дарна уу."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d өргөн %2$d өндөр"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> жижиг хэрэгсэл"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index c084fab..2f78245 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -38,6 +38,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ॲपची जोडी उपलब्ध नाही"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"विजेट हलवण्यासाठी स्पर्श करा आणि धरून ठेवा."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"विजेट हलवण्यासाठी किंवा कस्टम कृती वापरण्यासाठी दोनदा टॅप करा आणि धरून ठेवा."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"आणखी पर्याय"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"सर्व विजेट दाखवा"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d रूंद बाय %2$d उंच"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट"</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 72d92e2..d2fd7a8 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -38,6 +38,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Gandingan apl tidak tersedia"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Sentuh &amp; tahan untuk menggerakkan widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Ketik dua kali &amp; tahan untuk menggerakkan widget atau menggunakan tindakan tersuai."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Lagi pilihan"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Tunjukkan semua widget"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Lebar %1$d kali tinggi %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index 194ece7..a6b8500 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"အက်ပ်တွဲချိတ်ခြင်းကို မရနိုင်ပါ"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ဝိဂျက်ကို ရွှေ့ရန် တို့ပြီး ဖိထားပါ။"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ဝိဂျက်ကို ရွှေ့ရန် (သို့) စိတ်ကြိုက်လုပ်ဆောင်ချက်များကို သုံးရန် နှစ်ချက်တို့ပြီး ဖိထားပါ။"</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"အလျား %1$d နှင့် အမြင့် %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ဝိဂျက်"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index a9e6c5d..bc688aa 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Apptilkoblingen er ikke tilgjengelig"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Trykk og hold for å flytte en modul."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dobbelttrykk og hold inne for å flytte en modul eller bruke tilpassede handlinger."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d bredde x %2$d høyde"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>-modul"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 7880c36..eddcefb 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -38,6 +38,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"एप पेयर उपलब्ध छैन"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"कुनै विजेट सार्न डबल ट्याप गरेर छोइराख्नुहोस्।"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"कुनै विजेट सार्न वा आफ्नो रोजाइका कारबाही प्रयोग गर्न डबल ट्याप गरेर छोइराख्नुहोस्।"</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"थप विकल्पहरू"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"सबै विजेटहरू देखाउनुहोस्"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d चौडाइ गुणा %2$d उचाइ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट"</string>
diff --git a/res/values-night-v31/colors.xml b/res/values-night-v31/colors.xml
index 2688b83..07c450e 100644
--- a/res/values-night-v31/colors.xml
+++ b/res/values-night-v31/colors.xml
@@ -57,39 +57,4 @@
         @android:color/system_accent1_200</color>
     <color name="work_fab_icon_color">
         @android:color/system_accent1_900</color>
-
-    <color name="material_color_on_secondary_fixed_variant">@android:color/system_accent2_700</color>
-    <color name="material_color_on_tertiary_fixed_variant">@android:color/system_accent3_700</color>
-    <color name="material_color_on_primary_fixed_variant">@android:color/system_accent1_700</color>
-    <color name="material_color_on_secondary_container">@android:color/system_accent2_100</color>
-    <color name="material_color_on_tertiary_container">@android:color/system_accent3_100</color>
-    <color name="material_color_on_primary_container">@android:color/system_accent1_100</color>
-    <color name="material_color_secondary_fixed_dim">@android:color/system_accent2_200</color>
-    <color name="material_color_on_error_container">#FFDAD5</color>
-    <color name="material_color_on_secondary_fixed">@android:color/system_accent2_900</color>
-    <color name="material_color_on_surface_inverse">@android:color/system_neutral1_900</color>
-    <color name="material_color_tertiary_fixed_dim">@android:color/system_accent3_200</color>
-    <color name="material_color_on_tertiary_fixed">@android:color/system_accent3_900</color>
-    <color name="material_color_primary_fixed_dim">@android:color/system_accent1_200</color>
-    <color name="material_color_secondary_container">@android:color/system_accent2_700</color>
-    <color name="material_color_error_container">#930001</color>
-    <color name="material_color_on_primary_fixed">@android:color/system_accent1_900</color>
-    <color name="material_color_primary_inverse">@android:color/system_accent1_600</color>
-    <color name="material_color_secondary_fixed">@android:color/system_accent2_100</color>
-    <color name="material_color_tertiary_container">@android:color/system_accent3_700</color>
-    <color name="material_color_tertiary_fixed">@android:color/system_accent3_100</color>
-    <color name="material_color_primary_container">@android:color/system_accent1_700</color>
-    <color name="material_color_on_background">@android:color/system_neutral1_800</color>
-    <color name="material_color_primary_fixed">@android:color/system_accent1_100</color>
-    <color name="material_color_on_secondary">@android:color/system_accent2_800</color>
-    <color name="material_color_on_tertiary">@android:color/system_accent3_800</color>
-    <color name="material_color_on_error">#690001</color>
-    <color name="material_color_on_surface_variant">@android:color/system_neutral2_200</color>
-    <color name="material_color_outline">@android:color/system_neutral2_400</color>
-    <color name="material_color_outline_variant">@android:color/system_neutral2_700</color>
-    <color name="material_color_on_primary">@android:color/system_accent1_800</color>
-    <color name="material_color_on_surface">@android:color/system_neutral1_100</color>
-    <color name="material_color_primary">@android:color/system_accent1_200</color>
-    <color name="material_color_secondary">@android:color/system_accent2_200</color>
-    <color name="material_color_tertiary">@android:color/system_accent3_200</color>
 </resources>
\ No newline at end of file
diff --git a/res/values-night-v34/colors.xml b/res/values-night-v34/colors.xml
index af28119..abce763 100644
--- a/res/values-night-v34/colors.xml
+++ b/res/values-night-v34/colors.xml
@@ -27,4 +27,7 @@
         @android:color/system_on_surface_dark</color>
     <color name="widget_cell_subtitle_color_dark">
         @android:color/system_on_surface_variant_dark</color>
+    <color name="widget_picker_menu_options_color_dark">
+        @android:color/system_on_surface_variant_dark
+    </color>
 </resources>
diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml
deleted file mode 100644
index 95b3a63..0000000
--- a/res/values-night/colors.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-**
-** Copyright 2023, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-<resources>
-    <color name="material_color_on_secondary_fixed_variant">#3F4759</color>
-    <color name="material_color_on_tertiary_fixed_variant">#583E5B</color>
-    <color name="material_color_surface_container_lowest">#0D0E11</color>
-    <color name="material_color_on_primary_fixed_variant">#2B4678</color>
-    <color name="material_color_on_secondary_container">#DBE2F9</color>
-    <color name="material_color_on_tertiary_container">#FBD7FC</color>
-    <color name="material_color_surface_container_low">#1B1B1F</color>
-    <color name="material_color_on_primary_container">#D8E2FF</color>
-    <color name="material_color_secondary_fixed_dim">#BFC6DC</color>
-    <color name="material_color_on_error_container">#FFDAD5</color>
-    <color name="material_color_on_secondary_fixed">#141B2C</color>
-    <color name="material_color_on_surface_inverse">#1B1B1F</color>
-    <color name="material_color_tertiary_fixed_dim">#DEBCDF</color>
-    <color name="material_color_on_tertiary_fixed">#29132D</color>
-    <color name="material_color_primary_fixed_dim">#ADC6FF</color>
-    <color name="material_color_secondary_container">#3F4759</color>
-    <color name="material_color_error_container">#930001</color>
-    <color name="material_color_on_primary_fixed">#001A41</color>
-    <color name="material_color_primary_inverse">#445E91</color>
-    <color name="material_color_secondary_fixed">#DBE2F9</color>
-    <color name="material_color_surface_inverse">#FAF9FD</color>
-    <color name="material_color_surface_variant">#44474F</color>
-    <color name="material_color_tertiary_container">#583E5B</color>
-    <color name="material_color_tertiary_fixed">#FBD7FC</color>
-    <color name="material_color_primary_container">#2B4678</color>
-    <color name="material_color_on_background">#E3E2E6</color>
-    <color name="material_color_primary_fixed">#D8E2FF</color>
-    <color name="material_color_on_secondary">#293041</color>
-    <color name="material_color_on_tertiary">#402843</color>
-    <color name="material_color_surface_dim">#121316</color>
-    <color name="material_color_surface_bright">#38393C</color>
-    <color name="material_color_on_error">#690001</color>
-    <color name="material_color_surface">#121316</color>
-    <color name="material_color_surface_container_high">#292A2D</color>
-    <color name="material_color_surface_container_highest">#343538</color>
-    <color name="material_color_on_surface_variant">#C4C6D0</color>
-    <color name="material_color_outline">#72747D</color>
-    <color name="material_color_outline_variant">#444746</color>
-    <color name="material_color_on_primary">#102F60</color>
-    <color name="material_color_on_surface">#E3E2E6</color>
-    <color name="material_color_surface_container">#1F1F23</color>
-    <color name="material_color_primary">#ADC6FF</color>
-    <color name="material_color_secondary">#BFC6DC</color>
-    <color name="material_color_tertiary">#DEBCDF</color>
-</resources>
diff --git a/res/values-night/styles.xml b/res/values-night/styles.xml
index c95722f..06f0eee 100644
--- a/res/values-night/styles.xml
+++ b/res/values-night/styles.xml
@@ -26,9 +26,64 @@
         <item name="android:backgroundDimEnabled">true</item>
     </style>
 
-    <style name="WidgetPickerActivityTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
+    <style name="DynamicColorsBaseLauncherTheme" parent="@style/BaseLauncherTheme">
+        <item name="materialColorOnSecondaryFixedVariant">@color/system_on_secondary_fixed_variant</item>
+        <item name="materialColorOnTertiaryFixedVariant">@color/system_on_tertiary_fixed_variant</item>
+        <item name="materialColorSurfaceContainerLowest">@color/system_surface_container_lowest_dark</item>
+        <item name="materialColorOnPrimaryFixedVariant">@color/system_on_primary_fixed_variant</item>
+        <item name="materialColorOnSecondaryContainer">@color/system_on_secondary_container_dark</item>
+        <item name="materialColorOnTertiaryContainer">@color/system_on_tertiary_container_dark</item>
+        <item name="materialColorSurfaceContainerLow">@color/system_surface_container_low_dark</item>
+        <item name="materialColorOnPrimaryContainer">@color/system_on_primary_container_dark</item>
+        <item name="materialColorSecondaryFixedDim">@color/system_secondary_fixed_dim</item>
+        <item name="materialColorOnErrorContainer">@color/system_on_error_container_dark</item>
+        <item name="materialColorOnSecondaryFixed">@color/system_on_secondary_fixed</item>
+        <item name="materialColorOnSurfaceInverse">@color/system_on_surface_light</item>
+        <item name="materialColorTertiaryFixedDim">@color/system_tertiary_fixed_dim</item>
+        <item name="materialColorOnTertiaryFixed">@color/system_on_tertiary_fixed</item>
+        <item name="materialColorPrimaryFixedDim">@color/system_primary_fixed_dim</item>
+        <item name="materialColorSecondaryContainer">@color/system_secondary_container_dark</item>
+        <item name="materialColorErrorContainer">@color/system_error_container_dark</item>
+        <item name="materialColorOnPrimaryFixed">@color/system_on_primary_fixed</item>
+        <item name="materialColorPrimaryInverse">@color/system_primary_light</item>
+        <item name="materialColorSecondaryFixed">@color/system_secondary_fixed</item>
+        <item name="materialColorSurfaceInverse">@color/system_surface_light</item>
+        <item name="materialColorSurfaceVariant">@color/system_surface_variant_dark</item>
+        <item name="materialColorTertiaryContainer">@color/system_tertiary_container_dark</item>
+        <item name="materialColorTertiaryFixed">@color/system_tertiary_fixed</item>
+        <item name="materialColorPrimaryContainer">@color/system_primary_container_dark</item>
+        <item name="materialColorOnBackground">@color/system_on_background_dark</item>
+        <item name="materialColorPrimaryFixed">@color/system_primary_fixed</item>
+        <item name="materialColorOnSecondary">@color/system_on_secondary_dark</item>
+        <item name="materialColorOnTertiary">@color/system_on_tertiary_dark</item>
+        <item name="materialColorSurfaceDim">@color/system_surface_dim_dark</item>
+        <item name="materialColorSurfaceBright">@color/system_surface_bright_dark</item>
+        <item name="materialColorOnError">@color/system_on_error_dark</item>
+        <item name="materialColorSurface">@color/system_surface_dark</item>
+        <item name="materialColorSurfaceContainerHigh">@color/system_surface_container_high_dark</item>
+        <item name="materialColorSurfaceContainerHighest">@color/system_surface_container_highest_dark</item>
+        <item name="materialColorOnSurfaceVariant">@color/system_on_surface_variant_dark</item>
+        <item name="materialColorOutline">@color/system_outline_dark</item>
+        <item name="materialColorOutlineVariant">@color/system_outline_variant_dark</item>
+        <item name="materialColorOnPrimary">@color/system_on_primary_dark</item>
+        <item name="materialColorOnSurface">@color/system_on_surface_dark</item>
+        <item name="materialColorSurfaceContainer">@color/system_surface_container_dark</item>
+        <item name="materialColorPrimary">@color/system_primary_dark</item>
+        <item name="materialColorSecondary">@color/system_secondary_dark</item>
+        <item name="materialColorTertiary">@color/system_tertiary_dark</item>
+        <item name="materialColorError">@color/system_error_dark</item>
+    </style>
+
+    <style name="WidgetPickerActivityTheme" parent="@android:style/Theme.DeviceDefault.DayNight">
         <item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
         <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:colorBackgroundCacheHint">@null</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowAnimationStyle">@android:style/Animation</item>
+
+        <item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
         <item name="pageIndicatorDotColor">@color/page_indicator_dot_color_dark</item>
     </style>
 </resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 7b3f563..3402086 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -38,6 +38,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"App-paar is niet beschikbaar"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Tik en houd vast om een widget te verplaatsen."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dubbeltik en houd vast om een widget te verplaatsen of aangepaste acties te gebruiken."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Meer opties"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Alle widgets tonen"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d breed en %2$d hoog"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 33b7664..000ab13 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ଆପ ପେୟାର ଉପଲବ୍ଧ ନାହିଁ"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ଏକ ୱିଜେଟକୁ ମୁଭ୍ କରିବା ପାଇଁ ସ୍ପର୍ଶ କରି ଧରି ରଖନ୍ତୁ।"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ଏକ ୱିଜେଟକୁ ମୁଭ୍ କରିବା ପାଇଁ ଦୁଇଥର-ଟାପ୍ କରି ଧରି ରଖନ୍ତୁ କିମ୍ବା କଷ୍ଟମ୍ କାର୍ଯ୍ୟଗୁଡ଼ିକୁ ବ୍ୟବହାର କରନ୍ତୁ।"</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d ଓସାର ଓ %2$d ଉଚ୍ଚ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ୱିଜେଟ୍"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 71c25eb..6e558c3 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ਐਪ ਜੋੜਾਬੱਧ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ਕਿਸੇ ਵਿਜੇਟ ਨੂੰ ਲਿਜਾਉਣ ਲਈ ਸਪਰਸ਼ ਕਰਕੇ ਰੱਖੋ।"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ਵਿਜੇਟ ਲਿਜਾਉਣ ਲਈ ਜਾਂ ਵਿਉਂਂਤੀਆਂ ਕਾਰਵਾਈਆਂ ਵਰਤਣ ਲਈ ਦੋ ਵਾਰ ਟੈਪ ਕਰਕੇ ਦਬਾ ਕੇ ਰੱਖੋ।"</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d ਚੌੜਾਈ ਅਤੇ %2$d ਲੰਬਾਈ"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ਵਿਜੇਟ"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 587c40f..c2aaf69 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Para aplikacji jest niedostępna"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Naciśnij i przytrzymaj, aby przenieść widżet."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Naciśnij dwukrotnie i przytrzymaj, aby przenieść widżet lub użyć działań niestandardowych."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Szerokość %1$d, wysokość %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widżet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 67d93de..1f98805 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -38,6 +38,8 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"O par de apps não está disponível"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Toque sem soltar para mover um widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Toque duas vezes sem soltar para mover um widget ou utilizar ações personalizadas."</string>
+    <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Mais opções"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Mostrar todos os widgets"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d de largura por %2$d de altura"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 4302588..10cf03f 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"O Par de apps não está disponível"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Toque e pressione para mover um widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Toque duas vezes e mantenha a tela pressionada para mover um widget ou usar ações personalizadas."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d de largura por %2$d de altura"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index d9e0413..510f671 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Perechea de aplicații nu este disponibilă"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Atinge și ține apăsat pentru a muta un widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Atinge de două ori și ține apăsat pentru a muta un widget sau folosește acțiuni personalizate."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d lățime și %2$d înălțime"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widgetul <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 64a21f7..3add9fa 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Одновременное использование двух приложений недоступно"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Чтобы переместить виджет, нажмите на него и удерживайте"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Чтобы использовать специальные действия или перенести виджет, нажмите на него дважды и удерживайте."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d x %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Ширина %1$d, высота %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Виджет \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\""</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 71efc03..2adccc3 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"යෙදුම් යුගලයක් නොමැත"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"විජට් එකක් ගෙන යාමට ස්පර්ශ කර අල්ලා ගෙන සිටින්න."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"විජට් එකක් ගෙන යාමට හෝ අභිරුචි ක්‍රියා භාවිත කිරීමට දෙවරක් තට්ටු කර අල්ලා ගෙන සිටින්න."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"පළල %1$d උස %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> විජට්ටුව"</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 43bc29c..2a44faf 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Pár aplikácií nie je k dispozícii"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Pridržaním presuňte miniaplikáciu."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dvojitým klepnutím a pridržaním presuňte miniaplikáciu alebo použite vlastné akcie."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"šírka %1$d, výška %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Miniaplikácia <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 5d423ca..638f9e4 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Par aplikacij ni na voljo"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Pridržite pripomoček, da ga premaknete."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dvakrat se dotaknite pripomočka in ga pridržite, da ga premaknete, ali pa uporabite dejanja po meri."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Širina %1$d, višina %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Pripomoček <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index 8f4133d..72e4f04 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Çifti i aplikacioneve nuk ofrohet"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Prek dhe mbaj shtypur një miniaplikacion për ta zhvendosur."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Trokit dy herë dhe mbaje shtypur një miniapliikacion për ta zhvendosur atë ose për të përdorur veprimet e personalizuara."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d i gjerë me %2$d i lartë"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> miniaplikacion"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index cd6523c..0252765 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Пар апликација није доступан"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Додирните и задржите ради померања виџета."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Двапут додирните и задржите да бисте померали виџет или користите прилагођене радње."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d×%2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"ширина од %1$d и висина од %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> виџет"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 4f95c67..6c03884 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"App-paret är inte tillgängligt"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Tryck länge för att flytta en widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Tryck snabbt två gånger och håll kvar för att flytta en widget eller använda anpassade åtgärder."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d bred gånger %2$d hög"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget för <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 53a9abe..cb93952 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Kipengele cha jozi ya programu hakipatikani"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Gusa na ushikilie ili usogeze wijeti."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Gusa mara mbili na ushikilie ili usogeze wijeti au utumie vitendo maalum."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Upana wa %1$d na kimo cha %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Wijeti ya <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 738f85c..ef3635c 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ஆப்ஸ் ஜோடி கிடைக்கவில்லை"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"விட்ஜெட்டை நகர்த்தத் தொட்டுப் பிடிக்கவும்."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"விட்ஜெட்டை நகர்த்த இருமுறை தட்டிப் பிடிக்கவும் அல்லது பிரத்தியேகச் செயல்களைப் பயன்படுத்தவும்."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d அகலத்திற்கு %2$d உயரம்"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> விட்ஜெட்"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 0520ebf..12dfb60 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"యాప్ పెయిర్ అందుబాటులో లేదు"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"విడ్జెట్‌ను తరలించడానికి తాకి &amp; నొక్కి ఉంచండి."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"విడ్జెట్‌ను తరలించడానికి లేదా అనుకూల చర్యలను ఉపయోగించడానికి రెండుసార్లు నొక్కండి &amp; హోల్డ్ చేయి."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d వెడల్పు X %2$d ఎత్తు"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> విడ్జెట్"</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 554fd94..9522e572 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"การจับคู่อุปกรณ์ไม่พร้อมให้บริการ"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"แตะค้างไว้เพื่อย้ายวิดเจ็ต"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"แตะสองครั้งค้างไว้เพื่อย้ายวิดเจ็ตหรือใช้การดำเนินการที่กำหนดเอง"</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"กว้าง %1$d x สูง %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"วิดเจ็ต <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index cb6fe66..6c27385 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Hindi available ang pares ng app"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Pindutin nang matagal para ilipat ang widget."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"I-double tap at pindutin nang matagal para ilipat ang widget o gumamit ng mga custom na pagkilos."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d ang lapad at %2$d ang taas"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index b12ec27..fe4d538 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Uygulama çifti kullanılamıyor"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Widget\'ı taşımak için dokunup basılı tutun."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Widget\'ı taşımak veya özel işlemleri kullanmak için iki kez dokunup basılı tutun."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"genişlik: %1$d, yükseklik: %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget\'ı"</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 2a01dae..cdb943e 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Одночасне використання двох додатків недоступне"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Натисніть і втримуйте, щоб перемістити віджет."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Двічі натисніть і втримуйте віджет, щоб перемістити його або виконати інші дії."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Ширина – %1$d, висота – %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Віджет <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 544357e..a12e8f9 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"ایپ کا جوڑا دستیاب نہیں ہے"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"ویجیٹ منتقل کرنے کے لیے ٹچ کریں اور پکڑ کر رکھیں۔"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"ویجیٹ کو منتقل کرنے یا حسب ضرورت کارروائیاں استعمال کرنے کے لیے دوبار تھپتھپائیں اور پکڑ کر رکھیں۔"</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"‏%1$d چوڑا اور ‎%2$d اونچا"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ویجیٹ"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index 5038d4f..ad5137e 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Ikkita ilovadan bir vaqtda foydalanish mumkin emas"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Vidjetni bosib turgan holatda suring."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Ikki marta bosib va bosib turgan holatda vidjetni tanlang yoki maxsus amaldan foydalaning."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Eni %1$d, bo‘yi %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ta vidjet"</string>
diff --git a/res/values-v30/styles.xml b/res/values-v30/styles.xml
index ec5c113..a5c57c8 100644
--- a/res/values-v30/styles.xml
+++ b/res/values-v30/styles.xml
@@ -29,5 +29,6 @@
         <item name="android:windowLayoutInDisplayCutoutMode">always</item>
         <item name="android:enforceStatusBarContrast">false</item>
         <item name="android:enforceNavigationBarContrast">false</item>
+        <item name="materialColorOnPrimaryFixed">#FFFFFFFF</item>
     </style>
 </resources>
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index 7270366..5c81d49 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -110,39 +110,4 @@
         @android:color/system_accent1_900</color>
 
     <color name="overview_foreground_scrim_color">@android:color/system_neutral1_1000</color>
-
-    <color name="material_color_on_secondary_fixed_variant">@android:color/system_accent2_700</color>
-    <color name="material_color_on_tertiary_fixed_variant">@android:color/system_accent3_700</color>
-    <color name="material_color_on_primary_fixed_variant">@android:color/system_accent1_700</color>
-    <color name="material_color_on_secondary_container">@android:color/system_accent2_900</color>
-    <color name="material_color_on_tertiary_container">@android:color/system_accent3_900</color>
-    <color name="material_color_on_primary_container">@android:color/system_accent1_900</color>
-    <color name="material_color_secondary_fixed_dim">@android:color/system_accent2_200</color>
-    <color name="material_color_on_error_container">#410000</color>
-    <color name="material_color_on_secondary_fixed">@android:color/system_accent2_900</color>
-    <color name="material_color_on_surface_inverse">@android:color/system_neutral1_100</color>
-    <color name="material_color_tertiary_fixed_dim">@android:color/system_accent3_200</color>
-    <color name="material_color_on_tertiary_fixed">@android:color/system_accent3_900</color>
-    <color name="material_color_primary_fixed_dim">@android:color/system_accent1_200</color>
-    <color name="material_color_secondary_container">@android:color/system_accent2_100</color>
-    <color name="material_color_error_container">#FFDAD5</color>
-    <color name="material_color_on_primary_fixed">@android:color/system_accent1_900</color>
-    <color name="material_color_primary_inverse">@android:color/system_accent1_200</color>
-    <color name="material_color_secondary_fixed">@android:color/system_accent2_100</color>
-    <color name="material_color_tertiary_container">@android:color/system_accent3_100</color>
-    <color name="material_color_tertiary_fixed">@android:color/system_accent3_100</color>
-    <color name="material_color_primary_container">@android:color/system_accent1_100</color>
-    <color name="material_color_on_background">@android:color/system_neutral1_50</color>
-    <color name="material_color_primary_fixed">@android:color/system_accent1_100</color>
-    <color name="material_color_on_secondary">@android:color/system_accent2_0</color>
-    <color name="material_color_on_tertiary">@android:color/system_accent3_0</color>
-    <color name="material_color_on_error">#FFFFFF</color>
-    <color name="material_color_on_surface_variant">@android:color/system_neutral2_700</color>
-    <color name="material_color_outline">@android:color/system_neutral2_500</color>
-    <color name="material_color_outline_variant">@android:color/system_neutral2_200</color>
-    <color name="material_color_on_primary">@android:color/system_accent1_0</color>
-    <color name="material_color_on_surface">@android:color/system_neutral1_900</color>
-    <color name="material_color_primary">@android:color/system_accent1_600</color>
-    <color name="material_color_secondary">@android:color/system_accent2_600</color>
-    <color name="material_color_tertiary">@android:color/system_accent3_600</color>
 </resources>
diff --git a/res/values-v34/colors.xml b/res/values-v34/colors.xml
index 26d3712..4f3a769 100644
--- a/res/values-v34/colors.xml
+++ b/res/values-v34/colors.xml
@@ -27,4 +27,108 @@
         @android:color/system_on_surface_light</color>
     <color name="widget_cell_subtitle_color_light">
         @android:color/system_on_surface_variant_light</color>
+    <color name="widget_picker_menu_options_color_light">
+        @android:color/system_on_surface_variant_light
+    </color>
+
+    <color name="system_primary_container_light">@android:color/system_primary_container_light</color>
+    <color name="system_on_primary_container_light">@android:color/system_on_primary_container_light</color>
+    <color name="system_primary_light">@android:color/system_primary_light</color>
+    <color name="system_on_primary_light">@android:color/system_on_primary_light</color>
+    <color name="system_secondary_container_light">@android:color/system_secondary_container_light</color>
+    <color name="system_on_secondary_container_light">@android:color/system_on_secondary_container_light</color>
+    <color name="system_secondary_light">@android:color/system_secondary_light</color>
+    <color name="system_on_secondary_light">@android:color/system_on_secondary_light</color>
+    <color name="system_tertiary_container_light">@android:color/system_tertiary_container_light</color>
+    <color name="system_on_tertiary_container_light">@android:color/system_on_tertiary_container_light</color>
+    <color name="system_tertiary_light">@android:color/system_tertiary_light</color>
+    <color name="system_on_tertiary_light">@android:color/system_on_tertiary_light</color>
+    <color name="system_background_light">@android:color/system_background_light</color>
+    <color name="system_on_background_light">@android:color/system_on_background_light</color>
+    <color name="system_surface_light">@android:color/system_surface_light</color>
+    <color name="system_on_surface_light">@android:color/system_on_surface_light</color>
+    <color name="system_surface_container_low_light">@android:color/system_surface_container_low_light</color>
+    <color name="system_surface_container_lowest_light">@android:color/system_surface_container_lowest_light</color>
+    <color name="system_surface_container_light">@android:color/system_surface_container_light</color>
+    <color name="system_surface_container_high_light">@android:color/system_surface_container_high_light</color>
+    <color name="system_surface_container_highest_light">@android:color/system_surface_container_highest_light</color>
+    <color name="system_surface_bright_light">@android:color/system_surface_bright_light</color>
+    <color name="system_surface_dim_light">@android:color/system_surface_dim_light</color>
+    <color name="system_surface_variant_light">@android:color/system_surface_variant_light</color>
+    <color name="system_on_surface_variant_light">@android:color/system_on_surface_variant_light</color>
+    <color name="system_outline_light">@android:color/system_outline_light</color>
+    <color name="system_outline_variant_light">@android:color/system_outline_variant_light</color>
+    <color name="system_error_light">@android:color/system_error_light</color>
+    <color name="system_on_error_light">@android:color/system_on_error_light</color>
+    <color name="system_error_container_light">@android:color/system_error_container_light</color>
+    <color name="system_on_error_container_light">@android:color/system_on_error_container_light</color>
+    <color name="system_control_activated_light">@android:color/system_control_activated_light</color>
+    <color name="system_control_normal_light">@android:color/system_control_normal_light</color>
+    <color name="system_control_highlight_light">@android:color/system_control_highlight_light</color>
+    <color name="system_text_primary_inverse_light">@android:color/system_text_primary_inverse_light</color>
+    <color name="system_text_secondary_and_tertiary_inverse_light">@android:color/system_text_secondary_and_tertiary_inverse_light</color>
+    <color name="system_text_primary_inverse_disable_only_light">@android:color/system_text_primary_inverse_disable_only_light</color>
+    <color name="system_text_secondary_and_tertiary_inverse_disabled_light">@android:color/system_text_secondary_and_tertiary_inverse_disabled_light</color>
+    <color name="system_text_hint_inverse_light">@android:color/system_text_hint_inverse_light</color>
+    <color name="system_palette_key_color_primary_light">@android:color/system_palette_key_color_primary_light</color>
+    <color name="system_palette_key_color_secondary_light">@android:color/system_palette_key_color_secondary_light</color>
+    <color name="system_palette_key_color_tertiary_light">@android:color/system_palette_key_color_tertiary_light</color>
+    <color name="system_palette_key_color_neutral_light">@android:color/system_palette_key_color_neutral_light</color>
+    <color name="system_palette_key_color_neutral_variant_light">@android:color/system_palette_key_color_neutral_variant_light</color>
+    <color name="system_primary_container_dark">@android:color/system_primary_container_dark</color>
+    <color name="system_on_primary_container_dark">@android:color/system_on_primary_container_dark</color>
+    <color name="system_primary_dark">@android:color/system_primary_dark</color>
+    <color name="system_on_primary_dark">@android:color/system_on_primary_dark</color>
+    <color name="system_secondary_container_dark">@android:color/system_secondary_container_dark</color>
+    <color name="system_on_secondary_container_dark">@android:color/system_on_secondary_container_dark</color>
+    <color name="system_secondary_dark">@android:color/system_secondary_dark</color>
+    <color name="system_on_secondary_dark">@android:color/system_on_secondary_dark</color>
+    <color name="system_tertiary_container_dark">@android:color/system_tertiary_container_dark</color>
+    <color name="system_on_tertiary_container_dark">@android:color/system_on_tertiary_container_dark</color>
+    <color name="system_tertiary_dark">@android:color/system_tertiary_dark</color>
+    <color name="system_on_tertiary_dark">@android:color/system_on_tertiary_dark</color>
+    <color name="system_background_dark">@android:color/system_background_dark</color>
+    <color name="system_on_background_dark">@android:color/system_on_background_dark</color>
+    <color name="system_surface_dark">@android:color/system_surface_dark</color>
+    <color name="system_on_surface_dark">@android:color/system_on_surface_dark</color>
+    <color name="system_surface_container_low_dark">@android:color/system_surface_container_low_dark</color>
+    <color name="system_surface_container_lowest_dark">@android:color/system_surface_container_lowest_dark</color>
+    <color name="system_surface_container_dark">@android:color/system_surface_container_dark</color>
+    <color name="system_surface_container_high_dark">@android:color/system_surface_container_high_dark</color>
+    <color name="system_surface_container_highest_dark">@android:color/system_surface_container_highest_dark</color>
+    <color name="system_surface_bright_dark">@android:color/system_surface_bright_dark</color>
+    <color name="system_surface_dim_dark">@android:color/system_surface_dim_dark</color>
+    <color name="system_surface_variant_dark">@android:color/system_surface_variant_dark</color>
+    <color name="system_on_surface_variant_dark">@android:color/system_on_surface_variant_dark</color>
+    <color name="system_outline_dark">@android:color/system_outline_dark</color>
+    <color name="system_outline_variant_dark">@android:color/system_outline_variant_dark</color>
+    <color name="system_error_dark">@android:color/system_error_dark</color>
+    <color name="system_on_error_dark">@android:color/system_on_error_dark</color>
+    <color name="system_error_container_dark">@android:color/system_error_container_dark</color>
+    <color name="system_on_error_container_dark">@android:color/system_on_error_container_dark</color>
+    <color name="system_control_activated_dark">@android:color/system_control_activated_dark</color>
+    <color name="system_control_normal_dark">@android:color/system_control_normal_dark</color>
+    <color name="system_control_highlight_dark">@android:color/system_control_highlight_dark</color>
+    <color name="system_text_primary_inverse_dark">@android:color/system_text_primary_inverse_dark</color>
+    <color name="system_text_secondary_and_tertiary_inverse_dark">@android:color/system_text_secondary_and_tertiary_inverse_dark</color>
+    <color name="system_text_primary_inverse_disable_only_dark">@android:color/system_text_primary_inverse_disable_only_dark</color>
+    <color name="system_text_secondary_and_tertiary_inverse_disabled_dark">@android:color/system_text_secondary_and_tertiary_inverse_disabled_dark</color>
+    <color name="system_text_hint_inverse_dark">@android:color/system_text_hint_inverse_dark</color>
+    <color name="system_palette_key_color_primary_dark">@android:color/system_palette_key_color_primary_dark</color>
+    <color name="system_palette_key_color_secondary_dark">@android:color/system_palette_key_color_secondary_dark</color>
+    <color name="system_palette_key_color_tertiary_dark">@android:color/system_palette_key_color_tertiary_dark</color>
+    <color name="system_palette_key_color_neutral_dark">@android:color/system_palette_key_color_neutral_dark</color>
+    <color name="system_palette_key_color_neutral_variant_dark">@android:color/system_palette_key_color_neutral_variant_dark</color>
+    <color name="system_primary_fixed">@android:color/system_primary_fixed</color>
+    <color name="system_primary_fixed_dim">@android:color/system_primary_fixed_dim</color>
+    <color name="system_on_primary_fixed">@android:color/system_on_primary_fixed</color>
+    <color name="system_on_primary_fixed_variant">@android:color/system_on_primary_fixed_variant</color>
+    <color name="system_secondary_fixed">@android:color/system_secondary_fixed</color>
+    <color name="system_secondary_fixed_dim">@android:color/system_secondary_fixed_dim</color>
+    <color name="system_on_secondary_fixed">@android:color/system_on_secondary_fixed</color>
+    <color name="system_on_secondary_fixed_variant">@android:color/system_on_secondary_fixed_variant</color>
+    <color name="system_tertiary_fixed">@android:color/system_tertiary_fixed</color>
+    <color name="system_tertiary_fixed_dim">@android:color/system_tertiary_fixed_dim</color>
+    <color name="system_on_tertiary_fixed">@android:color/system_on_tertiary_fixed</color>
+    <color name="system_on_tertiary_fixed_variant">@android:color/system_on_tertiary_fixed_variant</color>
 </resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index e5252b1..bcb4b98 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Hiện không có cặp ứng dụng này"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Chạm và giữ để di chuyển một tiện ích."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Nhấn đúp và giữ để di chuyển một tiện ích hoặc sử dụng các thao tác tùy chỉnh."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Rộng %1$d x cao %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Tiện ích <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 7a76158..b36f7d6 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"应用对不可用"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"轻触并按住即可移动微件。"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"点按两次并按住微件即可移动该微件或使用自定义操作。"</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"宽 %1$d,高 %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"“<xliff:g id="WIDGET_NAME">%1$s</xliff:g>”微件"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index ed6e52f..d3de49c 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -27,7 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"在安全模式中無法使用小工具"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"沒有可用的捷徑"</string>
     <string name="home_screen" msgid="5629429142036709174">"主畫面"</string>
-    <string name="set_default_home_app" msgid="5808906607627586381">"前往「設定」將「<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>」設為預設主畫面應用程式"</string>
+    <string name="set_default_home_app" msgid="5808906607627586381">"在「設定」中將「<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>」設定為預設主頁應用程式"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"分割螢幕"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s 的應用程式資料"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"「%1$s」的用量設定"</string>
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"應用程式配對無法使用"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"輕觸並按住即可移動小工具。"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"㩒兩下之後㩒住,就可以郁小工具或者用自訂操作。"</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d 闊,%2$d 高"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"「<xliff:g id="WIDGET_NAME">%1$s</xliff:g>」小工具"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 264d607..5b65c35 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"這個應用程式配對無法使用"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"按住即可移動小工具。"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"輕觸兩下並按住即可移動小工具或使用自訂操作。"</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"寬度為 %1$d,高度為 %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"「<xliff:g id="WIDGET_NAME">%1$s</xliff:g>」小工具"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index be6cd49..fb6d8f2 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -38,6 +38,10 @@
     <string name="app_pair_not_available" msgid="3556767440808032031">"Ukubhangqwa kwe-app akutholakali"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Thinta uphinde ubambe ukuze uhambise iwijethi."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Thepha kabili uphinde ubambe ukuze uhambise iwijethi noma usebenzise izindlela ezingokwezifiso."</string>
+    <!-- no translation found for widget_picker_widget_options_button_description (4770099264476852363) -->
+    <skip />
+    <!-- no translation found for widget_picker_show_all_widgets_menu_item_title (9023638224586908119) -->
+    <skip />
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d ububanzi ngokungu-%2$d ukuya phezulu"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Iwijethi elingu-<xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index e4e047e..6151b5f 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -45,6 +45,55 @@
     <attr name="focusOutlineColor" format="color" />
     <attr name="focusInnerOutlineColor" format="color" />
 
+    <!--    Recreating Dynamic Color attributes found in the system. This should be the way to go on
+     all launcher projects since they all inherit from launcher3. Avoid creating other color
+     attributes if these can be user directly. -->
+    <attr name="materialColorOnSecondaryFixedVariant" format="color" />
+    <attr name="materialColorOnTertiaryFixedVariant" format="color" />
+    <attr name="materialColorSurfaceContainerLowest" format="color" />
+    <attr name="materialColorOnPrimaryFixedVariant" format="color" />
+    <attr name="materialColorOnSecondaryContainer" format="color" />
+    <attr name="materialColorOnTertiaryContainer" format="color" />
+    <attr name="materialColorSurfaceContainerLow" format="color" />
+    <attr name="materialColorOnPrimaryContainer" format="color" />
+    <attr name="materialColorSecondaryFixedDim" format="color" />
+    <attr name="materialColorOnErrorContainer" format="color" />
+    <attr name="materialColorOnSecondaryFixed" format="color" />
+    <attr name="materialColorOnSurfaceInverse" format="color" />
+    <attr name="materialColorTertiaryFixedDim" format="color" />
+    <attr name="materialColorOnTertiaryFixed" format="color" />
+    <attr name="materialColorPrimaryFixedDim" format="color" />
+    <attr name="materialColorSecondaryContainer" format="color" />
+    <attr name="materialColorErrorContainer" format="color" />
+    <attr name="materialColorOnPrimaryFixed" format="color" />
+    <attr name="materialColorPrimaryInverse" format="color" />
+    <attr name="materialColorSecondaryFixed" format="color" />
+    <attr name="materialColorSurfaceInverse" format="color" />
+    <attr name="materialColorSurfaceVariant" format="color" />
+    <attr name="materialColorTertiaryContainer" format="color" />
+    <attr name="materialColorTertiaryFixed" format="color" />
+    <attr name="materialColorPrimaryContainer" format="color" />
+    <attr name="materialColorOnBackground" format="color" />
+    <attr name="materialColorPrimaryFixed" format="color" />
+    <attr name="materialColorOnSecondary" format="color" />
+    <attr name="materialColorOnTertiary" format="color" />
+    <attr name="materialColorSurfaceDim" format="color" />
+    <attr name="materialColorSurfaceBright" format="color" />
+    <attr name="materialColorOnError" format="color" />
+    <attr name="materialColorSurface" format="color" />
+    <attr name="materialColorSurfaceContainerHigh" format="color" />
+    <attr name="materialColorSurfaceContainerHighest" format="color" />
+    <attr name="materialColorOnSurfaceVariant" format="color" />
+    <attr name="materialColorOutline" format="color" />
+    <attr name="materialColorOutlineVariant" format="color" />
+    <attr name="materialColorOnPrimary" format="color" />
+    <attr name="materialColorOnSurface" format="color" />
+    <attr name="materialColorSurfaceContainer" format="color" />
+    <attr name="materialColorPrimary" format="color" />
+    <attr name="materialColorSecondary" format="color" />
+    <attr name="materialColorTertiary" format="color" />
+    <attr name="materialColorError" format="color" />
+
     <attr name="pageIndicatorDotColor" format="color" />
     <attr name="folderPreviewColor" format="color" />
     <attr name="folderBackgroundColor" format="color" />
@@ -62,6 +111,7 @@
     <attr name="preloadIconBackgroundColor" format="color" />
     <attr name="widgetPickerTitleColor" format="color"/>
     <attr name="widgetPickerDescriptionColor" format="color"/>
+    <attr name="widgetPickerWidgetOptionsMenuColor" format="color"/>
     <attr name="widgetPickerPrimarySurfaceColor" format="color"/>
     <attr name="widgetPickerSecondarySurfaceColor" format="color"/>
     <attr name="widgetPickerHeaderAppTitleColor" format="color"/>
@@ -581,51 +631,6 @@
         <attr name="collapsable" format="boolean" />
     </declare-styleable>
 
-    <attr name="materialColorOnSecondaryFixedVariant" format="color" />
-    <attr name="materialColorOnTertiaryFixedVariant" format="color" />
-    <attr name="materialColorSurfaceContainerLowest" format="color" />
-    <attr name="materialColorOnPrimaryFixedVariant" format="color" />
-    <attr name="materialColorOnSecondaryContainer" format="color" />
-    <attr name="materialColorOnTertiaryContainer" format="color" />
-    <attr name="materialColorSurfaceContainerLow" format="color" />
-    <attr name="materialColorOnPrimaryContainer" format="color" />
-    <attr name="materialColorSecondaryFixedDim" format="color" />
-    <attr name="materialColorOnErrorContainer" format="color" />
-    <attr name="materialColorOnSecondaryFixed" format="color" />
-    <attr name="materialColorOnSurfaceInverse" format="color" />
-    <attr name="materialColorTertiaryFixedDim" format="color" />
-    <attr name="materialColorOnTertiaryFixed" format="color" />
-    <attr name="materialColorPrimaryFixedDim" format="color" />
-    <attr name="materialColorSecondaryContainer" format="color" />
-    <attr name="materialColorErrorContainer" format="color" />
-    <attr name="materialColorOnPrimaryFixed" format="color" />
-    <attr name="materialColorPrimaryInverse" format="color" />
-    <attr name="materialColorSecondaryFixed" format="color" />
-    <attr name="materialColorTertiaryContainer" format="color" />
-    <attr name="materialColorTertiaryFixed" format="color" />
-    <attr name="materialColorPrimaryContainer" format="color" />
-    <attr name="materialColorOnBackground" format="color" />
-    <attr name="materialColorPrimaryFixed" format="color" />
-    <attr name="materialColorOnSecondary" format="color" />
-    <attr name="materialColorOnTertiary" format="color" />
-    <attr name="materialColorOnError" format="color" />
-    <attr name="materialColorOnSurfaceVariant" format="color" />
-    <attr name="materialColorOutline" format="color" />
-    <attr name="materialColorOutlineVariant" format="color" />
-    <attr name="materialColorOnPrimary" format="color" />
-    <attr name="materialColorOnSurface" format="color" />
-    <attr name="materialColorPrimary" format="color" />
-    <attr name="materialColorSecondary" format="color" />
-    <attr name="materialColorTertiary" format="color" />
-    <attr name="materialColorSurfaceInverse" format="color" />
-    <attr name="materialColorSurfaceVariant" format="color" />
-    <attr name="materialColorSurfaceDim" format="color" />
-    <attr name="materialColorSurfaceBright" format="color" />
-    <attr name="materialColorSurface" format="color" />
-    <attr name="materialColorSurfaceContainerHigh" format="color" />
-    <attr name="materialColorSurfaceContainerHighest" format="color" />
-    <attr name="materialColorSurfaceContainer" format="color" />
-
     <declare-styleable name="WidgetSections">
         <!-- Component name of an app widget provider. -->
         <attr name="provider" format="string" />
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 8fa1992..c82358b 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -17,8 +17,7 @@
 ** limitations under the License.
 */
 -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
     <!-- The color tints to apply to the text and drag view when hovering
          over the delete target or the info target -->
     <color name="delete_target_hover_tint">#FFC1C1C1</color>
@@ -105,6 +104,7 @@
     <color name="widget_picker_secondary_surface_color_light">#FAF9F8</color>
     <color name="widget_picker_title_color_light">#1F1F1F</color>
     <color name="widget_picker_description_color_light">#4C4D50</color>
+    <color name="widget_picker_menu_options_color_light">@color/system_on_surface_variant_light</color>
     <color name="widget_picker_header_app_title_color_light">#1F1F1F</color>
     <color name="widget_picker_header_app_subtitle_color_light">#444746</color>
     <color name="widget_picker_header_background_color_light">#C2E7FF</color>
@@ -118,13 +118,14 @@
     <color name="widget_picker_collapse_handle_color_light">#C4C7C5</color>
     <color name="widget_picker_add_button_background_color_light">#0B57D0</color>
     <color name="widget_picker_add_button_text_color_light">#0B57D0</color>
-    <color name="widget_cell_title_color_light">@color/material_color_on_surface</color>
-    <color name="widget_cell_subtitle_color_light">@color/material_color_on_surface_variant</color>
+    <color name="widget_cell_title_color_light">@color/system_on_surface_light</color>
+    <color name="widget_cell_subtitle_color_light">@color/system_on_surface_variant_light</color>
 
     <color name="widget_picker_primary_surface_color_dark">#1F2020</color>
     <color name="widget_picker_secondary_surface_color_dark">#393939</color>
     <color name="widget_picker_title_color_dark">#E3E3E3</color>
     <color name="widget_picker_description_color_dark">#CCCDCF</color>
+    <color name="widget_picker_menu_options_color_dark">@color/system_on_surface_variant_dark</color>
     <color name="widget_picker_header_app_title_color_dark">#E3E3E3</color>
     <color name="widget_picker_header_app_subtitle_color_dark">#C4C7C5</color>
     <color name="widget_picker_header_background_color_dark">#004A77</color>
@@ -138,51 +139,107 @@
     <color name="widget_picker_collapse_handle_color_dark">#444746</color>
     <color name="widget_picker_add_button_background_color_dark">#062E6F</color>
     <color name="widget_picker_add_button_text_color_dark">#FFFFFF</color>
-    <color name="widget_cell_title_color_dark">@color/material_color_on_surface</color>
-    <color name="widget_cell_subtitle_color_dark">@color/material_color_on_surface_variant</color>
+    <color name="widget_cell_title_color_dark">@color/system_on_surface_dark</color>
+    <color name="widget_cell_subtitle_color_dark">@color/system_on_surface_variant_dark</color>
 
-    <color name="material_color_on_secondary_fixed_variant">#3F4759</color>
-    <color name="material_color_on_tertiary_fixed_variant">#583E5B</color>
-    <color name="material_color_surface_container_lowest">#FFFFFF</color>
-    <color name="material_color_on_primary_fixed_variant">#2B4678</color>
-    <color name="material_color_on_secondary_container">#141B2C</color>
-    <color name="material_color_on_tertiary_container">#29132D</color>
-    <color name="material_color_surface_container_low">#F5F3F7</color>
-    <color name="material_color_on_primary_container">#001A41</color>
-    <color name="material_color_secondary_fixed_dim">#BFC6DC</color>
-    <color name="material_color_on_error_container">#410000</color>
-    <color name="material_color_on_secondary_fixed">#141B2C</color>
-    <color name="material_color_on_surface_inverse">#E3E2E6</color>
-    <color name="material_color_tertiary_fixed_dim">#DEBCDF</color>
-    <color name="material_color_on_tertiary_fixed">#29132D</color>
-    <color name="material_color_primary_fixed_dim">#ADC6FF</color>
-    <color name="material_color_secondary_container">#DBE2F9</color>
-    <color name="material_color_error_container">#FFDAD5</color>
-    <color name="material_color_on_primary_fixed">#001A41</color>
-    <color name="material_color_primary_inverse">#ADC6FF</color>
-    <color name="material_color_secondary_fixed">#DBE2F9</color>
-    <color name="material_color_surface_inverse">#121316</color>
-    <color name="material_color_surface_variant">#E1E2EC</color>
-    <color name="material_color_tertiary_container">#FBD7FC</color>
-    <color name="material_color_tertiary_fixed">#FBD7FC</color>
-    <color name="material_color_primary_container">#D8E2FF</color>
-    <color name="material_color_on_background">#1B1B1F</color>
-    <color name="material_color_primary_fixed">#D8E2FF</color>
-    <color name="material_color_on_secondary">#FFFFFF</color>
-    <color name="material_color_on_tertiary">#FFFFFF</color>
-    <color name="material_color_surface_dim">#DBD9DD</color>
-    <color name="material_color_surface_bright">#FAF9FD</color>
-    <color name="material_color_on_error">#FFFFFF</color>
-    <color name="material_color_surface">#FAF9FD</color>
-    <color name="material_color_surface_container_high">#E9E7EC</color>
-    <color name="material_color_surface_container_highest">#E3E2E6</color>
-    <color name="material_color_on_surface_variant">#44474F</color>
-    <color name="material_color_outline">#72747D</color>
-    <color name="material_color_outline_variant">#C4C7C5</color>
-    <color name="material_color_on_primary">#FFFFFF</color>
-    <color name="material_color_on_surface">#1B1B1F</color>
-    <color name="material_color_surface_container">#EFEDF1</color>
-    <color name="material_color_primary">#445E91</color>
-    <color name="material_color_secondary">#575E71</color>
-    <color name="material_color_tertiary">#715573</color>
+    <color name="system_primary_container_light">#D9E2FF</color>
+    <color name="system_on_primary_container_light">#001945</color>
+    <color name="system_primary_light">#475D92</color>
+    <color name="system_on_primary_light">#FFFFFF</color>
+    <color name="system_secondary_container_light">#DCE2F9</color>
+    <color name="system_on_secondary_container_light">#151B2C</color>
+    <color name="system_secondary_light">#575E71</color>
+    <color name="system_on_secondary_light">#FFFFFF</color>
+    <color name="system_tertiary_container_light">#FDD7FA</color>
+    <color name="system_on_tertiary_container_light">#2A122C</color>
+    <color name="system_tertiary_light">#725572</color>
+    <color name="system_on_tertiary_light">#FFFFFF</color>
+    <color name="system_background_light">#FAF8FF</color>
+    <color name="system_on_background_light">#1A1B20</color>
+    <color name="system_surface_light">#FAF8FF</color>
+    <color name="system_on_surface_light">#1A1B20</color>
+    <color name="system_surface_container_low_light">#F4F3FA</color>
+    <color name="system_surface_container_lowest_light">#FFFFFF</color>
+    <color name="system_surface_container_light">#EEEDF4</color>
+    <color name="system_surface_container_high_light">#E8E7EF</color>
+    <color name="system_surface_container_highest_light">#E2E2E9</color>
+    <color name="system_surface_bright_light">#FAF8FF</color>
+    <color name="system_surface_dim_light">#DAD9E0</color>
+    <color name="system_surface_variant_light">#E1E2EC</color>
+    <color name="system_on_surface_variant_light">#44464F</color>
+    <color name="system_outline_light">#757780</color>
+    <color name="system_outline_variant_light">#C5C6D0</color>
+    <color name="system_error_light">#BA1A1A</color>
+    <color name="system_on_error_light">#FFFFFF</color>
+    <color name="system_error_container_light">#FFDAD6</color>
+    <color name="system_on_error_container_light">#410002</color>
+    <color name="system_control_activated_light">#D9E2FF</color>
+    <color name="system_control_normal_light">#44464F</color>
+    <color name="system_control_highlight_light">#000000</color>
+    <color name="system_text_primary_inverse_light">#E2E2E9</color>
+    <color name="system_text_secondary_and_tertiary_inverse_light">#C5C6D0</color>
+    <color name="system_text_primary_inverse_disable_only_light">#E2E2E9</color>
+    <color name="system_text_secondary_and_tertiary_inverse_disabled_light">#E2E2E9</color>
+    <color name="system_text_hint_inverse_light">#E2E2E9</color>
+    <color name="system_palette_key_color_primary_light">#6076AC</color>
+    <color name="system_palette_key_color_secondary_light">#70778B</color>
+    <color name="system_palette_key_color_tertiary_light">#8C6D8C</color>
+    <color name="system_palette_key_color_neutral_light">#76777D</color>
+    <color name="system_palette_key_color_neutral_variant_light">#757780</color>
+    <color name="system_primary_container_dark">#2F4578</color>
+    <color name="system_on_primary_container_dark">#D9E2FF</color>
+    <color name="system_primary_dark">#B0C6FF</color>
+    <color name="system_on_primary_dark">#152E60</color>
+    <color name="system_secondary_container_dark">#404659</color>
+    <color name="system_on_secondary_container_dark">#DCE2F9</color>
+    <color name="system_secondary_dark">#C0C6DC</color>
+    <color name="system_on_secondary_dark">#2A3042</color>
+    <color name="system_tertiary_container_dark">#593D59</color>
+    <color name="system_on_tertiary_container_dark">#FDD7FA</color>
+    <color name="system_tertiary_dark">#E0BBDD</color>
+    <color name="system_on_tertiary_dark">#412742</color>
+    <color name="system_background_dark">#121318</color>
+    <color name="system_on_background_dark">#E2E2E9</color>
+    <color name="system_surface_dark">#121318</color>
+    <color name="system_on_surface_dark">#E2E2E9</color>
+    <color name="system_surface_container_low_dark">#1A1B20</color>
+    <color name="system_surface_container_lowest_dark">#0C0E13</color>
+    <color name="system_surface_container_dark">#1E1F25</color>
+    <color name="system_surface_container_high_dark">#282A2F</color>
+    <color name="system_surface_container_highest_dark">#33343A</color>
+    <color name="system_surface_bright_dark">#38393F</color>
+    <color name="system_surface_dim_dark">#121318</color>
+    <color name="system_surface_variant_dark">#44464F</color>
+    <color name="system_on_surface_variant_dark">#C5C6D0</color>
+    <color name="system_outline_dark">#8F9099</color>
+    <color name="system_outline_variant_dark">#44464F</color>
+    <color name="system_error_dark">#FFB4AB</color>
+    <color name="system_on_error_dark">#690005</color>
+    <color name="system_error_container_dark">#93000A</color>
+    <color name="system_on_error_container_dark">#FFDAD6</color>
+    <color name="system_control_activated_dark">#2F4578</color>
+    <color name="system_control_normal_dark">#C5C6D0</color>
+    <color name="system_control_highlight_dark">#FFFFFF</color>
+    <color name="system_text_primary_inverse_dark">#1A1B20</color>
+    <color name="system_text_secondary_and_tertiary_inverse_dark">#44464F</color>
+    <color name="system_text_primary_inverse_disable_only_dark">#1A1B20</color>
+    <color name="system_text_secondary_and_tertiary_inverse_disabled_dark">#1A1B20</color>
+    <color name="system_text_hint_inverse_dark">#1A1B20</color>
+    <color name="system_palette_key_color_primary_dark">#6076AC</color>
+    <color name="system_palette_key_color_secondary_dark">#70778B</color>
+    <color name="system_palette_key_color_tertiary_dark">#8C6D8C</color>
+    <color name="system_palette_key_color_neutral_dark">#76777D</color>
+    <color name="system_palette_key_color_neutral_variant_dark">#757780</color>
+    <color name="system_primary_fixed">#D9E2FF</color>
+    <color name="system_primary_fixed_dim">#B0C6FF</color>
+    <color name="system_on_primary_fixed">#001945</color>
+    <color name="system_on_primary_fixed_variant">#2F4578</color>
+    <color name="system_secondary_fixed">#DCE2F9</color>
+    <color name="system_secondary_fixed_dim">#C0C6DC</color>
+    <color name="system_on_secondary_fixed">#151B2C</color>
+    <color name="system_on_secondary_fixed_variant">#404659</color>
+    <color name="system_tertiary_fixed">#FDD7FA</color>
+    <color name="system_tertiary_fixed_dim">#E0BBDD</color>
+    <color name="system_on_tertiary_fixed">#2A122C</color>
+    <color name="system_on_tertiary_fixed_variant">#593D59</color>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 05724e2..af91b5a 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -172,8 +172,6 @@
     <dimen name="padded_rounded_button_height">48dp</dimen>
     <dimen name="rounded_button_height">48dp</dimen>
     <dimen name="rounded_button_radius">200dp</dimen>
-    <dimen name="rounded_button_padding">8dp</dimen>
-
 
     <!-- Widget tray -->
     <dimen name="widget_cell_vertical_padding">8dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d33adc4..3b458c2 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -61,6 +61,12 @@
     <string name="long_press_widget_to_add">Touch &amp; hold to move a widget.</string>
     <!-- Accessibility spoken hint message in widget picker, which allows user to add a widget. Custom action is the label for additional accessibility actions available in this mode [CHAR_LIMIT=100] -->
     <string name="long_accessible_way_to_add">Double-tap &amp; hold to move a widget or use custom actions.</string>
+    <!-- Accessibility label for the icon button shown in the widget picker that opens a overflow
+         menu with widgets viewing options. [CHAR_LIMIT=25] -->
+    <string name="widget_picker_widget_options_button_description">More options</string>
+    <!-- Label for the checkbox shown in the widget picker toggles whether to show all widgets or
+     the default set. [CHAR_LIMIT=25] -->
+    <string name="widget_picker_show_all_widgets_menu_item_title">Show all widgets</string>
     <!-- The format string for the dimensions of a widget in the drawer -->
     <!-- There is a special version of this format string for Farsi -->
     <string name="widget_dims_format">%1$d \u00d7 %2$d</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index f7273a0..ee7ed26 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -17,7 +17,7 @@
 */
 -->
 
-<resources>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
     <!-- Launcher theme -->
     <style name="BaseLauncherTheme" parent="@android:style/Theme.DeviceDefault.Light">
         <item name="disabledIconAlpha">.54</item>
@@ -29,17 +29,65 @@
         <item name="android:windowShowWallpaper">true</item>
     </style>
 
-    <style name="LauncherTheme" parent="@style/BaseLauncherTheme">
+    <style name="DynamicColorsBaseLauncherTheme" parent="@style/BaseLauncherTheme">
+        <item name="materialColorOnSecondaryFixedVariant">@color/system_on_secondary_fixed_variant</item>
+        <item name="materialColorOnTertiaryFixedVariant">@color/system_on_tertiary_fixed_variant</item>
+        <item name="materialColorSurfaceContainerLowest">@color/system_surface_container_lowest_light</item>
+        <item name="materialColorOnPrimaryFixedVariant">@color/system_on_primary_fixed_variant</item>
+        <item name="materialColorOnSecondaryContainer">@color/system_on_secondary_container_light</item>
+        <item name="materialColorOnTertiaryContainer">@color/system_on_tertiary_container_light</item>
+        <item name="materialColorSurfaceContainerLow">@color/system_surface_container_low_light</item>
+        <item name="materialColorOnPrimaryContainer">@color/system_on_primary_container_light</item>
+        <item name="materialColorSecondaryFixedDim">@color/system_secondary_fixed_dim</item>
+        <item name="materialColorOnErrorContainer">@color/system_on_error_container_light</item>
+        <item name="materialColorOnSecondaryFixed">@color/system_on_secondary_fixed</item>
+        <item name="materialColorOnSurfaceInverse">@color/system_on_surface_dark</item>
+        <item name="materialColorTertiaryFixedDim">@color/system_tertiary_fixed_dim</item>
+        <item name="materialColorOnTertiaryFixed">@color/system_on_tertiary_fixed</item>
+        <item name="materialColorPrimaryFixedDim">@color/system_primary_fixed_dim</item>
+        <item name="materialColorSecondaryContainer">@color/system_secondary_container_light</item>
+        <item name="materialColorErrorContainer">@color/system_error_container_light</item>
+        <item name="materialColorOnPrimaryFixed">@color/system_on_primary_fixed</item>
+        <item name="materialColorPrimaryInverse">@color/system_primary_dark</item>
+        <item name="materialColorSecondaryFixed">@color/system_secondary_fixed</item>
+        <item name="materialColorSurfaceInverse">@color/system_surface_dark</item>
+        <item name="materialColorSurfaceVariant">@color/system_surface_variant_light</item>
+        <item name="materialColorTertiaryContainer">@color/system_tertiary_container_light</item>
+        <item name="materialColorTertiaryFixed">@color/system_tertiary_fixed</item>
+        <item name="materialColorPrimaryContainer">@color/system_primary_container_light</item>
+        <item name="materialColorOnBackground">@color/system_on_background_light</item>
+        <item name="materialColorPrimaryFixed">@color/system_primary_fixed</item>
+        <item name="materialColorOnSecondary">@color/system_on_secondary_light</item>
+        <item name="materialColorOnTertiary">@color/system_on_tertiary_light</item>
+        <item name="materialColorSurfaceDim">@color/system_surface_dim_light</item>
+        <item name="materialColorSurfaceBright">@color/system_surface_bright_light</item>
+        <item name="materialColorOnError">@color/system_on_error_light</item>
+        <item name="materialColorSurface">@color/system_surface_light</item>
+        <item name="materialColorSurfaceContainerHigh">@color/system_surface_container_high_light</item>
+        <item name="materialColorSurfaceContainerHighest">@color/system_surface_container_highest_light</item>
+        <item name="materialColorOnSurfaceVariant">@color/system_on_surface_variant_light</item>
+        <item name="materialColorOutline">@color/system_outline_light</item>
+        <item name="materialColorOutlineVariant">@color/system_outline_variant_light</item>
+        <item name="materialColorOnPrimary">@color/system_on_primary_light</item>
+        <item name="materialColorOnSurface">@color/system_on_surface_light</item>
+        <item name="materialColorSurfaceContainer">@color/system_surface_container_light</item>
+        <item name="materialColorPrimary">@color/system_primary_light</item>
+        <item name="materialColorSecondary">@color/system_secondary_light</item>
+        <item name="materialColorTertiary">@color/system_tertiary_light</item>
+        <item name="materialColorError">@color/system_error_light</item>
+    </style>
+
+
+    <style name="LauncherTheme" parent="@style/DynamicColorsBaseLauncherTheme">
         <item name="android:textColorSecondary">#DE000000</item>
         <item name="allAppsScrimColor">?attr/materialColorSurfaceDim</item>
-        <item name="allappsHeaderProtectionColor">
-            @color/material_color_surface_container_highest</item>
+        <item name="allappsHeaderProtectionColor">?attr/materialColorSurfaceContainerHighest</item>
         <item name="allAppsNavBarScrimColor">#66FFFFFF</item>
         <item name="popupColorPrimary">@color/popup_color_primary_light</item>
         <item name="popupColorSecondary">@color/popup_color_secondary_light</item>
         <item name="popupColorTertiary">@color/popup_color_tertiary_light</item>
         <item name="popupColorBackground">#EFEDED</item>
-        <item name="popupTextColor">@color/popup_text_color_light</item>
+        <item name="popupTextColor">@color/system_on_surface_light</item>
         <item name="popupShadeFirst">@color/popup_shade_first_light</item>
         <item name="popupShadeSecond">@color/popup_shade_second_light</item>
         <item name="popupShadeThird">@color/popup_shade_third_light</item>
@@ -53,15 +101,15 @@
         <item name="workspaceKeyShadowColor">#89000000</item>
         <item name="widgetsTheme">@style/WidgetContainerTheme</item>
         <item name="pageIndicatorDotColor">@color/page_indicator_dot_color_light</item>
-        <item name="focusOutlineColor">@color/material_color_secondary_fixed</item>
-        <item name="focusInnerOutlineColor">@color/material_color_on_secondary_fixed_variant</item>
+        <item name="focusOutlineColor">?attr/materialColorSecondaryFixed</item>
+        <item name="focusInnerOutlineColor">?attr/materialColorOnSecondaryFixedVariant</item>
         <item name="folderPreviewColor">@color/folder_preview_light</item>
         <item name="folderBackgroundColor">@color/folder_background_light</item>
         <item name="folderIconBorderColor">?android:attr/colorPrimary</item>
         <item name="isFolderDarkText">true</item>
         <item name="folderTextColor">@color/folder_text_color_light</item>
         <item name="folderHintTextColor">@color/folder_hint_text_color_light</item>
-        <item name="appPairSurfaceInFolder">@color/material_color_surface_container_lowest</item>
+        <item name="appPairSurfaceInFolder">?attr/materialColorSurfaceContainerLowest</item>
         <item name="loadingIconColor">#CCFFFFFF</item>
         <item name="iconOnlyShortcutColor">?android:attr/textColorSecondary</item>
         <item name="eduHalfSheetBGColor">?android:attr/colorAccent</item>
@@ -78,51 +126,6 @@
         <item name="android:statusBarColor">#00000000</item>
         <item name="android:navigationBarColor">#00000000</item>
         <item name="android:switchStyle">@style/SwitchStyle</item>
-
-        <item name="materialColorOnSecondaryFixedVariant">@color/material_color_on_secondary_fixed_variant</item>
-        <item name="materialColorOnTertiaryFixedVariant">@color/material_color_on_tertiary_fixed_variant</item>
-        <item name="materialColorSurfaceContainerLowest">@color/material_color_surface_container_lowest</item>
-        <item name="materialColorOnPrimaryFixedVariant">@color/material_color_on_primary_fixed_variant</item>
-        <item name="materialColorOnSecondaryContainer">@color/material_color_on_secondary_container</item>
-        <item name="materialColorOnTertiaryContainer">@color/material_color_on_tertiary_container</item>
-        <item name="materialColorSurfaceContainerLow">@color/material_color_surface_container_low</item>
-        <item name="materialColorOnPrimaryContainer">@color/material_color_on_primary_container</item>
-        <item name="materialColorSecondaryFixedDim">@color/material_color_secondary_fixed_dim</item>
-        <item name="materialColorOnErrorContainer">@color/material_color_on_error_container</item>
-        <item name="materialColorOnSecondaryFixed">@color/material_color_on_secondary_fixed</item>
-        <item name="materialColorOnSurfaceInverse">@color/material_color_on_surface_inverse</item>
-        <item name="materialColorTertiaryFixedDim">@color/material_color_tertiary_fixed_dim</item>
-        <item name="materialColorOnTertiaryFixed">@color/material_color_on_tertiary_fixed</item>
-        <item name="materialColorPrimaryFixedDim">@color/material_color_primary_fixed_dim</item>
-        <item name="materialColorSecondaryContainer">@color/material_color_secondary_container</item>
-        <item name="materialColorErrorContainer">@color/material_color_error_container</item>
-        <item name="materialColorOnPrimaryFixed">@color/material_color_on_primary_fixed</item>
-        <item name="materialColorPrimaryInverse">@color/material_color_primary_inverse</item>
-        <item name="materialColorSecondaryFixed">@color/material_color_secondary_fixed</item>
-        <item name="materialColorSurfaceInverse">@color/material_color_surface_inverse</item>
-        <item name="materialColorSurfaceVariant">@color/material_color_surface_variant</item>
-        <item name="materialColorTertiaryContainer">@color/material_color_tertiary_container</item>
-        <item name="materialColorTertiaryFixed">@color/material_color_tertiary_fixed</item>
-        <item name="materialColorPrimaryContainer">@color/material_color_primary_container</item>
-        <item name="materialColorOnBackground">@color/material_color_on_background</item>
-        <item name="materialColorPrimaryFixed">@color/material_color_primary_fixed</item>
-        <item name="materialColorOnSecondary">@color/material_color_on_secondary</item>
-        <item name="materialColorOnTertiary">@color/material_color_on_tertiary</item>
-        <item name="materialColorSurfaceDim">@color/material_color_surface_dim</item>
-        <item name="materialColorSurfaceBright">@color/material_color_surface_bright</item>
-        <item name="materialColorOnError">@color/material_color_on_error</item>
-        <item name="materialColorSurface">@color/material_color_surface</item>
-        <item name="materialColorSurfaceContainerHigh">@color/material_color_surface_container_high</item>
-        <item name="materialColorSurfaceContainerHighest">@color/material_color_surface_container_highest</item>
-        <item name="materialColorOnSurfaceVariant">@color/material_color_on_surface_variant</item>
-        <item name="materialColorOutline">@color/material_color_outline</item>
-        <item name="materialColorOutlineVariant">@color/material_color_outline_variant</item>
-        <item name="materialColorOnPrimary">@color/material_color_on_primary</item>
-        <item name="materialColorOnSurface">@color/material_color_on_surface</item>
-        <item name="materialColorSurfaceContainer">@color/material_color_surface_container</item>
-        <item name="materialColorPrimary">@color/material_color_primary</item>
-        <item name="materialColorSecondary">@color/material_color_secondary</item>
-        <item name="materialColorTertiary">@color/material_color_tertiary</item>
     </style>
 
     <style name="SwitchStyle"
@@ -153,13 +156,13 @@
         <item name="android:colorControlHighlight">#19FFFFFF</item>
         <item name="android:colorPrimary">#FF212121</item>
         <item name="allAppsScrimColor">?attr/materialColorSurfaceDim</item>
-        <item name="allappsHeaderProtectionColor">@color/material_color_surface_container_low</item>
+        <item name="allappsHeaderProtectionColor">?attr/materialColorSurfaceContainerLow</item>
         <item name="allAppsNavBarScrimColor">#80000000</item>
         <item name="popupColorPrimary">@color/popup_color_primary_dark</item>
         <item name="popupColorSecondary">@color/popup_color_secondary_dark</item>
         <item name="popupColorTertiary">@color/popup_color_tertiary_dark</item>
         <item name="popupColorBackground">#1F2020</item>
-        <item name="popupTextColor">@color/popup_text_color_dark</item>
+        <item name="popupTextColor">@color/system_on_surface_dark</item>
         <item name="popupNotificationDotColor">@color/popup_notification_dot_dark</item>
         <item name="popupShadeFirst">@color/popup_shade_first_dark</item>
         <item name="popupShadeSecond">@color/popup_shade_second_dark</item>
@@ -173,7 +176,7 @@
         <item name="isFolderDarkText">false</item>
         <item name="folderTextColor">@color/folder_text_color_dark</item>
         <item name="folderHintTextColor">@color/folder_hint_text_color_dark</item>
-        <item name="appPairSurfaceInFolder">@color/material_color_surface_container_lowest</item>
+        <item name="appPairSurfaceInFolder">?attr/materialColorSurfaceContainerLowest</item>
         <item name="isMainColorDark">true</item>
         <item name="loadingIconColor">#99FFFFFF</item>
         <item name="iconOnlyShortcutColor">#B3FFFFFF</item>
@@ -244,6 +247,7 @@
             @color/widget_picker_secondary_surface_color_light</item>
         <item name="widgetPickerTitleColor">@color/widget_picker_title_color_light</item>
         <item name="widgetPickerDescriptionColor">@color/widget_picker_description_color_light</item>
+        <item name="widgetPickerWidgetOptionsMenuColor">@color/widget_picker_menu_options_color_light</item>
         <item name="widgetPickerHeaderAppTitleColor">
             @color/widget_picker_header_app_title_color_light</item>
         <item name="widgetPickerHeaderAppSubtitleColor">
@@ -284,6 +288,7 @@
         <item name="widgetPickerTitleColor">
             @color/widget_picker_title_color_dark</item>
         <item name="widgetPickerDescriptionColor">@color/widget_picker_description_color_dark</item>
+        <item name="widgetPickerWidgetOptionsMenuColor">@color/widget_picker_menu_options_color_dark</item>
         <item name="widgetPickerHeaderAppTitleColor">
             @color/widget_picker_header_app_title_color_dark</item>
         <item name="widgetPickerHeaderAppSubtitleColor">
@@ -463,7 +468,7 @@
 
     <style name="PrivateSpaceHeaderTextStyle">
         <item name="android:textSize">16sp</item>
-        <item name="android:textColor">@color/material_color_on_surface</item>
+        <item name="android:textColor">?attr/materialColorOnSurface</item>
         <item name="android:fontFamily">google-sans-text-medium</item>
         <item name="android:ellipsize">end</item>
     </style>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 83427a0..0cb2137 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -40,11 +40,15 @@
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.icu.text.MessageFormat;
+import android.text.Spannable;
+import android.text.SpannableString;
 import android.text.StaticLayout;
 import android.text.TextPaint;
 import android.text.TextUtils;
 import android.text.TextUtils.TruncateAt;
+import android.text.style.ImageSpan;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.util.Property;
 import android.util.Size;
 import android.util.TypedValue;
@@ -54,6 +58,7 @@
 import android.view.ViewDebug;
 import android.widget.TextView;
 
+import androidx.annotation.DrawableRes;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
@@ -96,6 +101,8 @@
 public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
         IconLabelDotView, DraggableView, Reorderable {
 
+    public static final String TAG = "BubbleTextView";
+
     public static final int DISPLAY_WORKSPACE = 0;
     public static final int DISPLAY_ALL_APPS = 1;
     public static final int DISPLAY_FOLDER = 2;
@@ -494,7 +501,13 @@
             mLastOriginalText = label;
             mLastModifiedText = mLastOriginalText;
             mBreakPointsIntArray = StringMatcherUtility.getListOfBreakpoints(label, MATCHER);
-            setText(label);
+            if (Flags.enableNewArchivingIcon()
+                    && info instanceof ItemInfoWithIcon infoWithIcon
+                    && infoWithIcon.isInactiveArchive()) {
+                setTextWithStartIcon(label, R.drawable.cloud_download_24px);
+            } else {
+                setText(label);
+            }
         }
         if (info.contentDescription != null) {
             setContentDescription(info.isDisabled()
@@ -804,7 +817,13 @@
                     getLineSpacingExtra());
             if (!TextUtils.equals(modifiedString, mLastModifiedText)) {
                 mLastModifiedText = modifiedString;
-                setText(modifiedString);
+                if (Flags.enableNewArchivingIcon()
+                        && getTag() instanceof ItemInfoWithIcon infoWithIcon
+                        && infoWithIcon.isInactiveArchive()) {
+                    setTextWithStartIcon(modifiedString, R.drawable.cloud_download_24px);
+                } else {
+                    setText(modifiedString);
+                }
                 // if text contains NEW_LINE, set max lines to 2
                 if (TextUtils.indexOf(modifiedString, NEW_LINE) != -1) {
                     setSingleLine(false);
@@ -825,6 +844,28 @@
         super.setTextColor(getModifiedColor());
     }
 
+    /**
+     * Uses a SpannableString to set text with a Drawable at the start of the TextView
+     * @param text text to use for TextView
+     * @param drawableRes Drawable Resource to use for drawing image at start of text
+     */
+    private void setTextWithStartIcon(CharSequence text, @DrawableRes int drawableRes) {
+        Drawable drawable = getContext().getDrawable(drawableRes);
+        if (drawable == null) {
+            setText(text);
+            Log.w(TAG, "setTextWithStartIcon: start icon Drawable not found from resources"
+                    + ", will just set text instead. text=" + text);
+            return;
+        }
+        drawable.setTint(getCurrentTextColor());
+        drawable.setBounds(0, 0, Math.round(getTextSize()), Math.round(getTextSize()));
+        ImageSpan imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_CENTER);
+        // First space will be replaced with Drawable, second space is for space before text.
+        SpannableString spannable = new SpannableString("  " + text);
+        spannable.setSpan(imageSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+        setText(spannable);
+    }
+
     @Override
     public void setTextColor(ColorStateList colors) {
         mTextColor = colors.getDefaultColor();
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index f775673..0d4ebe0 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -162,14 +162,14 @@
                 animatorSet.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_X, tx));
             }
         }
-        if (mQsb instanceof HorizontalInsettableView) {
-            HorizontalInsettableView horizontalInsettableQsb = (HorizontalInsettableView) mQsb;
-            ValueAnimator qsbAnimator = ValueAnimator.ofFloat(0f, 1f);
+        if (mQsb instanceof HorizontalInsettableView horizontalInsettableQsb) {
+            final float currentInsetFraction = horizontalInsettableQsb.getHorizontalInsets();
+            final float targetInsetFraction =
+                    isBubbleBarVisible ? (float) dp.iconSizePx / dp.hotseatQsbWidth : 0;
+            ValueAnimator qsbAnimator =
+                    ValueAnimator.ofFloat(currentInsetFraction, targetInsetFraction);
             qsbAnimator.addUpdateListener(animation -> {
-                float fraction = qsbAnimator.getAnimatedFraction();
-                float insetFraction = isBubbleBarVisible
-                        ? (float) dp.iconSizePx * fraction / dp.hotseatQsbWidth
-                        : (float) dp.iconSizePx * (1 - fraction) / dp.hotseatQsbWidth;
+                float insetFraction = (float) animation.getAnimatedValue();
                 horizontalInsettableQsb.setHorizontalInsets(insetFraction);
             });
             animatorSet.play(qsbAnimator);
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index cb897dc..6c706be 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -70,6 +70,7 @@
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
 import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
 import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE;
 import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.SHOW;
 import static com.android.launcher3.logging.StatsLogManager.EventEnum;
@@ -188,7 +189,6 @@
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.dragndrop.LauncherDragController;
 import com.android.launcher3.folder.Folder;
-import com.android.launcher3.folder.FolderGridOrganizer;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
@@ -266,6 +266,7 @@
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
 import com.android.launcher3.widget.util.WidgetSizes;
 import com.android.systemui.plugins.LauncherOverlayPlugin;
 import com.android.systemui.plugins.PluginListener;
@@ -371,6 +372,7 @@
     private LauncherAccessibilityDelegate mAccessibilityDelegate;
 
     private PopupDataProvider mPopupDataProvider;
+    private WidgetPickerDataProvider mWidgetPickerDataProvider;
 
     // We only want to get the SharedPreferences once since it does an FS stat each time we get
     // it from the context.
@@ -532,6 +534,7 @@
                 mFocusHandler, new CellLayout(mWorkspace.getContext(), mWorkspace));
 
         mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
+        mWidgetPickerDataProvider = new WidgetPickerDataProvider();
 
         boolean internalStateHandled = ACTIVITY_TRACKER.handleCreate(this);
         if (internalStateHandled) {
@@ -816,7 +819,7 @@
             View collectionIcon = getWorkspace().getHomescreenIconByItemId(info.container);
             if (collectionIcon instanceof FolderIcon folderIcon
                     && collectionIcon.getTag() instanceof FolderInfo) {
-                if (new FolderGridOrganizer(getDeviceProfile())
+                if (createFolderGridOrganizer(getDeviceProfile())
                         .setFolderInfo((FolderInfo) folderIcon.getTag())
                         .isItemInPreview(info.rank)) {
                     folderIcon.invalidate();
@@ -2702,6 +2705,7 @@
         mDragLayer.dump(prefix, writer);
         mStateManager.dump(prefix, writer);
         mPopupDataProvider.dump(prefix, writer);
+        mWidgetPickerDataProvider.dump(prefix, writer);
         mDeviceProfile.dump(this, prefix, writer);
         mAppsView.getAppsStore().dump(prefix, writer);
 
@@ -3011,6 +3015,12 @@
         return mPopupDataProvider;
     }
 
+    @NonNull
+    @Override
+    public WidgetPickerDataProvider getWidgetPickerDataProvider() {
+        return mWidgetPickerDataProvider;
+    }
+
     @Override
     public DotInfo getDotInfoForItem(ItemInfo info) {
         return mPopupDataProvider.getDotInfoForItem(info);
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 85c8b57..08ccfb2 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -115,6 +115,7 @@
         if (BuildCompat.isAtLeastV() && Flags.enableSupportForArchiving()) {
             ArchiveCompatibilityParams params = new ArchiveCompatibilityParams();
             params.setEnableUnarchivalConfirmation(false);
+            params.setEnableIconOverlay(!Flags.enableNewArchivingIcon());
             launcherApps.setArchiveCompatibility(params);
         }
 
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 83c34ce..d57f8a0 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -254,8 +254,8 @@
         PopupContainerWithArrow.dismissInvalidPopup(launcher)
     }
 
-    override fun bindAllWidgets(allWidgets: List<WidgetsListBaseEntry?>?) {
-        launcher.popupDataProvider.allWidgets = allWidgets
+    override fun bindAllWidgets(allWidgets: List<WidgetsListBaseEntry>) {
+        launcher.widgetPickerDataProvider.setWidgets(allWidgets, /* defaultWidgets= */ listOf())
     }
 
     /** Returns the ids of the workspaces to bind. */
@@ -304,7 +304,8 @@
         }
 
         val widgetsListBaseEntry: WidgetsListBaseEntry =
-            launcher.popupDataProvider.allWidgets.firstOrNull { item: WidgetsListBaseEntry ->
+            launcher.widgetPickerDataProvider.get().allWidgets.firstOrNull {
+                item: WidgetsListBaseEntry ->
                 item.mPkgItem.packageName == BuildConfig.APPLICATION_ID
             } ?: return
 
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 39e9dd1..fde7014 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -106,8 +106,6 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.function.Predicate;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 /**
  * Various utilities shared amongst the Launcher's classes.
@@ -116,8 +114,7 @@
 
     private static final String TAG = "Launcher.Utilities";
 
-    private static final Pattern sTrimPattern =
-            Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$");
+    private static final String TRIM_PATTERN = "(^\\h+|\\h+$)";
 
     private static final Matrix sMatrix = new Matrix();
     private static final Matrix sInverseMatrix = new Matrix();
@@ -450,10 +447,7 @@
         if (s == null) {
             return "";
         }
-
-        // Just strip any sequence of whitespace or java space characters from the beginning and end
-        Matcher m = sTrimPattern.matcher(s);
-        return m.replaceAll("$1");
+        return s.toString().replaceAll(TRIM_PATTERN, "").trim();
     }
 
     /**
@@ -727,9 +721,54 @@
     }
 
     /**
-     * Rotates `inOutBounds` by `delta` 90-degree increments. Rotation is visually CCW. Parent
+     * Rotates `inOutBounds` by `delta` 90-degree increments. Rotation is visually CW. Parent
      * sizes represent the "space" that will rotate carrying inOutBounds along with it to determine
      * the final bounds.
+     *
+     * As an example if this is the input:
+     * +-------------+
+     * |   +-----+   |
+     * |   |     |   |
+     * |   +-----+   |
+     * |             |
+     * |             |
+     * |             |
+     * +-------------+
+     * This would be case delta % 4 == 0:
+     * +-------------+
+     * |   +-----+   |
+     * |   |     |   |
+     * |   +-----+   |
+     * |             |
+     * |             |
+     * |             |
+     * +-------------+
+     * This would be case delta % 4 == 1:
+     * +----------------+
+     * |          +--+  |
+     * |          |  |  |
+     * |          |  |  |
+     * |          +--+  |
+     * |                |
+     * +----------------+
+     * This would be case delta % 4 == 2: // This is case was reverted to previous behaviour which
+     * doesn't match the illustration due to b/353965234
+     * +-------------+
+     * |             |
+     * |             |
+     * |             |
+     * |   +-----+   |
+     * |   |     |   |
+     * |   +-----+   |
+     * +-------------+
+     * This would be case delta % 4 == 3:
+     * +----------------+
+     * |  +--+          |
+     * |  |  |          |
+     * |  |  |          |
+     * |  +--+          |
+     * |                |
+     * +----------------+
      */
     public static void rotateBounds(Rect inOutBounds, int parentWidth, int parentHeight,
             int delta) {
@@ -835,6 +874,9 @@
             @NonNull Rect inclusionBounds,
             @NonNull Rect exclusionBounds,
             @AdjustmentDirection int adjustmentDirection) {
+        if (!Rect.intersects(targetViewBounds, exclusionBounds)) {
+            return;
+        }
         switch (adjustmentDirection) {
             case TRANSLATE_RIGHT:
                 targetView.setTranslationX(Math.min(
diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
deleted file mode 100644
index 79b8187..0000000
--- a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.accessibility;
-
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewGroup.OnHierarchyChangeListener;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.CellLayout;
-import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.dragndrop.DragController.DragListener;
-import com.android.launcher3.dragndrop.DragOptions;
-
-import java.util.function.Function;
-
-/**
- * Utility listener to enable/disable accessibility drag flags for a ViewGroup
- * containing CellLayouts
- */
-public class AccessibleDragListenerAdapter implements DragListener, OnHierarchyChangeListener {
-
-    private final ViewGroup mViewGroup;
-    private final Function<CellLayout, DragAndDropAccessibilityDelegate> mDelegateFactory;
-
-    /**
-     * @param parent the viewgroup containing all the children
-     * @param delegateFactory function to create no delegates
-     */
-    public AccessibleDragListenerAdapter(ViewGroup parent,
-            Function<CellLayout, DragAndDropAccessibilityDelegate> delegateFactory) {
-        mViewGroup = parent;
-        mDelegateFactory = delegateFactory;
-    }
-
-    @Override
-    public void onDragStart(DragObject dragObject, DragOptions options) {
-        mViewGroup.setOnHierarchyChangeListener(this);
-        enableAccessibleDrag(true, dragObject);
-    }
-
-    @Override
-    public void onDragEnd() {
-        mViewGroup.setOnHierarchyChangeListener(null);
-        enableAccessibleDrag(false, null);
-        Launcher.getLauncher(mViewGroup.getContext()).getDragController().removeDragListener(this);
-    }
-
-
-    @Override
-    public void onChildViewAdded(View parent, View child) {
-        if (parent == mViewGroup) {
-            setEnableForLayout((CellLayout) child, true);
-        }
-    }
-
-    @Override
-    public void onChildViewRemoved(View parent, View child) {
-        if (parent == mViewGroup) {
-            setEnableForLayout((CellLayout) child, false);
-        }
-    }
-
-    protected void enableAccessibleDrag(boolean enable, @Nullable DragObject dragObject) {
-        for (int i = 0; i < mViewGroup.getChildCount(); i++) {
-            setEnableForLayout((CellLayout) mViewGroup.getChildAt(i), enable);
-        }
-    }
-
-    protected final void setEnableForLayout(CellLayout layout, boolean enable) {
-        layout.setDragAndDropAccessibilityDelegate(enable ? mDelegateFactory.apply(layout) : null);
-    }
-}
diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt
new file mode 100644
index 0000000..21c2caf
--- /dev/null
+++ b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.accessibility
+
+import android.view.View
+import android.view.ViewGroup
+import com.android.launcher3.CellLayout
+import com.android.launcher3.DropTarget.DragObject
+import com.android.launcher3.dragndrop.DragController
+import com.android.launcher3.dragndrop.DragOptions
+import com.android.launcher3.views.ActivityContext
+import java.util.function.Function
+
+/**
+ * Utility listener to enable/disable accessibility drag flags for a ViewGroup containing
+ * CellLayouts
+ */
+open class AccessibleDragListenerAdapter
+/**
+ * @param parent the viewgroup containing all the children
+ * @param delegateFactory function to create no delegates
+ */
+(
+    private val mViewGroup: ViewGroup,
+    private val mDelegateFactory: Function<CellLayout, DragAndDropAccessibilityDelegate>
+) : DragController.DragListener, ViewGroup.OnHierarchyChangeListener {
+    override fun onDragStart(dragObject: DragObject, options: DragOptions) {
+        mViewGroup.setOnHierarchyChangeListener(this)
+        enableAccessibleDrag(true, dragObject)
+    }
+
+    override fun onDragEnd() {
+        mViewGroup.setOnHierarchyChangeListener(null)
+        enableAccessibleDrag(false, null)
+        val activityContext = ActivityContext.lookupContext(mViewGroup.context) as ActivityContext
+        activityContext.getDragController<DragController<*>>()?.removeDragListener(this)
+    }
+
+    override fun onChildViewAdded(parent: View, child: View) {
+        if (parent === mViewGroup) {
+            setEnableForLayout(child as CellLayout, true)
+        }
+    }
+
+    override fun onChildViewRemoved(parent: View, child: View) {
+        if (parent === mViewGroup) {
+            setEnableForLayout(child as CellLayout, false)
+        }
+    }
+
+    protected open fun enableAccessibleDrag(enable: Boolean, dragObject: DragObject?) {
+        for (i in 0 until mViewGroup.childCount) {
+            setEnableForLayout(mViewGroup.getChildAt(i) as CellLayout, enable)
+        }
+    }
+
+    protected fun setEnableForLayout(layout: CellLayout, enable: Boolean) {
+        layout.setDragAndDropAccessibilityDelegate(
+            if (enable) mDelegateFactory.apply(layout) else null
+        )
+    }
+}
diff --git a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
index d0fc175..6f73e07 100644
--- a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
@@ -29,9 +29,9 @@
 import androidx.customview.widget.ExploreByTouchHelper;
 
 import com.android.launcher3.CellLayout;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.BaseDragLayer;
 
 import java.util.List;
 
@@ -47,16 +47,17 @@
 
     protected final CellLayout mView;
     protected final Context mContext;
+    protected final ActivityContext mActivityContext;
     protected final LauncherAccessibilityDelegate mDelegate;
-    protected final DragLayer mDragLayer;
+    protected final BaseDragLayer<?> mDragLayer;
 
     public DragAndDropAccessibilityDelegate(CellLayout forView) {
         super(forView);
         mView = forView;
         mContext = mView.getContext();
-        Launcher launcher = Launcher.getLauncher(mContext);
-        mDelegate = launcher.getAccessibilityDelegate();
-        mDragLayer = launcher.getDragLayer();
+        mActivityContext = ActivityContext.lookupContext(mContext);
+        mDelegate = (LauncherAccessibilityDelegate) mActivityContext.getAccessibilityDelegate();
+        mDragLayer = mActivityContext.getDragLayer();
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 56a7fef..ad660d2 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -71,6 +71,7 @@
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget.DragObject;
+import com.android.launcher3.Flags;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.R;
@@ -764,6 +765,16 @@
         }
     }
 
+    /**
+     * Force header height update with an offset. Used by {@link UniversalSearchInputView} to
+     * request {@link FloatingHeaderView} to update its maxTranslation for multiline search bar.
+     */
+    public void forceUpdateHeaderHeight(int offset) {
+        if (Flags.multilineSearchBar()) {
+            mHeader.updateSearchBarOffset(offset);
+        }
+    }
+
     protected void updateHeaderScroll(int scrolledOffset) {
         float prog1 = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f);
         int headerColor = getHeaderColor(prog1);
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index 89e6adc..a4f130a 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -262,11 +262,12 @@
         writer.println(prefix + "\tAllAppsStore Apps[] size: " + mApps.length);
         for (int i = 0; i < mApps.length; i++) {
             writer.println(String.format(Locale.getDefault(),
-                    "%s\tPackage index, name, and class: " + "%d/%s:%s",
+                    "%s\tPackage index, name, class, and description: %d/%s:%s, %s",
                     prefix,
                     i,
                     mApps[i].componentName.getPackageName(),
-                    mApps[i].componentName.getClassName()));
+                    mApps[i].componentName.getClassName(),
+                    mApps[i].contentDescription));
         }
     }
 }
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java
index 92c589c..a2bd5dd 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderView.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java
@@ -30,6 +30,7 @@
 import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder;
@@ -104,6 +105,8 @@
     private boolean mFloatingRowsCollapsed;
     // Total height of all current floating rows. Collapsed rows == 0 height.
     private int mFloatingRowsHeight;
+    // Offset of search bar. Adds to the floating view height when multi-line is supported.
+    private int mSearchBarOffset = 0;
 
     // This is initialized once during inflation and stays constant after that. Fixed views
     // cannot be added or removed dynamically.
@@ -198,6 +201,14 @@
         }
     }
 
+    /**
+     * Offset floating rows height by search bar
+     */
+    void updateSearchBarOffset(int offset) {
+        mSearchBarOffset = offset;
+        onHeightUpdated();
+    }
+
     @Override
     public void onPluginDisconnected(AllAppsRow plugin) {
         PluginHeaderRow row = mPluginRows.get(plugin);
@@ -258,9 +269,18 @@
         mTabLayout.setVisibility(mTabsHidden ? GONE : visibility);
     }
 
+    /** Returns whether search bar has multi-line support, and is currently in multi-line state. */
+    private boolean isSearchBarMultiline() {
+        return Flags.multilineSearchBar() && mSearchBarOffset > 0;
+    }
+
     private void updateExpectedHeight() {
         updateFloatingRowsHeight();
         mMaxTranslation = 0;
+        boolean shouldAddSearchBarHeight = isSearchBarMultiline() && !Flags.floatingSearchBar();
+        if (shouldAddSearchBarHeight) {
+            mMaxTranslation += mSearchBarOffset;
+        }
         if (mFloatingRowsCollapsed) {
             return;
         }
diff --git a/src/com/android/launcher3/allapps/FloatingMaskView.java b/src/com/android/launcher3/allapps/FloatingMaskView.java
index 606eb03..cee5e18 100644
--- a/src/com/android/launcher3/allapps/FloatingMaskView.java
+++ b/src/com/android/launcher3/allapps/FloatingMaskView.java
@@ -21,6 +21,7 @@
 import android.view.ViewGroup;
 import android.widget.ImageView;
 
+import androidx.annotation.VisibleForTesting;
 import androidx.constraintlayout.widget.ConstraintLayout;
 
 import com.android.launcher3.R;
@@ -53,13 +54,21 @@
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
-        ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams();
-        AllAppsRecyclerView allAppsContainerView =
-                mActivityContext.getAppsView().getActiveRecyclerView();
+        setParameters((ViewGroup.MarginLayoutParams) getLayoutParams(),
+                mActivityContext.getAppsView().getActiveRecyclerView());
+    }
+
+    @VisibleForTesting
+    void setParameters(ViewGroup.MarginLayoutParams lp, AllAppsRecyclerView recyclerView) {
         if (lp != null) {
-            lp.rightMargin = allAppsContainerView.getPaddingRight();
-            lp.leftMargin = allAppsContainerView.getPaddingLeft();
-            mBottomBox.setMinimumHeight(allAppsContainerView.getPaddingBottom());
+            lp.rightMargin = recyclerView.getPaddingRight();
+            lp.leftMargin = recyclerView.getPaddingLeft();
+            getBottomBox().setMinimumHeight(recyclerView.getPaddingBottom());
         }
     }
+
+    @VisibleForTesting
+    ImageView getBottomBox() {
+        return mBottomBox;
+    }
 }
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index 0f4204f..e215cab 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -41,7 +41,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
-import android.animation.LayoutTransition;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.content.Context;
@@ -92,14 +91,14 @@
 public class PrivateProfileManager extends UserProfileManager {
 
     private static final String TAG = "PrivateProfileManager";
-    private static final int EXPAND_COLLAPSE_DURATION = 800;
+    private static final int EXPAND_COLLAPSE_DURATION = 400;
     private static final int SETTINGS_OPACITY_DURATION = 400;
     private static final int TEXT_UNLOCK_OPACITY_DURATION = 300;
     private static final int TEXT_LOCK_OPACITY_DURATION = 50;
     private static final int APP_OPACITY_DURATION = 400;
     private static final int MASK_VIEW_DURATION = 200;
     private static final int APP_OPACITY_DELAY = 400;
-    private static final int SETTINGS_AND_LOCK_GROUP_TRANSITION_DELAY = 400;
+    private static final int PILL_TRANSITION_DELAY = 400;
     private static final int SETTINGS_OPACITY_DELAY = 400;
     private static final int LOCK_TEXT_OPACITY_DELAY = 500;
     private static final int MASK_VIEW_DELAY = 400;
@@ -109,6 +108,8 @@
     private final Predicate<UserHandle> mPrivateProfileMatcher;
     private final int mPsHeaderHeight;
     private final int mFloatingMaskViewCornerRadius;
+    private final int mLockTextMarginStart;
+    private final int mLockTextMarginEnd;
     private final RecyclerView.OnScrollListener mOnIdleScrollListener =
             new RecyclerView.OnScrollListener() {
         @Override
@@ -133,6 +134,11 @@
     private Runnable mOnPSHeaderAdded;
     @Nullable
     private RelativeLayout mPSHeader;
+    @Nullable
+    private TextView mLockText;
+    @Nullable
+    private PrivateSpaceSettingsButton mPrivateSpaceSettingsButton;
+    @Nullable
     private ConstraintLayout mFloatingMaskView;
     private final String mLockedStateContentDesc;
     private final String mUnLockedStateContentDesc;
@@ -155,6 +161,10 @@
                 .getString(R.string.ps_container_unlock_button_content_description);
         mFloatingMaskViewCornerRadius = mAllApps.getContext().getResources().getDimensionPixelSize(
                 R.dimen.ps_floating_mask_corner_radius);
+        mLockTextMarginStart = mAllApps.getContext().getResources().getDimensionPixelSize(
+                R.dimen.ps_lock_icon_text_margin_start_expanded);
+        mLockTextMarginEnd = mAllApps.getContext().getResources().getDimensionPixelSize(
+                R.dimen.ps_lock_icon_text_margin_end_expanded);
     }
 
     /** Adds Private Space Header to the layout. */
@@ -354,20 +364,12 @@
     /** Add Private Space Header view elements based upon {@link UserProfileState} */
     public void bindPrivateSpaceHeaderViewElements(RelativeLayout parent) {
         mPSHeader = parent;
+        Log.d(TAG, "bindPrivateSpaceHeaderViewElements: " + "Binding private space.");
+        updateView();
         if (mOnPSHeaderAdded != null) {
             MAIN_EXECUTOR.execute(mOnPSHeaderAdded);
             mOnPSHeaderAdded = null;
         }
-        // Set the transition duration for the settings and lock button to animate.
-        ViewGroup settingAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup);
-        if (mReadyToAnimate) {
-            enableLayoutTransition(settingAndLockGroup);
-        } else {
-            // Ensure any unwanted animations to not happen.
-            settingAndLockGroup.setLayoutTransition(null);
-            Log.d(TAG, "bindPrivateSpaceHeaderViewElements: removing transitions ");
-        }
-        updateView();
     }
 
     /** Update the states of the views that make up the header at the state it is called in. */
@@ -375,12 +377,15 @@
         if (mPSHeader == null) {
             return;
         }
+        Log.d(TAG, "bindPrivateSpaceHeaderViewElements: " + "Updating view with state: "
+                + getCurrentState());
         mPSHeader.setAlpha(1);
         ViewGroup lockPill = mPSHeader.findViewById(R.id.ps_lock_unlock_button);
         assert lockPill != null;
-        TextView lockText = lockPill.findViewById(R.id.lock_text);
-        PrivateSpaceSettingsButton settingsButton = mPSHeader.findViewById(R.id.ps_settings_button);
-        assert settingsButton != null;
+        mLockText = lockPill.findViewById(R.id.lock_text);
+        assert mLockText != null;
+        mPrivateSpaceSettingsButton = mPSHeader.findViewById(R.id.ps_settings_button);
+        assert mPrivateSpaceSettingsButton != null;
         //Add image for private space transitioning view
         ImageView transitionView = mPSHeader.findViewById(R.id.ps_transition_image);
         assert transitionView != null;
@@ -391,12 +396,19 @@
                 // Remove header from accessibility target when enabled.
                 mPSHeader.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
 
-                lockText.setVisibility(VISIBLE);
+                if (!mReadyToAnimate) {
+                    // Don't set visibilities when animating as the animation will handle it.
+                    mLockText.setVisibility(VISIBLE);
+                    mLockText.setAlpha(1);
+                    mLockText.setHorizontallyScrolling(false);
+                    mPrivateSpaceSettingsButton.setVisibility(
+                            isPrivateSpaceSettingsAvailable() ? VISIBLE : GONE);
+                    mPrivateSpaceSettingsButton.setClickable(isPrivateSpaceSettingsAvailable());
+                }
                 lockPill.setVisibility(VISIBLE);
                 lockPill.setOnClickListener(view -> lockingAction(/* lock */ true));
                 lockPill.setContentDescription(mUnLockedStateContentDesc);
 
-                settingsButton.setVisibility(isPrivateSpaceSettingsAvailable() ? VISIBLE : GONE);
                 transitionView.setVisibility(GONE);
             }
             case STATE_DISABLED -> {
@@ -406,12 +418,15 @@
                 mPSHeader.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
                 mPSHeader.setContentDescription(mLockedStateContentDesc);
 
-                lockText.setVisibility(GONE);
+                mLockText.setVisibility(GONE);
+                mLockText.setAlpha(0);
+                mLockText.setHorizontallyScrolling(false);
                 lockPill.setVisibility(VISIBLE);
                 lockPill.setOnClickListener(view -> lockingAction(/* lock */ false));
                 lockPill.setContentDescription(mLockedStateContentDesc);
 
-                settingsButton.setVisibility(GONE);
+                mPrivateSpaceSettingsButton.setVisibility(GONE);
+                mPrivateSpaceSettingsButton.setClickable(false);
                 transitionView.setVisibility(GONE);
             }
             case STATE_TRANSITION -> {
@@ -585,6 +600,51 @@
         return alphaAnim;
     }
 
+    private ValueAnimator animatePillTransition(boolean isExpanding) {
+        if (mLockText == null) {
+            return new ValueAnimator().setDuration(0);
+        }
+        mLockText.measure(0,0);
+        int currentWidth = mLockText.getWidth();
+        int fullWidth = mLockText.getMeasuredWidth();
+        float from = isExpanding ? 0 : currentWidth;
+        float to = isExpanding ? fullWidth : 0;
+        ValueAnimator pillAnim = ObjectAnimator.ofFloat(from, to);
+        pillAnim.setStartDelay(isExpanding ? PILL_TRANSITION_DELAY : 0);
+        pillAnim.setDuration(EXPAND_COLLAPSE_DURATION);
+        pillAnim.setInterpolator(Interpolators.STANDARD);
+        pillAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                float translation = (float) valueAnimator.getAnimatedValue();
+                float translationFraction = translation / fullWidth;
+                ViewGroup.MarginLayoutParams layoutParams =
+                        (ViewGroup.MarginLayoutParams) mLockText.getLayoutParams();
+                layoutParams.width = (int) translation;
+                layoutParams.setMarginStart((int) (mLockTextMarginStart * translationFraction));
+                layoutParams.setMarginEnd((int) (mLockTextMarginEnd * translationFraction));
+                mLockText.setLayoutParams(layoutParams);
+                mLockText.requestLayout();
+            }
+        });
+        pillAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                if (!isExpanding) {
+                    mLockText.setVisibility(GONE);
+                }
+                mLockText.setHorizontallyScrolling(false);
+            }
+
+            @Override
+            public void onAnimationStart(Animator animator) {
+                mLockText.setHorizontallyScrolling(true);
+                mLockText.setVisibility(VISIBLE);
+            }
+        });
+        return pillAnim;
+    }
+
     /**
      * Using PropertySetter{@link PropertySetter}, we can update the view's attributes within an
      * animation. At the moment, collapsing, setting alpha changes, and animating the text is done
@@ -596,26 +656,13 @@
         }
         if (mPSHeader == null) {
             mOnPSHeaderAdded = () -> updatePrivateStateAnimator(expand);
-            setAnimationRunning(false);
+            // Set animation to true, because onBind will be called after this return where we want
+            // the views to be updated accordingly so animation can happen.
+            setAnimationRunning(true);
             return;
         }
         attachFloatingMaskView(expand);
-        ViewGroup settingsAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup);
-        TextView lockText = mPSHeader.findViewById(R.id.lock_text);
-        PrivateSpaceSettingsButton privateSpaceSettingsButton =
-                mPSHeader.findViewById(R.id.ps_settings_button);
-        if (settingsAndLockGroup.getLayoutTransition() == null) {
-            // Set a new transition if the current ViewGroup does not already contain one as each
-            // transition should only happen once when applied.
-            enableLayoutTransition(settingsAndLockGroup);
-        }
-        settingsAndLockGroup.getLayoutTransition().setStartDelay(
-                LayoutTransition.CHANGING,
-                expand ? SETTINGS_AND_LOCK_GROUP_TRANSITION_DELAY : NO_DELAY);
-        PropertySetter headerSetter = new AnimatedPropertySetter();
-        headerSetter.add(updateSettingsGearAlpha(expand));
-        headerSetter.add(updateLockTextAlpha(expand));
-        AnimatorSet animatorSet = headerSetter.buildAnim();
+        AnimatorSet animatorSet = new AnimatedPropertySetter().buildAnim();
         animatorSet.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
@@ -626,8 +673,6 @@
                                 ? LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN
                                 : LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN,
                         mAllApps.getActiveRecyclerView());
-                // Animate the collapsing of the text at the same time while updating lock button.
-                lockText.setVisibility(expand ? VISIBLE : GONE);
                 setAnimationRunning(true);
             }
 
@@ -646,10 +691,10 @@
                             : LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END,
                     mAllApps.getActiveRecyclerView());
             Log.d(TAG, "updatePrivateStateAnimator: lockText visibility: "
-                    + lockText.getVisibility() + " lockTextAlpha: " + lockText.getAlpha());
+                    + mLockText.getVisibility() + " lockTextAlpha: " + mLockText.getAlpha());
             Log.d(TAG, "updatePrivateStateAnimator: settingsCog visibility: "
-                    + privateSpaceSettingsButton.getVisibility()
-                    + " settingsCogAlpha: " + privateSpaceSettingsButton.getAlpha());
+                    + mPrivateSpaceSettingsButton.getVisibility()
+                    + " settingsCogAlpha: " + mPrivateSpaceSettingsButton.getAlpha());
             if (!expand) {
                 mAllApps.mAH.get(MAIN).mRecyclerView.removeItemDecoration(
                         mPrivateAppsSectionDecorator);
@@ -662,16 +707,24 @@
             }
         }));
         if (expand) {
-            animatorSet.playTogether(animateAlphaOfIcons(true),
+            animatorSet.playTogether(updateSettingsGearAlpha(true),
+                    updateLockTextAlpha(true),
+                    animateAlphaOfIcons(true),
+                    animatePillTransition(true),
                     translateFloatingMaskView(false));
         } else {
+            AnimatorSet parallelSet = new AnimatorSet();
+            parallelSet.playTogether(updateSettingsGearAlpha(false),
+                    updateLockTextAlpha(false),
+                    animateAlphaOfIcons(false),
+                    animatePillTransition(false));
             if (isPrivateSpaceHidden()) {
-                animatorSet.playSequentially(animateAlphaOfIcons(false),
+                animatorSet.playSequentially(parallelSet,
                         animateAlphaOfPrivateSpaceContainer(),
                         animateCollapseAnimation());
             } else {
                 animatorSet.playSequentially(translateFloatingMaskView(true),
-                        animateAlphaOfIcons(false),
+                        parallelSet,
                         animateCollapseAnimation());
             }
         }
@@ -702,7 +755,7 @@
     /** Fades out the private space container. */
     private ValueAnimator translateFloatingMaskView(boolean animateIn) {
         if (!Flags.privateSpaceAddFloatingMaskView() || mFloatingMaskView == null) {
-            return new ValueAnimator();
+            return new ValueAnimator().setDuration(0);
         }
         // Translate base on the height amount. Translates out on expand and in on collapse.
         float floatingMaskViewHeight = getFloatingMaskViewHeight();
@@ -714,42 +767,19 @@
         alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                if (mFloatingMaskView == null) {
+                    return;
+                }
                 mFloatingMaskView.setTranslationY((float) valueAnimator.getAnimatedValue());
             }
         });
         return alphaAnim;
     }
 
-    /** Animates the layout changes when the text of the button becomes visible/gone. */
-    private void enableLayoutTransition(ViewGroup settingsAndLockGroup) {
-        LayoutTransition settingsAndLockTransition = new LayoutTransition();
-        settingsAndLockTransition.enableTransitionType(LayoutTransition.CHANGING);
-        settingsAndLockTransition.setDuration(EXPAND_COLLAPSE_DURATION);
-        settingsAndLockTransition.setInterpolator(LayoutTransition.CHANGING,
-                Interpolators.STANDARD);
-        settingsAndLockTransition.addTransitionListener(new LayoutTransition.TransitionListener() {
-            @Override
-            public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
-                    View view, int i) {
-                Log.d(TAG, "updatePrivateStateAnimator: transition started: " + transition);
-            }
-            @Override
-            public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
-                    View view, int i) {
-                settingsAndLockGroup.setLayoutTransition(null);
-                mReadyToAnimate = false;
-                Log.d(TAG, "updatePrivateStateAnimator: transition finished: " + transition);
-            }
-        });
-        settingsAndLockGroup.setLayoutTransition(settingsAndLockTransition);
-        Log.d(TAG, "updatePrivateStateAnimator: setting transition: "
-                + settingsAndLockTransition);
-    }
-
     /** Change the settings gear alpha when expanded or collapsed. */
     private ValueAnimator updateSettingsGearAlpha(boolean expand) {
-        if (mPSHeader == null) {
-            return new ValueAnimator();
+        if (mPrivateSpaceSettingsButton == null || !isPrivateSpaceSettingsAvailable()) {
+            return new ValueAnimator().setDuration(0);
         }
         float from = expand ? 0 : 1;
         float to = expand ? 1 : 0;
@@ -760,16 +790,29 @@
         settingsAlphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator valueAnimator) {
-                mPSHeader.findViewById(R.id.ps_settings_button)
-                        .setAlpha((float) valueAnimator.getAnimatedValue());
+                mPrivateSpaceSettingsButton.setAlpha((float) valueAnimator.getAnimatedValue());
+            }
+        });
+        settingsAlphaAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animator) {
+                mPrivateSpaceSettingsButton.setVisibility(VISIBLE);
+                mPrivateSpaceSettingsButton.setClickable(false);
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                if (expand) {
+                    mPrivateSpaceSettingsButton.setClickable(true);
+                }
             }
         });
         return settingsAlphaAnim;
     }
 
     private ValueAnimator updateLockTextAlpha(boolean expand) {
-        if (mPSHeader == null) {
-            return new ValueAnimator();
+        if (mLockText == null) {
+            return new ValueAnimator().setDuration(0);
         }
         float from = expand ? 0 : 1;
         float to = expand ? 1 : 0;
@@ -780,8 +823,7 @@
         alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator valueAnimator) {
-                mPSHeader.findViewById(R.id.lock_text).setAlpha(
-                        (float) valueAnimator.getAnimatedValue());
+                mLockText.setAlpha((float) valueAnimator.getAnimatedValue());
             }
         });
         return alphaAnim;
@@ -819,8 +861,22 @@
         if (!Flags.privateSpaceAddFloatingMaskView()) {
             return;
         }
+        // Use getLocationOnScreen() as simply checking for mPSHeader.getBottom() is only relative
+        // to its parent.
+        int[] psHeaderLocation = new int[2];
+        mPSHeader.getLocationOnScreen(psHeaderLocation);
+        int psHeaderBottomY = psHeaderLocation[1] + mPsHeaderHeight;
+        // Calculate the topY of the floatingMaskView as if it was added.
+        int floatingMaskViewBottomBoxTopY =
+                (int) (mAllApps.getBottom() - getMainRecyclerView().getPaddingBottom());
+        // Don't attach if the header will be clipped by the floating mask view.
+        if (psHeaderBottomY > floatingMaskViewBottomBoxTopY) {
+            mFloatingMaskView = null;
+            return;
+        }
         mFloatingMaskView = (FloatingMaskView) mAllApps.getLayoutInflater().inflate(
                 R.layout.private_space_mask_view, mAllApps, false);
+        assert mFloatingMaskView != null;
         mAllApps.addView(mFloatingMaskView);
         // Translate off the screen first if its collapsing so this header view isn't visible to
         // user when animation starts.
diff --git a/src/com/android/launcher3/allapps/SectionDecorationHandler.java b/src/com/android/launcher3/allapps/SectionDecorationHandler.java
index ac9b146..eaeb8bb 100644
--- a/src/com/android/launcher3/allapps/SectionDecorationHandler.java
+++ b/src/com/android/launcher3/allapps/SectionDecorationHandler.java
@@ -25,7 +25,6 @@
 import android.view.View;
 
 import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
 
 import com.android.launcher3.R;
 import com.android.launcher3.util.Themes;
@@ -61,10 +60,10 @@
 
         mContext = context;
         mFillAlpha = fillAlpha;
-        mFocusColor = ContextCompat.getColor(context,
-                R.color.material_color_surface_bright); // UX recommended
-        mFillColor = ContextCompat.getColor(context,
-                R.color.material_color_surface_container_high); // UX recommended
+        mFocusColor = Themes.getAttrColor(context,
+                R.attr.materialColorSurfaceBright); // UX recommended
+        mFillColor = Themes.getAttrColor(context,
+                R.attr.materialColorSurfaceContainerHigh); // UX recommended
 
         mIsTopLeftRound = isTopLeftRound;
         mIsTopRightRound = isTopRightRound;
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index db693f0..8b1f42b 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -157,7 +157,8 @@
                         isOverFolderOrSearchBar = isEventOverView(topView, ev) ||
                                 isEventOverAccessibleDropTargetBar(ev);
                         if (!isOverFolderOrSearchBar) {
-                            sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
+                            sendTapOutsideFolderAccessibilityEvent(
+                                    currentFolder.getIsEditingName());
                             mHoverPointClosesFolder = true;
                             return true;
                         }
@@ -167,7 +168,8 @@
                         isOverFolderOrSearchBar = isEventOverView(topView, ev) ||
                                 isEventOverAccessibleDropTargetBar(ev);
                         if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) {
-                            sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
+                            sendTapOutsideFolderAccessibilityEvent(
+                                    currentFolder.getIsEditingName());
                             mHoverPointClosesFolder = true;
                             return true;
                         } else if (!isOverFolderOrSearchBar) {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index d3c1a02..3edf1f2 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -26,6 +26,7 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
 import static com.android.launcher3.config.FeatureFlags.ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_LABEL_UPDATED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED;
 import static com.android.launcher3.testing.shared.TestProtocol.FOLDER_OPENED_MESSAGE;
@@ -135,7 +136,8 @@
      * We avoid measuring {@link #mContent} with a 0 width or height, as this
      * results in CellLayout being measured as UNSPECIFIED, which it does not support.
      */
-    private static final int MIN_CONTENT_DIMEN = 5;
+    @VisibleForTesting
+    static final int MIN_CONTENT_DIMEN = 5;
 
     public static final int STATE_CLOSED = 0;
     public static final int STATE_ANIMATING = 1;
@@ -143,7 +145,8 @@
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({STATE_CLOSED, STATE_ANIMATING, STATE_OPEN})
-    public @interface FolderState {}
+    public @interface FolderState {
+    }
 
     /**
      * Time for which the scroll hint is shown before automatically changing page.
@@ -164,7 +167,7 @@
     private static final int FOLDER_COLOR_ANIMATION_DURATION = 200;
 
     private static final int REORDER_DELAY = 250;
-    private static final int ON_EXIT_CLOSE_DELAY = 400;
+    static final int ON_EXIT_CLOSE_DELAY = 400;
     private static final Rect sTempRect = new Rect();
     private static final int MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION = 10;
 
@@ -184,10 +187,10 @@
                 || itemType == ITEM_TYPE_APP_PAIR;
     }
 
-    private final Alarm mReorderAlarm = new Alarm(Looper.getMainLooper());
-    private final Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper());
-    private final Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper());
-    final Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper());
+    private Alarm mReorderAlarm = new Alarm(Looper.getMainLooper());
+    private Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper());
+    private Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper());
+    private Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper());
 
     final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
 
@@ -197,7 +200,7 @@
     // Folder can be displayed in Launcher's activity or a separate window (e.g. Taskbar).
     // Anything specific to Launcher should use mLauncherDelegate, otherwise should
     // use mActivityContext.
-    protected final LauncherDelegate mLauncherDelegate;
+    protected LauncherDelegate mLauncherDelegate;
     protected final ActivityContext mActivityContext;
 
     protected DragController mDragController;
@@ -210,7 +213,7 @@
 
     @Thunk
     FolderPagedView mContent;
-    public FolderNameEditText mFolderName;
+    private FolderNameEditText mFolderName;
     private PageIndicatorDots mPageIndicator;
 
     protected View mFooter;
@@ -234,10 +237,10 @@
     private OnFolderStateChangedListener mPriorityOnFolderStateChangedListener;
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mRearrangeOnClose = false;
-    boolean mItemsInvalidated = false;
+    private boolean mItemsInvalidated = false;
     private View mCurrentDragView;
     private boolean mIsExternalDrag;
-    private boolean mDragInProgress = false;
+    private boolean mIsDragInProgress = false;
     private boolean mDeleteFolderOnDropCompleted = false;
     private boolean mSuppressFolderDeletion = false;
     private boolean mItemAddedBackToSelfViaIcon = false;
@@ -250,7 +253,7 @@
     private int mScrollAreaOffset;
 
     @Thunk
-    int mScrollHintDir = SCROLL_NONE;
+    private int mScrollHintDir = SCROLL_NONE;
     @Thunk
     int mCurrentScrollDir = SCROLL_NONE;
 
@@ -315,9 +318,9 @@
                 | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
         mFolderName.forceDisableSuggestions(true);
         mFolderName.setPadding(mFolderName.getPaddingLeft(),
-                (mFooterHeight - mFolderName.getLineHeight()) / 2,
+                (getFooterHeight() - mFolderName.getLineHeight()) / 2,
                 mFolderName.getPaddingRight(),
-                (mFooterHeight - mFolderName.getLineHeight()) / 2);
+                (getFooterHeight() - mFolderName.getLineHeight()) / 2);
 
         mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this);
         setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback);
@@ -325,42 +328,54 @@
 
     public boolean onLongClick(View v) {
         // Return if global dragging is not enabled
-        if (!mLauncherDelegate.isDraggingEnabled()) return true;
+        if (!getIsLauncherDraggingEnabled()) return true;
         return startDrag(v, new DragOptions());
     }
 
+    @VisibleForTesting
+    boolean getIsLauncherDraggingEnabled() {
+        return mLauncherDelegate.isDraggingEnabled();
+    }
+
     public boolean startDrag(View v, DragOptions options) {
         Object tag = v.getTag();
         if (tag instanceof ItemInfo item) {
             mEmptyCellRank = item.rank;
             mCurrentDragView = v;
 
-            mDragController.addDragListener(this);
-            if (options.isAccessibleDrag) {
-                mDragController.addDragListener(new AccessibleDragListenerAdapter(
-                        mContent, FolderAccessibilityHelper::new) {
-                    @Override
-                    protected void enableAccessibleDrag(boolean enable,
-                            @Nullable DragObject dragObject) {
-                        super.enableAccessibleDrag(enable, dragObject);
-                        mFooter.setImportantForAccessibility(enable
-                                ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                                : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-                    }
-                });
-            }
-
-            mLauncherDelegate.beginDragShared(v, this, options);
+            addDragListener(options);
+            callBeginDragShared(v, options);
         }
         return true;
     }
 
+    void callBeginDragShared(View v, DragOptions options) {
+        mLauncherDelegate.beginDragShared(v, this, options);
+    }
+
+    void addDragListener(DragOptions options) {
+        getDragController().addDragListener(this);
+        if (!options.isAccessibleDrag) {
+            return;
+        }
+        getDragController().addDragListener(new AccessibleDragListenerAdapter(
+                mContent, FolderAccessibilityHelper::new) {
+            @Override
+            protected void enableAccessibleDrag(boolean enable,
+                    @Nullable DragObject dragObject) {
+                super.enableAccessibleDrag(enable, dragObject);
+                mFooter.setImportantForAccessibility(enable
+                        ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                        : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+            }
+        });
+    }
+
     @Override
     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
         if (dragObject.dragSource != this) {
             return;
         }
-
         mContent.removeItem(mCurrentDragView);
         mItemsInvalidated = true;
 
@@ -369,29 +384,23 @@
         try (SuppressInfoChanges s = new SuppressInfoChanges()) {
             mInfo.remove(dragObject.dragInfo, true);
         }
-        mDragInProgress = true;
+        mIsDragInProgress = true;
         mItemAddedBackToSelfViaIcon = false;
     }
 
     @Override
     public void onDragEnd() {
-        if (mIsExternalDrag && mDragInProgress) {
+        if (mIsExternalDrag && mIsDragInProgress) {
             completeDragExit();
         }
-        mDragInProgress = false;
-        mDragController.removeDragListener(this);
-    }
-
-    public boolean isEditingName() {
-        return mIsEditingName;
+        mIsDragInProgress = false;
+        getDragController().removeDragListener(this);
     }
 
     public void startEditingFolderName() {
-        post(() -> {
-            showLabelSuggestions();
-            mFolderName.setHint("");
-            mIsEditingName = true;
-        });
+        showLabelSuggestions();
+        mFolderName.setHint("");
+        mIsEditingName = true;
     }
 
     @Override
@@ -459,7 +468,11 @@
         return mFolderIcon;
     }
 
-    public void setDragController(DragController dragController) {
+    DragController getDragController() {
+        return mDragController;
+    }
+
+    void setDragController(DragController dragController) {
         mDragController = dragController;
     }
 
@@ -540,7 +553,7 @@
      * Show suggested folder title in FolderEditText if the first suggestion is non-empty, push
      * rest of the suggestions to InputMethodManager.
      */
-    private void showLabelSuggestions() {
+    void showLabelSuggestions() {
         if (mInfo.suggestedFolderNames == null) {
             return;
         }
@@ -634,11 +647,11 @@
      */
     public void beginExternalDrag() {
         mIsExternalDrag = true;
-        mDragInProgress = true;
+        mIsDragInProgress = true;
 
         // Since this folder opened by another controller, it might not get onDrop or
         // onDropComplete. Perform cleanup once drag-n-drop ends.
-        mDragController.addDragListener(this);
+        getDragController().addDragListener(this);
 
         ArrayList<ItemInfo> items = new ArrayList<>(mInfo.getContents());
         mEmptyCellRank = items.size();
@@ -662,16 +675,12 @@
      * is played.
      */
     private void animateOpen(List<ItemInfo> items, int pageNo) {
-        if (items == null || items.size() <= 1) {
-            Log.d(TAG, "Couldn't animate folder open because items is: " + items);
+        if (!shouldAnimateOpen(items)) {
             return;
         }
 
         Folder openFolder = getOpen(mActivityContext);
-        if (openFolder != null && openFolder != this) {
-            // Close any open folder before opening a folder.
-            openFolder.close(true);
-        }
+        closeOpenFolder(openFolder);
 
         mContent.bindItems(items);
         centerAboutIcon();
@@ -685,7 +694,7 @@
         // There was a one-off crash where the folder had a parent already.
         if (getParent() == null) {
             dragLayer.addView(this);
-            mDragController.addDropTarget(this);
+            getDragController().addDropTarget(this);
         } else {
             if (FeatureFlags.IS_STUDIO_BUILD) {
                 Log.e(TAG, "Opening folder (" + this + ") which already has a parent:"
@@ -734,7 +743,7 @@
 
             // Do not update the flag if we are in drag mode. The flag will be updated, when we
             // actually drop the icon.
-            final boolean updateAnimationFlag = !mDragInProgress;
+            final boolean updateAnimationFlag = !mIsDragInProgress;
             anim.addListener(new AnimatorListenerAdapter() {
 
                 @SuppressLint("InlinedApi")
@@ -768,12 +777,36 @@
         anim.start();
 
         // Make sure the folder picks up the last drag move even if the finger doesn't move.
-        if (mDragController.isDragging()) {
-            mDragController.forceTouchMove();
+        if (getDragController().isDragging()) {
+            getDragController().forceTouchMove();
         }
         mContent.verifyVisibleHighResIcons(mContent.getNextPage());
     }
 
+    /**
+     * Determines whether we should animate the folder opening.
+     */
+    boolean shouldAnimateOpen(List<ItemInfo> items) {
+        if (items == null || items.size() <= 1) {
+            Log.d(TAG, "Couldn't animate folder open because items is: " + items);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * If there's a folder already open, we want to close it before opening another one.
+     */
+    @VisibleForTesting
+    boolean closeOpenFolder(Folder openFolder) {
+        if (openFolder != null && openFolder != this) {
+            // Close any open folder before opening a folder.
+            openFolder.close(true);
+            return true;
+        }
+        return false;
+    }
+
     @Override
     protected boolean isOfType(int type) {
         return (type & TYPE_FOLDER) != 0;
@@ -787,7 +820,7 @@
             mCurrentAnimator.cancel();
         }
 
-        if (isEditingName()) {
+        if (mIsEditingName) {
             mFolderName.dispatchBackKey();
         }
 
@@ -871,7 +904,7 @@
         if (parent != null) {
             parent.removeView(this);
         }
-        mDragController.removeDropTarget(this);
+        getDragController().removeDropTarget(this);
         clearFocus();
         if (mFolderIcon != null) {
             mFolderIcon.setVisibility(View.VISIBLE);
@@ -892,12 +925,12 @@
             mRearrangeOnClose = false;
         }
         if (getItemCount() <= 1) {
-            if (!mDragInProgress && !mSuppressFolderDeletion) {
+            if (!mIsDragInProgress && !mSuppressFolderDeletion) {
                 replaceFolderWithFinalItem();
-            } else if (mDragInProgress) {
+            } else if (mIsDragInProgress) {
                 mDeleteFolderOnDropCompleted = true;
             }
-        } else if (!mDragInProgress) {
+        } else if (!mIsDragInProgress) {
             mContent.unbindItems();
         }
         mSuppressFolderDeletion = false;
@@ -1017,7 +1050,8 @@
         }
     }
 
-    private void clearDragInfo() {
+    @VisibleForTesting
+    void clearDragInfo() {
         mCurrentDragView = null;
         mIsExternalDrag = false;
     }
@@ -1058,7 +1092,8 @@
             if (getItemCount() <= 1) {
                 mDeleteFolderOnDropCompleted = true;
             }
-            if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target != this) {
+            if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon
+                    && target != this) {
                 replaceFolderWithFinalItem();
             }
         } else {
@@ -1089,7 +1124,7 @@
         }
 
         mDeleteFolderOnDropCompleted = false;
-        mDragInProgress = false;
+        mIsDragInProgress = false;
         mItemAddedBackToSelfViaIcon = false;
         mCurrentDragView = null;
 
@@ -1106,7 +1141,7 @@
     }
 
     private void updateItemLocationsInDatabaseBatch(boolean isBind) {
-        FolderGridOrganizer verifier = new FolderGridOrganizer(
+        FolderGridOrganizer verifier = createFolderGridOrganizer(
                 mActivityContext.getDeviceProfile()).setFolderInfo(mInfo);
 
         ArrayList<ItemInfo> items = new ArrayList<>();
@@ -1132,7 +1167,7 @@
     }
 
     public void notifyDrop() {
-        if (mDragInProgress) {
+        if (mIsDragInProgress) {
             mItemAddedBackToSelfViaIcon = true;
         }
     }
@@ -1175,28 +1210,41 @@
     }
 
     protected int getContentAreaHeight() {
-        DeviceProfile grid = mActivityContext.getDeviceProfile();
-        int maxContentAreaHeight = grid.availableHeightPx - grid.getTotalWorkspacePadding().y
-                - mFooterHeight;
-        int height = Math.min(maxContentAreaHeight,
+        int height = Math.min(getMaxContentAreaHeight(),
                 mContent.getDesiredHeight());
         return Math.max(height, MIN_CONTENT_DIMEN);
     }
 
-    private int getContentAreaWidth() {
+    @VisibleForTesting
+    int getMaxContentAreaHeight() {
+        DeviceProfile grid = mActivityContext.getDeviceProfile();
+        return grid.availableHeightPx - grid.getTotalWorkspacePadding().y
+                - getFooterHeight();
+    }
+
+    @VisibleForTesting
+    int getContentAreaWidth() {
         return Math.max(mContent.getDesiredWidth(), MIN_CONTENT_DIMEN);
     }
 
-    private int getFolderWidth() {
+    @VisibleForTesting
+    int getFolderWidth() {
         return getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
     }
 
-    private int getFolderHeight() {
+    @VisibleForTesting
+    int getFolderHeight() {
         return getFolderHeight(getContentAreaHeight());
     }
 
-    private int getFolderHeight(int contentAreaHeight) {
-        return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight;
+    @VisibleForTesting
+    int getFolderHeight(int contentAreaHeight) {
+        return getPaddingTop() + getPaddingBottom() + contentAreaHeight + getFooterHeight();
+    }
+
+    @VisibleForTesting
+    int getFooterHeight() {
+        return mFooterHeight;
     }
 
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
@@ -1366,7 +1414,7 @@
         }
 
         // Clear the drag info, as it is no longer being dragged.
-        mDragInProgress = false;
+        mIsDragInProgress = false;
 
         if (mContent.getPageCount() > 1) {
             // The animation has already been shown while opening the folder.
@@ -1404,7 +1452,7 @@
 
     @Override
     public void onAdd(ItemInfo item, int rank) {
-        FolderGridOrganizer verifier = new FolderGridOrganizer(
+        FolderGridOrganizer verifier = createFolderGridOrganizer(
                 mActivityContext.getDeviceProfile()).setFolderInfo(mInfo);
         verifier.updateRankAndPos(item, rank);
         mLauncherDelegate.getModelWriter().addOrMoveItemInDatabase(item, mInfo.id, 0, item.cellX,
@@ -1435,7 +1483,8 @@
         }
     }
 
-    private View getViewForInfo(final ItemInfo item) {
+    @VisibleForTesting
+    View getViewForInfo(final ItemInfo item) {
         return mContent.iterateOverItems((info, view) -> info == item);
     }
 
@@ -1493,7 +1542,7 @@
             if (hasFocus) {
                 mFromLabelState = mInfo.getFromLabelState();
                 mFromTitle = mInfo.title;
-                startEditingFolderName();
+                post(this::startEditingFolderName);
             } else {
                 StatsLogger statsLogger = mStatsLogManager.logger()
                         .withItemInfo(mInfo)
@@ -1626,7 +1675,7 @@
     /** Navigation bar back key or hardware input back key has been issued. */
     @Override
     public void onBackInvoked() {
-        if (isEditingName()) {
+        if (mIsEditingName) {
             mFolderName.dispatchBackKey();
         } else {
             super.onBackInvoked();
@@ -1638,7 +1687,7 @@
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             BaseDragLayer dl = (BaseDragLayer) getParent();
 
-            if (isEditingName()) {
+            if (mIsEditingName) {
                 if (!dl.isEventOverView(mFolderName, ev)) {
                     mFolderName.dispatchBackKey();
                     return true;
@@ -1685,6 +1734,95 @@
         return mContent;
     }
 
+    @VisibleForTesting
+    void setItemAddedBackToSelfViaIcon(boolean value) {
+        mItemAddedBackToSelfViaIcon = value;
+    }
+
+    @VisibleForTesting
+    boolean getItemAddedBackToSelfViaIcon() {
+        return mItemAddedBackToSelfViaIcon;
+    }
+
+    @VisibleForTesting
+    void setIsDragInProgress(boolean value) {
+        mIsDragInProgress = value;
+    }
+
+    @VisibleForTesting
+    boolean getIsDragInProgress() {
+        return mIsDragInProgress;
+    }
+
+    @VisibleForTesting
+    View getCurrentDragView() {
+        return mCurrentDragView;
+    }
+
+    @VisibleForTesting
+    void setCurrentDragView(View view) {
+        mCurrentDragView = view;
+    }
+
+    @VisibleForTesting
+    boolean getItemsInvalidated() {
+        return mItemsInvalidated;
+    }
+
+    @VisibleForTesting
+    void setItemsInvalidated(boolean value) {
+        mItemsInvalidated = value;
+    }
+
+    @VisibleForTesting
+    boolean getIsExternalDrag() {
+        return mIsExternalDrag;
+    }
+
+    @VisibleForTesting
+    void setIsExternalDrag(boolean value) {
+        mIsExternalDrag = value;
+    }
+
+    public boolean getIsEditingName() {
+        return mIsEditingName;
+    }
+
+    @VisibleForTesting
+    void setIsEditingName(boolean value) {
+        mIsEditingName = value;
+    }
+
+    @VisibleForTesting
+    void setFolderName(FolderNameEditText value) {
+        mFolderName = value;
+    }
+
+    @VisibleForTesting
+    FolderNameEditText getFolderName() {
+        return mFolderName;
+    }
+
+    @VisibleForTesting
+    boolean getIsOpen() {
+        return mIsOpen;
+    }
+
+    @VisibleForTesting
+    void setIsOpen(boolean value) {
+        mIsOpen = value;
+    }
+
+    @VisibleForTesting
+    boolean getRearrangeOnClose() {
+        return mRearrangeOnClose;
+    }
+
+    @VisibleForTesting
+    void setRearrangeOnClose(boolean value) {
+        mRearrangeOnClose = value;
+    }
+
     /** Returns the height of the current folder's bottom edge from the bottom of the screen. */
     private int getHeightFromBottom() {
         BaseDragLayer.LayoutParams layoutParams = (BaseDragLayer.LayoutParams) getLayoutParams();
@@ -1695,10 +1833,15 @@
     }
 
     @VisibleForTesting
-    public boolean getDeleteFolderOnDropCompleted() {
+    boolean getDeleteFolderOnDropCompleted() {
         return mDeleteFolderOnDropCompleted;
     }
 
+    @VisibleForTesting
+    void setDeleteFolderOnDropCompleted(boolean value) {
+        mDeleteFolderOnDropCompleted = value;
+    }
+
     /**
      * Save this listener for the special case of when we update the state and concurrently
      * add another listener to {@link #mOnFolderStateChangedListeners} to avoid a
@@ -1708,7 +1851,13 @@
         mPriorityOnFolderStateChangedListener = listener;
     }
 
-    private void setState(@FolderState int newState) {
+    @VisibleForTesting
+    int getState() {
+        return mState;
+    }
+
+    @VisibleForTesting
+    void setState(@FolderState int newState) {
         mState = newState;
         if (mPriorityOnFolderStateChangedListener != null) {
             mPriorityOnFolderStateChangedListener.onFolderStateChanged(mState);
@@ -1720,6 +1869,60 @@
         }
     }
 
+    @VisibleForTesting
+    Alarm getOnExitAlarm() {
+        return mOnExitAlarm;
+    }
+
+    @VisibleForTesting
+    void setOnExitAlarm(Alarm value) {
+        mOnExitAlarm = value;
+    }
+
+    @VisibleForTesting
+    Alarm getReorderAlarm() {
+        return mReorderAlarm;
+    }
+
+    @VisibleForTesting
+    void setReorderAlarm(Alarm value) {
+        mReorderAlarm = value;
+    }
+
+    @VisibleForTesting
+    Alarm getOnScrollHintAlarm() {
+        return mOnScrollHintAlarm;
+    }
+
+    @VisibleForTesting
+    void setOnScrollHintAlarm(Alarm value) {
+        mOnScrollHintAlarm = value;
+    }
+
+    @VisibleForTesting
+    Alarm getScrollPauseAlarm() {
+        return mScrollPauseAlarm;
+    }
+
+    @VisibleForTesting
+    void setScrollPauseAlarm(Alarm value) {
+        mScrollPauseAlarm = value;
+    }
+
+    @VisibleForTesting
+    int getScrollHintDir() {
+        return mScrollHintDir;
+    }
+
+    @VisibleForTesting
+    void setScrollHintDir(int value) {
+        mScrollHintDir = value;
+    }
+
+    @VisibleForTesting
+    int getScrollAreaOffset() {
+        return mScrollAreaOffset;
+    }
     /**
      * Adds the provided listener to the running list of Folder listeners
      * {@link #mOnFolderStateChangedListeners}
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 37a8d9b..588a6db 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.BubbleTextView.TEXT_ALPHA_PROPERTY;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -99,7 +100,7 @@
 
         mContext = folder.getContext();
         mDeviceProfile = folder.mActivityContext.getDeviceProfile();
-        mPreviewVerifier = new FolderGridOrganizer(mDeviceProfile);
+        mPreviewVerifier = createFolderGridOrganizer(mDeviceProfile);
 
         mIsOpening = isOpening;
 
@@ -255,8 +256,8 @@
                 mFolder.getContent(), contentStart, contentEnd, finalRadius, !mIsOpening));
 
         // Fade in the folder name, as the text can overlap the icons when grid size is small.
-        mFolder.mFolderName.setAlpha(mIsOpening ? 0f : 1f);
-        play(a, getAnimator(mFolder.mFolderName, View.ALPHA, 0, 1),
+        mFolder.getFolderName().setAlpha(mIsOpening ? 0f : 1f);
+        play(a, getAnimator(mFolder.getFolderName(), View.ALPHA, 0, 1),
                 mIsOpening ? FOLDER_NAME_ALPHA_DURATION : 0,
                 mIsOpening ? mDuration - FOLDER_NAME_ALPHA_DURATION : FOLDER_NAME_ALPHA_DURATION);
 
@@ -317,7 +318,7 @@
                 mFolder.mFooter.setScaleX(1f);
                 mFolder.mFooter.setScaleY(1f);
                 mFolder.mFooter.setTranslationX(0f);
-                mFolder.mFolderName.setAlpha(1f);
+                mFolder.getFolderName().setAlpha(1f);
 
                 mFolder.setClipChildren(mFolderClipChildren);
                 mFolder.setClipToPadding(mFolderClipToPadding);
diff --git a/src/com/android/launcher3/folder/FolderGridOrganizer.java b/src/com/android/launcher3/folder/FolderGridOrganizer.java
index 593673d..a7ab7b9 100644
--- a/src/com/android/launcher3/folder/FolderGridOrganizer.java
+++ b/src/com/android/launcher3/folder/FolderGridOrganizer.java
@@ -47,13 +47,20 @@
     /**
      * Note: must call {@link #setFolderInfo(FolderInfo)} manually for verifier to work.
      */
-    public FolderGridOrganizer(DeviceProfile profile) {
-        mMaxCountX = profile.numFolderColumns;
-        mMaxCountY = profile.numFolderRows;
+    public FolderGridOrganizer(int maxCountX, int maxCountY) {
+        mMaxCountX = maxCountX;
+        mMaxCountY = maxCountY;
         mMaxItemsPerPage = mMaxCountX * mMaxCountY;
     }
 
     /**
+     * Creates a FolderGridOrganizer for the given DeviceProfile
+     */
+    public static FolderGridOrganizer createFolderGridOrganizer(DeviceProfile profile) {
+        return new FolderGridOrganizer(profile.numFolderColumns, profile.numFolderRows);
+    }
+
+    /**
      * Updates the organizer with the provided folder info
      */
     public FolderGridOrganizer setFolderInfo(FolderInfo info) {
@@ -194,6 +201,7 @@
             int row = rank / mCountX;
             return col < PREVIEW_MAX_COLUMNS && row < PREVIEW_MAX_ROWS;
         }
+        // If we have less than 4 items do this
         return rank < MAX_NUM_ITEMS_IN_PREVIEW;
     }
 }
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 00636a3..a0b695a 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.Flags.enableCursorHoverStates;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
 import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY;
@@ -223,7 +224,7 @@
 
         icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
 
-        icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile());
+        icon.mPreviewVerifier = createFolderGridOrganizer(activity.getDeviceProfile());
         icon.mPreviewVerifier.setFolderInfo(folderInfo);
         icon.updatePreviewItems(false);
 
@@ -458,7 +459,7 @@
 
         mInfo.setTitle(newTitle, mFolder.mLauncherDelegate.getModelWriter());
         onTitleChanged(mInfo.title);
-        mFolder.mFolderName.setText(mInfo.title);
+        mFolder.getFolderName().setText(mInfo.title);
 
         // Logging for folder creation flow
         StatsLogManager.newInstance(getContext()).logger()
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 8eaa0dc..9dc2d24 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
 import static com.android.launcher3.AbstractFloatingView.TYPE_FOLDER;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
@@ -100,10 +101,21 @@
     private boolean mViewsBound = false;
 
     public FolderPagedView(Context context, AttributeSet attrs) {
+        this(
+                context,
+                attrs,
+                createFolderGridOrganizer(ActivityContext.lookupContext(context).getDeviceProfile())
+        );
+    }
+
+    public FolderPagedView(
+            Context context,
+            AttributeSet attrs,
+            FolderGridOrganizer folderGridOrganizer
+    ) {
         super(context, attrs);
         ActivityContext activityContext = ActivityContext.lookupContext(context);
-        DeviceProfile profile = activityContext.getDeviceProfile();
-        mOrganizer = new FolderGridOrganizer(profile);
+        mOrganizer = folderGridOrganizer;
 
         mIsRtl = Utilities.isRtl(getResources());
         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
@@ -361,8 +373,8 @@
         // Update footer
         mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE);
         // Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text.
-        mFolder.mFolderName.setGravity(getPageCount() > 1 ?
-                (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
+        mFolder.getFolderName().setGravity(getPageCount() > 1
+                ? (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
     }
 
     public int getDesiredWidth() {
diff --git a/src/com/android/launcher3/graphics/IconPalette.java b/src/com/android/launcher3/graphics/IconPalette.java
index 778b32a..00f1c67 100644
--- a/src/com/android/launcher3/graphics/IconPalette.java
+++ b/src/com/android/launcher3/graphics/IconPalette.java
@@ -16,22 +16,15 @@
 
 package com.android.launcher3.graphics;
 
-import android.app.Notification;
 import android.content.Context;
 import android.graphics.Color;
-import android.util.Log;
 
-import androidx.core.graphics.ColorUtils;
-
-import com.android.launcher3.R;
 import com.android.launcher3.util.Themes;
 
 /**
  * Contains colors based on the dominant color of an icon.
  */
 public class IconPalette {
-
-    private static final boolean DEBUG = false;
     private static final String TAG = "IconPalette";
 
     private static final float MIN_PRELOAD_COLOR_SATURATION = 0.2f;
@@ -54,95 +47,4 @@
         }
         return result;
     }
-
-    /**
-     * Resolves a color such that it has enough contrast to be used as the
-     * color of an icon or text on the given background color.
-     *
-     * @return a color of the same hue with enough contrast against the background.
-     *
-     * This was copied from com.android.internal.util.NotificationColorUtil.
-     */
-    public static int resolveContrastColor(Context context, int color, int background) {
-        final int resolvedColor = resolveColor(context, color);
-
-        int contrastingColor = ensureTextContrast(resolvedColor, background);
-
-        if (contrastingColor != resolvedColor) {
-            if (DEBUG){
-                Log.w(TAG, String.format(
-                        "Enhanced contrast of notification for %s " +
-                                "%s (over background) by changing #%s to %s",
-                        context.getPackageName(),
-                        contrastChange(resolvedColor, contrastingColor, background),
-                        Integer.toHexString(resolvedColor), Integer.toHexString(contrastingColor)));
-            }
-        }
-        return contrastingColor;
-    }
-
-    /**
-     * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT}
-     *
-     * This was copied from com.android.internal.util.NotificationColorUtil.
-     */
-    private static int resolveColor(Context context, int color) {
-        if (color == Notification.COLOR_DEFAULT) {
-            return context.getColor(R.color.notification_icon_default_color);
-        }
-        return color;
-    }
-
-    /** For debugging. This was copied from com.android.internal.util.NotificationColorUtil. */
-    private static String contrastChange(int colorOld, int colorNew, int bg) {
-        return String.format("from %.2f:1 to %.2f:1",
-                ColorUtils.calculateContrast(colorOld, bg),
-                ColorUtils.calculateContrast(colorNew, bg));
-    }
-
-    /**
-     * Finds a text color with sufficient contrast over bg that has the same hue as the original
-     * color.
-     *
-     * This was copied from com.android.internal.util.NotificationColorUtil.
-     */
-    private static int ensureTextContrast(int color, int bg) {
-        return findContrastColor(color, bg, 4.5);
-    }
-    /**
-     * Finds a suitable color such that there's enough contrast.
-     *
-     * @param fg the color to start searching from.
-     * @param bg the color to ensure contrast against.
-     * @param minRatio the minimum contrast ratio required.
-     * @return a color with the same hue as {@param color}, potentially darkened to meet the
-     *          contrast ratio.
-     *
-     * This was copied from com.android.internal.util.NotificationColorUtil.
-     */
-    private static int findContrastColor(int fg, int bg, double minRatio) {
-        if (ColorUtils.calculateContrast(fg, bg) >= minRatio) {
-            return fg;
-        }
-
-        double[] lab = new double[3];
-        ColorUtils.colorToLAB(bg, lab);
-        double bgL = lab[0];
-        ColorUtils.colorToLAB(fg, lab);
-        double fgL = lab[0];
-        boolean isBgDark = bgL < 50;
-
-        double low = isBgDark ? fgL : 0, high = isBgDark ? 100 : fgL;
-        final double a = lab[1], b = lab[2];
-        for (int i = 0; i < 15 && high - low > 0.00001; i++) {
-            final double l = (low + high) / 2;
-            fg = ColorUtils.LABToColor(l, a, b);
-            if (ColorUtils.calculateContrast(fg, bg) > minRatio) {
-                if (isBgDark) high = l; else low = l;
-            } else {
-                if (isBgDark) low = l; else high = l;
-            }
-        }
-        return ColorUtils.LABToColor(low, a, b);
-    }
 }
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 6088941..2408955 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -78,8 +78,6 @@
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
-import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.model.data.AppPairInfo;
 import com.android.launcher3.model.data.CollectionInfo;
 import com.android.launcher3.model.data.FolderInfo;
@@ -106,6 +104,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * Utility class for generating the preview of Launcher for a given InvariantDeviceProfile.
@@ -376,15 +375,6 @@
                 getApplicationContext(), providerInfo));
     }
 
-    private void inflateAndAddWidgets(LauncherAppWidgetInfo info, WidgetsModel widgetsModel) {
-        WidgetItem widgetItem = widgetsModel.getWidgetProviderInfoByProviderName(
-                info.providerName, info.user, mContext);
-        if (widgetItem == null) {
-            return;
-        }
-        inflateAndAddWidgets(info, widgetItem.widgetInfo);
-    }
-
     private void inflateAndAddWidgets(
             LauncherAppWidgetInfo info, LauncherAppWidgetProviderInfo providerInfo) {
         AppWidgetHostView view = mAppWidgetHost.createView(
@@ -468,17 +458,22 @@
                     break;
             }
         }
+        Map<ComponentKey, AppWidgetProviderInfo> widgetsMap = widgetProviderInfoMap;
         for (ItemInfo itemInfo : currentAppWidgets) {
             switch (itemInfo.itemType) {
                 case Favorites.ITEM_TYPE_APPWIDGET:
                 case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
-                    if (widgetProviderInfoMap != null) {
-                        inflateAndAddWidgets(
-                                (LauncherAppWidgetInfo) itemInfo, widgetProviderInfoMap);
-                    } else {
-                        inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo,
-                                dataModel.widgetsModel);
+                    if (widgetsMap == null) {
+                        widgetsMap = dataModel.widgetsModel.getWidgetsByComponentKey()
+                                .entrySet()
+                                .stream()
+                                .filter(entry -> entry.getValue().widgetInfo != null)
+                                .collect(Collectors.toMap(
+                                        Map.Entry::getKey,
+                                        entry -> entry.getValue().widgetInfo
+                                ));
                     }
+                    inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo, widgetsMap);
                     break;
                 default:
                     break;
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index e6ade61..5faa2b8 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -52,6 +52,7 @@
 import com.android.launcher3.util.LooperIdleLock;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
 import java.util.ArrayList;
@@ -195,8 +196,8 @@
         if (!WIDGETS_ENABLED) {
             return;
         }
-        final List<WidgetsListBaseEntry> widgets =
-                mBgDataModel.widgetsModel.getWidgetsListForPicker(mApp.getContext());
+        List<WidgetsListBaseEntry> widgets = new WidgetsListBaseEntriesBuilder(mApp.getContext())
+                .build(mBgDataModel.widgetsModel.getWidgetsByPackageItem());
         executeCallbacksTask(c -> c.bindAllWidgets(widgets), mUiExecutor);
     }
 
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 312c6f4..269cb9f 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -568,7 +568,7 @@
     private void processFolderItems() {
         // Sort the folder items, update ranks, and make sure all preview items are high res.
         List<FolderGridOrganizer> verifiers = mApp.getInvariantDeviceProfile().supportedProfiles
-                .stream().map(FolderGridOrganizer::new).toList();
+                .stream().map(FolderGridOrganizer::createFolderGridOrganizer).toList();
         for (CollectionInfo collection : mBgDataModel.collections) {
             if (!(collection instanceof FolderInfo folder)) {
                 continue;
diff --git a/src/com/android/launcher3/model/ModelTaskController.kt b/src/com/android/launcher3/model/ModelTaskController.kt
index 266ed0c..cf2cadc 100644
--- a/src/com/android/launcher3/model/ModelTaskController.kt
+++ b/src/com/android/launcher3/model/ModelTaskController.kt
@@ -24,6 +24,7 @@
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder
 import java.util.Objects
 import java.util.concurrent.Executor
 import java.util.function.Predicate
@@ -78,7 +79,9 @@
     }
 
     fun bindUpdatedWidgets(dataModel: BgDataModel) {
-        val widgets = dataModel.widgetsModel.getWidgetsListForPicker(app.context)
+        val widgets =
+            WidgetsListBaseEntriesBuilder(app.context)
+                .build(dataModel.widgetsModel.widgetsByPackageItem)
         scheduleCallbackTask { it.bindAllWidgets(widgets) }
     }
 
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 079987b..2febb22 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -109,7 +109,7 @@
         final IconCache iconCache = app.getIconCache();
 
         final String[] packages = mPackages;
-        final int N = packages.length;
+        final int packageCount = packages.length;
         final FlagOp flagOp;
         final HashSet<String> packageSet = new HashSet<>(Arrays.asList(packages));
         final Predicate<ItemInfo> matcher = mOp == OP_USER_AVAILABILITY_CHANGE
@@ -123,7 +123,7 @@
         }
         switch (mOp) {
             case OP_ADD: {
-                for (int i = 0; i < N; i++) {
+                for (int i = 0; i < packageCount; i++) {
                     iconCache.updateIconsForPkg(packages[i], mUser);
                     if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
                         if (DEBUG) {
@@ -146,7 +146,7 @@
                             + " Look for earlier AllAppsList logs to find more information.");
                     removedComponents.add(a.componentName);
                 })) {
-                    for (int i = 0; i < N; i++) {
+                    for (int i = 0; i < packageCount; i++) {
                         iconCache.updateIconsForPkg(packages[i], mUser);
                         activitiesLists.put(packages[i],
                                 appsList.updatePackage(context, packages[i], mUser));
@@ -156,13 +156,13 @@
                 flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE);
                 break;
             case OP_REMOVE: {
-                for (int i = 0; i < N; i++) {
+                for (int i = 0; i < packageCount; i++) {
                     iconCache.removeIconsForPkg(packages[i], mUser);
                 }
                 // Fall through
             }
             case OP_UNAVAILABLE:
-                for (int i = 0; i < N; i++) {
+                for (int i = 0; i < packageCount; i++) {
                     if (DEBUG) {
                         Log.d(TAG, getOpString() + ": removing package=" + packages[i]);
                     }
@@ -217,44 +217,44 @@
             // For system apps, package manager send OP_UPDATE when an app is enabled.
             final boolean isNewApkAvailable = mOp == OP_ADD || mOp == OP_UPDATE;
             synchronized (dataModel) {
-                dataModel.forAllWorkspaceItemInfos(mUser, si -> {
+                dataModel.forAllWorkspaceItemInfos(mUser, itemInfo -> {
 
                     boolean infoUpdated = false;
                     boolean shortcutUpdated = false;
 
-                    ComponentName cn = si.getTargetComponent();
-                    if (cn != null && matcher.test(si)) {
+                    ComponentName cn = itemInfo.getTargetComponent();
+                    if (cn != null && matcher.test(itemInfo)) {
                         String packageName = cn.getPackageName();
 
-                        if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) {
-                            forceKeepShortcuts.add(si.id);
+                        if (itemInfo.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) {
+                            forceKeepShortcuts.add(itemInfo.id);
                             if (mOp == OP_REMOVE) {
                                 return;
                             }
                         }
 
-                        if (si.isPromise() && isNewApkAvailable) {
+                        if (itemInfo.isPromise() && isNewApkAvailable) {
                             boolean isTargetValid = !cn.getClassName().equals(
                                     IconCache.EMPTY_CLASS_NAME);
-                            if (si.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                            if (itemInfo.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
                                 List<ShortcutInfo> shortcut =
                                         new ShortcutRequest(context, mUser)
                                                 .forPackage(cn.getPackageName(),
-                                                        si.getDeepShortcutId())
+                                                        itemInfo.getDeepShortcutId())
                                                 .query(ShortcutRequest.PINNED);
                                 if (shortcut.isEmpty()) {
                                     isTargetValid = false;
                                     if (DEBUG) {
                                         Log.d(TAG, "Pinned Shortcut not found for updated"
-                                                + " package=" + si.getTargetPackage());
+                                                + " package=" + itemInfo.getTargetPackage());
                                     }
                                 } else {
                                     if (DEBUG) {
                                         Log.d(TAG, "Found pinned shortcut for updated"
-                                                + " package=" + si.getTargetPackage()
+                                                + " package=" + itemInfo.getTargetPackage()
                                                 + ", isTargetValid=" + isTargetValid);
                                     }
-                                    si.updateFromDeepShortcutInfo(shortcut.get(0), context);
+                                    itemInfo.updateFromDeepShortcutInfo(shortcut.get(0), context);
                                     infoUpdated = true;
                                 }
                             } else if (isTargetValid) {
@@ -262,39 +262,39 @@
                                         .isActivityEnabled(cn, mUser);
                             }
 
-                            if (!isTargetValid && (si.hasStatusFlag(
+                            if (!isTargetValid && (itemInfo.hasStatusFlag(
                                     FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)
-                                    || si.isArchived())) {
-                                if (updateWorkspaceItemIntent(context, si, packageName)) {
+                                    || itemInfo.isArchived())) {
+                                if (updateWorkspaceItemIntent(context, itemInfo, packageName)) {
                                     infoUpdated = true;
-                                } else if (si.hasPromiseIconUi()) {
-                                    removedShortcuts.add(si.id);
+                                } else if (itemInfo.hasPromiseIconUi()) {
+                                    removedShortcuts.add(itemInfo.id);
                                     if (DEBUG) {
                                         FileLog.w(TAG, "Removing restored shortcut promise icon"
                                                 + " that no longer points to valid component."
-                                                + " id=" + si.id
-                                                + ", package=" + si.getTargetPackage()
-                                                + ", status=" + si.status
-                                                + ", isArchived=" + si.isArchived());
+                                                + " id=" + itemInfo.id
+                                                + ", package=" + itemInfo.getTargetPackage()
+                                                + ", status=" + itemInfo.status
+                                                + ", isArchived=" + itemInfo.isArchived());
                                     }
                                     return;
                                 }
                             } else if (!isTargetValid) {
-                                removedShortcuts.add(si.id);
+                                removedShortcuts.add(itemInfo.id);
                                 if (DEBUG) {
                                     FileLog.w(TAG, "Removing shortcut that no longer points to"
                                             + " valid component."
-                                            + " id=" + si.id
-                                            + " package=" + si.getTargetPackage()
-                                            + " status=" + si.status);
+                                            + " id=" + itemInfo.id
+                                            + " package=" + itemInfo.getTargetPackage()
+                                            + " status=" + itemInfo.status);
                                 }
                                 return;
                             } else {
-                                si.status = WorkspaceItemInfo.DEFAULT;
+                                itemInfo.status = WorkspaceItemInfo.DEFAULT;
                                 infoUpdated = true;
                             }
                         } else if (isNewApkAvailable && removedComponents.contains(cn)) {
-                            if (updateWorkspaceItemIntent(context, si, packageName)) {
+                            if (updateWorkspaceItemIntent(context, itemInfo, packageName)) {
                                 infoUpdated = true;
                             }
                         }
@@ -304,7 +304,7 @@
                                     packageName);
                             // TODO: See if we can migrate this to
                             //  AppInfo#updateRuntimeFlagsForActivityTarget
-                            si.setProgressLevel(
+                            itemInfo.setProgressLevel(
                                     activities == null || activities.isEmpty()
                                             ? 100
                                             : PackageManagerHelper.getLoadingProgress(
@@ -313,42 +313,42 @@
                             // In case an app is archived, we need to make sure that archived state
                             // in WorkspaceItemInfo is refreshed.
                             if (Flags.enableSupportForArchiving() && !activities.isEmpty()) {
-                                boolean newArchivalState = activities.get(
-                                        0).getActivityInfo().isArchived;
-                                if (newArchivalState != si.isArchived()) {
-                                    si.runtimeStatusFlags ^= FLAG_ARCHIVED;
+                                boolean newArchivalState = activities.get(0)
+                                        .getActivityInfo().isArchived;
+                                if (newArchivalState != itemInfo.isArchived()) {
+                                    itemInfo.runtimeStatusFlags ^= FLAG_ARCHIVED;
                                     infoUpdated = true;
                                 }
                             }
-                            if (si.itemType == Favorites.ITEM_TYPE_APPLICATION) {
+                            if (itemInfo.itemType == Favorites.ITEM_TYPE_APPLICATION) {
                                 if (activities != null && !activities.isEmpty()) {
-                                    si.setNonResizeable(ApiWrapper.INSTANCE.get(context)
+                                    itemInfo.setNonResizeable(ApiWrapper.INSTANCE.get(context)
                                             .isNonResizeableActivity(activities.get(0)));
                                 }
-                                iconCache.getTitleAndIcon(si, si.usingLowResIcon());
+                                iconCache.getTitleAndIcon(itemInfo, itemInfo.usingLowResIcon());
                                 infoUpdated = true;
                             }
                         }
 
-                        int oldRuntimeFlags = si.runtimeStatusFlags;
-                        si.runtimeStatusFlags = flagOp.apply(si.runtimeStatusFlags);
-                        if (si.runtimeStatusFlags != oldRuntimeFlags) {
+                        int oldRuntimeFlags = itemInfo.runtimeStatusFlags;
+                        itemInfo.runtimeStatusFlags = flagOp.apply(itemInfo.runtimeStatusFlags);
+                        if (itemInfo.runtimeStatusFlags != oldRuntimeFlags) {
                             shortcutUpdated = true;
                         }
                     }
 
                     if (infoUpdated || shortcutUpdated) {
-                        updatedWorkspaceItems.add(si);
+                        updatedWorkspaceItems.add(itemInfo);
                     }
-                    if (infoUpdated && si.id != ItemInfo.NO_ID) {
-                        taskController.getModelWriter().updateItemInDatabase(si);
+                    if (infoUpdated && itemInfo.id != ItemInfo.NO_ID) {
+                        taskController.getModelWriter().updateItemInDatabase(itemInfo);
                     }
                 });
 
                 for (LauncherAppWidgetInfo widgetInfo : dataModel.appWidgets) {
                     if (mUser.equals(widgetInfo.user)
                             && widgetInfo.hasRestoreFlag(
-                                    LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
+                            LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
                             && packageSet.contains(widgetInfo.providerName.getPackageName())) {
                         widgetInfo.restoreStatus &=
                                 ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY
@@ -391,7 +391,7 @@
         } else if (mOp == OP_UPDATE) {
             // Mark disabled packages in the broadcast to be removed
             final LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
-            for (int i=0; i<N; i++) {
+            for (int i = 0; i < packageCount; i++) {
                 if (!launcherApps.isPackageEnabled(packages[i], mUser)) {
                     if (DEBUG) {
                         Log.d(TAG, "OP_UPDATE:"
@@ -423,7 +423,7 @@
         if (mOp == OP_ADD) {
             // Load widgets for the new package. Changes due to app updates are handled through
             // AppWidgetHost events, this is just to initialize the long-press options.
-            for (int i = 0; i < N; i++) {
+            for (int i = 0; i < packageCount; i++) {
                 dataModel.widgetsModel.update(app, new PackageUserKey(packages[i], mUser));
             }
             taskController.bindUpdatedWidgets(dataModel);
diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java
index 454ae96..35064cf 100644
--- a/src/com/android/launcher3/model/WidgetsModel.java
+++ b/src/com/android/launcher3/model/WidgetsModel.java
@@ -26,7 +26,6 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.compat.AlphabeticIndexCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.ComponentWithLabelAndIcon;
 import com.android.launcher3.icons.IconCache;
@@ -39,9 +38,6 @@
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 import com.android.launcher3.widget.WidgetSections;
-import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import com.android.launcher3.widget.model.WidgetsListContentEntry;
-import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
 import com.android.wm.shell.Flags;
 
 import java.util.ArrayList;
@@ -54,7 +50,9 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 /**
  * Widgets data model that is used by the adapters of the widget views and controllers.
@@ -67,84 +65,31 @@
     private static final boolean DEBUG = false;
 
     /* Map of widgets and shortcuts that are tracked per package. */
-    private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsList = new HashMap<>();
+    private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsByPackageItem = new HashMap<>();
 
     /**
-     * Returns a list of {@link WidgetsListBaseEntry} filtered using given widget item filter. All
-     * {@link WidgetItem}s in a single row are sorted (based on label and user), but the overall
-     * list of {@link WidgetsListBaseEntry}s is not sorted.
-     *
-     * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
+     * Returns all widgets keyed by their component key.
      */
-    public synchronized ArrayList<WidgetsListBaseEntry> getFilteredWidgetsListForPicker(
-            Context context,
-            Predicate<WidgetItem> widgetItemFilter) {
-        if (!WIDGETS_ENABLED) {
-            return new ArrayList<>();
-        }
-        ArrayList<WidgetsListBaseEntry> result = new ArrayList<>();
-        AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context);
-
-        for (Map.Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsList.entrySet()) {
-            PackageItemInfo pkgItem = entry.getKey();
-            List<WidgetItem> widgetItems = entry.getValue()
-                    .stream()
-                    .filter(widgetItemFilter).toList();
-            if (!widgetItems.isEmpty()) {
-                String sectionName = (pkgItem.title == null) ? "" :
-                        indexer.computeSectionName(pkgItem.title);
-                result.add(WidgetsListHeaderEntry.create(pkgItem, sectionName, widgetItems));
-                result.add(new WidgetsListContentEntry(pkgItem, sectionName, widgetItems));
-            }
-        }
-        return result;
-    }
-
-    /**
-     * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row
-     * are sorted (based on label and user), but the overall list of
-     * {@link WidgetsListBaseEntry}s is not sorted.
-     *
-     * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
-     */
-    public synchronized ArrayList<WidgetsListBaseEntry> getWidgetsListForPicker(Context context) {
-        // return all items
-        return getFilteredWidgetsListForPicker(context, /*widgetItemFilter=*/ item -> true);
-    }
-
-    /** Returns a mapping of packages to their widgets without static shortcuts. */
-    public synchronized Map<PackageUserKey, List<WidgetItem>> getAllWidgetsWithoutShortcuts() {
+    public synchronized Map<ComponentKey, WidgetItem> getWidgetsByComponentKey() {
         if (!WIDGETS_ENABLED) {
             return Collections.emptyMap();
         }
-        Map<PackageUserKey, List<WidgetItem>> packagesToWidgets = new HashMap<>();
-        mWidgetsList.forEach((packageItemInfo, widgetsAndShortcuts) -> {
-            List<WidgetItem> widgets = widgetsAndShortcuts.stream()
-                    .filter(item -> item.widgetInfo != null)
-                    .collect(toList());
-            if (widgets.size() > 0) {
-                packagesToWidgets.put(
-                        new PackageUserKey(packageItemInfo.packageName, packageItemInfo.user),
-                        widgets);
-            }
-        });
-        return packagesToWidgets;
+        return mWidgetsByPackageItem.values().stream()
+                .flatMap(Collection::stream).distinct()
+                .collect(Collectors.toMap(
+                        widget -> new ComponentKey(widget.componentName, widget.user),
+                        Function.identity()
+                ));
     }
 
     /**
-     * Returns a map of widget component keys to corresponding widget items. Excludes the
-     * shortcuts.
+     * Returns widgets grouped by the package item that they should belong to.
      */
-    public synchronized Map<ComponentKey, WidgetItem> getAllWidgetComponentsWithoutShortcuts() {
+    public synchronized Map<PackageItemInfo, List<WidgetItem>> getWidgetsByPackageItem() {
         if (!WIDGETS_ENABLED) {
             return Collections.emptyMap();
         }
-        Map<ComponentKey, WidgetItem> widgetsMap = new HashMap<>();
-        mWidgetsList.forEach((packageItemInfo, widgetsAndShortcuts) ->
-                widgetsAndShortcuts.stream().filter(item -> item.widgetInfo != null).forEach(
-                        item -> widgetsMap.put(new ComponentKey(item.componentName, item.user),
-                                item)));
-        return widgetsMap;
+        return mWidgetsByPackageItem;
     }
 
     /**
@@ -210,14 +155,14 @@
 
         if (packageUser == null) {
             // Clear the list if this is an update on all widgets and shortcuts.
-            mWidgetsList.clear();
+            mWidgetsByPackageItem.clear();
         } else {
             // Otherwise, only clear the widgets and shortcuts for the changed package.
-            mWidgetsList.remove(packageItemInfoCache.getOrCreate(packageUser));
+            mWidgetsByPackageItem.remove(packageItemInfoCache.getOrCreate(packageUser));
         }
 
         // add and update.
-        mWidgetsList.putAll(rawWidgetsShortcuts.stream()
+        mWidgetsByPackageItem.putAll(rawWidgetsShortcuts.stream()
                 .filter(new WidgetValidityCheck(app))
                 .filter(new WidgetFlagCheck())
                 .flatMap(widgetItem -> getPackageUserKeys(app.getContext(), widgetItem).stream()
@@ -237,7 +182,7 @@
             return;
         }
         WidgetManagerHelper widgetManager = new WidgetManagerHelper(app.getContext());
-        for (Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsList.entrySet()) {
+        for (Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsByPackageItem.entrySet()) {
             if (packageNames.contains(entry.getKey().packageName)) {
                 List<WidgetItem> items = entry.getValue();
                 int count = items.size();
@@ -258,50 +203,6 @@
         }
     }
 
-    private PackageItemInfo createPackageItemInfo(
-            ComponentName providerName,
-            UserHandle user,
-            int category
-    ) {
-        if (category == NO_CATEGORY) {
-            return new PackageItemInfo(providerName.getPackageName(), user);
-        } else {
-            return new PackageItemInfo("" , category, user);
-        }
-    }
-
-    private IntSet getCategories(ComponentName providerName, Context context) {
-        IntSet categories = WidgetSections.getWidgetsToCategory(context).get(providerName);
-        if (categories != null) {
-            return categories;
-        }
-        categories = new IntSet();
-        categories.add(NO_CATEGORY);
-        return categories;
-    }
-
-    public WidgetItem getWidgetProviderInfoByProviderName(
-            ComponentName providerName, UserHandle user, Context context) {
-        if (!WIDGETS_ENABLED) {
-            return null;
-        }
-        IntSet categories = getCategories(providerName, context);
-
-        // Checking if we have a provider in any of the categories.
-        for (Integer category: categories) {
-            PackageItemInfo key = createPackageItemInfo(providerName, user, category);
-            List<WidgetItem> widgets = mWidgetsList.get(key);
-            if (widgets != null) {
-                return widgets.stream().filter(
-                                item -> item.componentName.equals(providerName)
-                        )
-                        .findFirst()
-                        .orElse(null);
-            }
-        }
-        return null;
-    }
-
     /** Returns {@link PackageItemInfo} of a pending widget. */
     public static PackageItemInfo newPendingItemInfo(Context context, ComponentName provider,
             UserHandle user) {
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index e44ea1d..a691e45 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -43,6 +43,7 @@
 import android.view.animation.OvershootInterpolator;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Insettable;
 import com.android.launcher3.R;
@@ -131,7 +132,8 @@
     private float mCurrentPosition;
     private float mFinalPosition;
     private boolean mIsScrollPaused;
-    private boolean mIsTwoPanels;
+    @VisibleForTesting
+    boolean mIsTwoPanels;
     private ObjectAnimator mAnimator;
     private @Nullable ObjectAnimator mAlphaAnimator;
 
@@ -477,6 +479,21 @@
         return sTempRect;
     }
 
+    @VisibleForTesting
+    int getActivePage() {
+        return mActivePage;
+    }
+
+    @VisibleForTesting
+    int getNumPages() {
+        return mNumPages;
+    }
+
+    @VisibleForTesting
+    float getCurrentPosition() {
+        return mCurrentPosition;
+    }
+
     private class MyOutlineProver extends ViewOutlineProvider {
 
         @Override
diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java
index 24d58f3..c117be4 100644
--- a/src/com/android/launcher3/pm/InstallSessionTracker.java
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -32,7 +32,6 @@
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.Flags;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.lang.ref.WeakReference;
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 7339111..e861961 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -183,6 +183,11 @@
         mUserToSerialMap.put(userHandle, info);
     }
 
+    @VisibleForTesting
+    public void putToPreInstallCache(UserHandle userHandle, List<String> preInstalledApps) {
+        mUserToPreInstallAppMap.put(userHandle, preInstalledApps);
+    }
+
     /**
      * @see UserManager#getUserProfiles()
      */
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index fb463f7..8a5e388 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -24,28 +24,20 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.dot.DotInfo;
-import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.notification.NotificationKeyData;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.ShortcutUtil;
-import com.android.launcher3.widget.PendingAddWidgetInfo;
-import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import com.android.launcher3.widget.model.WidgetsListContentEntry;
-import com.android.launcher3.widget.picker.WidgetRecommendationCategory;
 
 import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.function.Consumer;
-import java.util.function.Function;
 import java.util.function.Predicate;
-import java.util.stream.Collectors;
 
 /**
  * Provides data for the popup menu that appears after long-clicking on apps.
@@ -62,13 +54,6 @@
     /** Maps packages to their DotInfo's . */
     private Map<PackageUserKey, DotInfo> mPackageUserToDotInfos = new HashMap<>();
 
-    /** All installed widgets. */
-    private List<WidgetsListBaseEntry> mAllWidgets = List.of();
-    /** Widgets that can be recommended to the users. */
-    private List<ItemInfo> mRecommendedWidgets = List.of();
-
-    private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE;
-
     public PopupDataProvider(Consumer<Predicate<PackageUserKey>> notificationDotsChangeListener) {
         mNotificationDotsChangeListener = notificationDotsChangeListener;
     }
@@ -183,102 +168,8 @@
         })) ? dotInfo : null;
     }
 
-    /**
-     * Sets a list of recommended widgets ordered by their order of appearance in the widgets
-     * recommendation UI.
-     */
-    public void setRecommendedWidgets(List<ItemInfo> recommendedWidgets) {
-        mRecommendedWidgets = recommendedWidgets;
-        mChangeListener.onRecommendedWidgetsBound();
-    }
-
-    public void setAllWidgets(List<WidgetsListBaseEntry> allWidgets) {
-        mAllWidgets = allWidgets;
-        mChangeListener.onWidgetsBound();
-    }
-
-    public void setChangeListener(PopupDataChangeListener listener) {
-        mChangeListener = listener == null ? PopupDataChangeListener.INSTANCE : listener;
-    }
-
-    public List<WidgetsListBaseEntry> getAllWidgets() {
-        return mAllWidgets;
-    }
-
-    /** Returns a list of recommended widgets. */
-    public List<WidgetItem> getRecommendedWidgets() {
-        HashMap<ComponentKey, WidgetItem> allWidgetItems = new HashMap<>();
-        mAllWidgets.stream()
-                .filter(entry -> entry instanceof WidgetsListContentEntry)
-                .forEach(entry -> ((WidgetsListContentEntry) entry).mWidgets
-                        .forEach(widget -> allWidgetItems.put(
-                                new ComponentKey(widget.componentName, widget.user), widget)));
-        return mRecommendedWidgets.stream()
-                .map(recommendedWidget -> allWidgetItems.get(
-                        new ComponentKey(recommendedWidget.getTargetComponent(),
-                                recommendedWidget.user)))
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-    }
-
-    /** Returns the recommended widgets mapped by their category. */
-    @NonNull
-    public Map<WidgetRecommendationCategory, List<WidgetItem>> getCategorizedRecommendedWidgets() {
-        Map<ComponentKey, WidgetItem> allWidgetItems = mAllWidgets.stream()
-                .filter(entry -> entry instanceof WidgetsListContentEntry)
-                .flatMap(entry -> entry.mWidgets.stream())
-                .distinct()
-                .collect(Collectors.toMap(
-                        widget -> new ComponentKey(widget.componentName, widget.user),
-                        Function.identity()
-                ));
-        return mRecommendedWidgets.stream()
-                .filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo
-                        && ((PendingAddWidgetInfo) itemInfo).recommendationCategory != null)
-                .collect(Collectors.groupingBy(
-                        it -> ((PendingAddWidgetInfo) it).recommendationCategory,
-                        Collectors.collectingAndThen(
-                                Collectors.toList(),
-                                list -> list.stream()
-                                        .map(it -> allWidgetItems.get(
-                                                new ComponentKey(it.getTargetComponent(),
-                                                        it.user)))
-                                        .filter(Objects::nonNull)
-                                        .collect(Collectors.toList())
-                        )
-                ));
-    }
-
-    public List<WidgetItem> getWidgetsForPackageUser(PackageUserKey packageUserKey) {
-        return mAllWidgets.stream()
-                .filter(row -> row instanceof WidgetsListContentEntry
-                        && row.mPkgItem.packageName.equals(packageUserKey.mPackageName))
-                .flatMap(row -> ((WidgetsListContentEntry) row).mWidgets.stream())
-                .filter(widget -> packageUserKey.mUser.equals(widget.user))
-                .collect(Collectors.toList());
-    }
-
-    /** Gets the WidgetsListContentEntry for the currently selected header. */
-    public WidgetsListContentEntry getSelectedAppWidgets(PackageUserKey packageUserKey) {
-        return (WidgetsListContentEntry) mAllWidgets.stream()
-                .filter(row -> row instanceof WidgetsListContentEntry
-                        && PackageUserKey.fromPackageItemInfo(row.mPkgItem).equals(packageUserKey))
-                .findAny()
-                .orElse(null);
-    }
-
     public void dump(String prefix, PrintWriter writer) {
         writer.println(prefix + "PopupDataProvider:");
         writer.println(prefix + "\tmPackageUserToDotInfos:" + mPackageUserToDotInfos);
     }
-
-    public interface PopupDataChangeListener {
-
-        PopupDataChangeListener INSTANCE = new PopupDataChangeListener() { };
-
-        default void onWidgetsBound() { }
-
-        /** A callback to get notified when recommended widgets are bound. */
-        default void onRecommendedWidgetsBound() { }
-    }
 }
diff --git a/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java b/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java
index 4c94f94..1fd3557 100644
--- a/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java
+++ b/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java
@@ -19,6 +19,8 @@
 import android.view.View;
 
 import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider.WidgetPickerDataChangeListener;
 
 /**
  * Utility class to handle updates while the popup is visible (like widgets and
@@ -27,7 +29,7 @@
  * @param <T> The activity on which the popup shows
  */
 public abstract class PopupLiveUpdateHandler<T extends Context & ActivityContext> implements
-        PopupDataProvider.PopupDataChangeListener, View.OnAttachStateChangeListener {
+        WidgetPickerDataChangeListener, View.OnAttachStateChangeListener {
 
     protected final T mContext;
     protected final PopupContainerWithArrow<T> mPopupContainerWithArrow;
@@ -40,19 +42,25 @@
 
     @Override
     public void onViewAttachedToWindow(View view) {
-        PopupDataProvider popupDataProvider = mContext.getPopupDataProvider();
+        WidgetPickerDataProvider widgetsDataProvider = mContext.getWidgetPickerDataProvider();
 
-        if (popupDataProvider != null) {
-            popupDataProvider.setChangeListener(this);
+        if (widgetsDataProvider != null) {
+            widgetsDataProvider.setChangeListener(this);
         }
     }
 
     @Override
     public void onViewDetachedFromWindow(View view) {
-        PopupDataProvider popupDataProvider = mContext.getPopupDataProvider();
+        WidgetPickerDataProvider widgetsDataProvider = mContext.getWidgetPickerDataProvider();
 
-        if (popupDataProvider != null) {
-            popupDataProvider.setChangeListener(null);
+        if (widgetsDataProvider != null) {
+            widgetsDataProvider.setChangeListener(null);
         }
     }
+
+    @Override
+    public void onWidgetsBound() {} // NO_OP
+
+    @Override
+    public void onRecommendedWidgetsBound() {} // NO_OP
 }
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index 83e9810..f7e1168 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -5,6 +5,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_WIDGETS_TAP;
+import static com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findAllWidgetsForPackageUser;
 
 import android.app.ActivityOptions;
 import android.content.ComponentName;
@@ -28,7 +29,6 @@
 import com.android.launcher3.SecondaryDropTarget;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.PrivateProfileManager;
-import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.pm.UserCache;
@@ -39,9 +39,9 @@
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.widget.WidgetsBottomSheet;
+import com.android.launcher3.widget.picker.model.data.WidgetPickerData;
 
 import java.util.Arrays;
-import java.util.List;
 
 /**
  * Represents a system shortcut for a given app. The shortcut should have a label and icon, and an
@@ -107,11 +107,12 @@
     }
 
     public static final Factory<ActivityContext> WIDGETS = (context, itemInfo, originalView) -> {
-        if (itemInfo.getTargetComponent() == null) return null;
-        final List<WidgetItem> widgets =
-                context.getPopupDataProvider().getWidgetsForPackageUser(new PackageUserKey(
-                        itemInfo.getTargetComponent().getPackageName(), itemInfo.user));
-        if (widgets.isEmpty()) {
+        final PackageUserKey packageUserKey = PackageUserKey.fromItemInfo(itemInfo);
+        if (packageUserKey == null) return null;
+
+        final WidgetPickerData data = context.getWidgetPickerDataProvider().get();
+        if (findAllWidgetsForPackageUser(data, packageUserKey).isEmpty()) {
+            // hides widget picker shortcut if there are no widgets for the package.
             return null;
         }
         return new Widgets(context, itemInfo, originalView);
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 6d6b3b6..f895b30 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -17,6 +17,9 @@
 package com.android.launcher3.recyclerview
 
 import android.content.Context
+import android.view.ViewGroup
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PROTECTED
 import androidx.recyclerview.widget.RecyclerView
 import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
@@ -38,21 +41,18 @@
  * [RecyclerView]. The view inflation will happen on background thread and inflated [ViewHolder]s
  * will be added to [RecycledViewPool] on main thread.
  */
-class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
+class AllAppsRecyclerViewPool<T> : RecycledViewPool() where T : Context, T : ActivityContext {
 
     var hasWorkProfile = false
-    private var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
+    @VisibleForTesting(otherwise = PROTECTED)
+    var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
 
     /**
      * Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
      */
-    fun <T> preInflateAllAppsViewHolders(context: T) where T : Context, T : ActivityContext {
+    fun preInflateAllAppsViewHolders(context: T) {
         val appsView = context.appsView ?: return
         val activeRv: RecyclerView = appsView.activeRecyclerView ?: return
-        val preInflateCount = getPreinflateCount(context)
-        if (preInflateCount <= 0) {
-            return
-        }
 
         // Create a separate context dedicated for all apps preinflation thread. The goal is to
         // create a separate AssetManager obj internally to avoid lock contention with
@@ -77,34 +77,51 @@
                     null
                 ) {
                 override fun setAppsPerRow(appsPerRow: Int) = Unit
+
                 override fun getLayoutManager(): RecyclerView.LayoutManager? = null
             }
 
+        preInflateAllAppsViewHolders(adapter, BaseAllAppsAdapter.VIEW_TYPE_ICON, activeRv) {
+            getPreinflateCount(context)
+        }
+    }
+
+    @VisibleForTesting(otherwise = PROTECTED)
+    fun preInflateAllAppsViewHolders(
+        adapter: RecyclerView.Adapter<*>,
+        viewType: Int,
+        parent: ViewGroup,
+        preInflationCountProvider: () -> Int
+    ) {
+        val preinflationCount = preInflationCountProvider.invoke()
+        if (preinflationCount <= 0) {
+            return
+        }
         mCancellableTask?.cancel()
         var task: CancellableTask<List<ViewHolder>>? = null
         task =
             CancellableTask(
                 {
                     val list: ArrayList<ViewHolder> = ArrayList()
-                    for (i in 0 until preInflateCount) {
+                    for (i in 0 until preinflationCount) {
                         if (task?.canceled == true) {
                             break
                         }
-                        list.add(
-                            adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
-                        )
+                        list.add(adapter.createViewHolder(parent, viewType))
                     }
                     list
                 },
                 MAIN_EXECUTOR,
                 { viewHolders ->
-                    for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
+                    // Run preInflationCountProvider again as the needed VH might have changed
+                    val newPreinflationCount = preInflationCountProvider.invoke()
+                    for (i in 0 until minOf(viewHolders.size, newPreinflationCount)) {
                         putRecycledView(viewHolders[i])
                     }
                 }
             )
         mCancellableTask = task
-        VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask)
+        VIEW_PREINFLATION_EXECUTOR.execute(mCancellableTask)
     }
 
     /**
@@ -125,7 +142,7 @@
      * app icons in size of one all apps pages, so that opening all apps don't need to inflate app
      * icons.
      */
-    fun <T> getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
+    fun getPreinflateCount(context: T): Int {
         var targetPreinflateCount =
             PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
                 EXTRA_ICONS_COUNT
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index 0299a23..9b3292d 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -59,6 +59,7 @@
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -76,6 +77,7 @@
     private View mAppsButton;
 
     private PopupDataProvider mPopupDataProvider;
+    private WidgetPickerDataProvider mWidgetPickerDataProvider;
 
     private boolean mAppDrawerShown = false;
 
@@ -315,6 +317,11 @@
     }
 
     @Override
+    public WidgetPickerDataProvider getWidgetPickerDataProvider() {
+        return mWidgetPickerDataProvider;
+    }
+
+    @Override
     public OnClickListener getItemOnClickListener() {
         return this::onIconClicked;
     }
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index 52ce4e8..bd9298b 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -44,11 +44,13 @@
 import androidx.preference.PreferenceFragmentCompat;
 import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback;
 import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartScreenCallback;
+import androidx.preference.PreferenceGroup;
 import androidx.preference.PreferenceGroup.PreferencePositionCallback;
 import androidx.preference.PreferenceScreen;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.BuildConfig;
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherFiles;
 import com.android.launcher3.R;
 import com.android.launcher3.states.RotationHelper;
@@ -165,6 +167,7 @@
         private boolean mRestartOnResume = false;
 
         private String mHighLightKey;
+
         private boolean mPreferenceHighlighted = false;
 
         @Override
@@ -198,11 +201,62 @@
                 }
             }
 
+            // If the target preference is not in the current preference screen, find the parent
+            // preference screen that contains the target preference and set it as the preference
+            // screen.
+            if (Flags.navigateToChildPreference()
+                    && mHighLightKey != null
+                    && !isKeyInPreferenceGroup(mHighLightKey, screen)) {
+                final PreferenceScreen parentPreferenceScreen =
+                        findParentPreference(screen, mHighLightKey);
+                if (parentPreferenceScreen != null && getActivity() != null) {
+                    if (!TextUtils.isEmpty(parentPreferenceScreen.getTitle())) {
+                        getActivity().setTitle(parentPreferenceScreen.getTitle());
+                    }
+                    setPreferenceScreen(parentPreferenceScreen);
+                    return;
+                }
+            }
+
             if (getActivity() != null && !TextUtils.isEmpty(getPreferenceScreen().getTitle())) {
                 getActivity().setTitle(getPreferenceScreen().getTitle());
             }
         }
 
+        private boolean isKeyInPreferenceGroup(String targetKey, PreferenceGroup parent) {
+            for (int i = 0; i < parent.getPreferenceCount(); i++) {
+                Preference pref = parent.getPreference(i);
+                if (pref.getKey() != null && pref.getKey().equals(targetKey)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Finds the parent preference screen for the given target key.
+         *
+         * @param parent the parent preference screen
+         * @param targetKey the key of the preference to find
+         * @return the parent preference screen that contains the target preference
+         */
+        @Nullable
+        private PreferenceScreen findParentPreference(PreferenceScreen parent, String targetKey) {
+            for (int i = 0; i < parent.getPreferenceCount(); i++) {
+                Preference pref = parent.getPreference(i);
+                if (pref instanceof PreferenceScreen) {
+                    PreferenceScreen foundKey = findParentPreference((PreferenceScreen) pref,
+                            targetKey);
+                    if (foundKey != null) {
+                        return foundKey;
+                    }
+                } else if (pref.getKey() != null && pref.getKey().equals(targetKey)) {
+                    return parent;
+                }
+            }
+            return null;
+        }
+
         @Override
         public void onViewCreated(View view, Bundle savedInstanceState) {
             super.onViewCreated(view, savedInstanceState);
diff --git a/src/com/android/launcher3/util/LogConfig.java b/src/com/android/launcher3/util/LogConfig.java
index 3d4b409..f183f18 100644
--- a/src/com/android/launcher3/util/LogConfig.java
+++ b/src/com/android/launcher3/util/LogConfig.java
@@ -76,4 +76,5 @@
      * When turned on, we enable zero state web data loader related logging.
      */
     public static final String ZERO_WEB_DATA_LOADER = "ZeroStateWebDataLoaderLog";
+    public static final String SEARCH_TARGET_UTIL_LOG = "SearchTargetUtilLog";
 }
diff --git a/src/com/android/launcher3/util/ScreenOnTracker.java b/src/com/android/launcher3/util/ScreenOnTracker.java
index 12eff61..8ee799a 100644
--- a/src/com/android/launcher3/util/ScreenOnTracker.java
+++ b/src/com/android/launcher3/util/ScreenOnTracker.java
@@ -24,7 +24,10 @@
 import android.content.Context;
 import android.content.Intent;
 
+import androidx.annotation.VisibleForTesting;
+
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Consumer;
 
 /**
  * Utility class for tracking if the screen is currently on or off
@@ -34,8 +37,7 @@
     public static final MainThreadInitializedObject<ScreenOnTracker> INSTANCE =
             new MainThreadInitializedObject<>(ScreenOnTracker::new);
 
-    private final SimpleBroadcastReceiver mReceiver =
-            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onReceive);
+    private final SimpleBroadcastReceiver mReceiver;
     private final CopyOnWriteArrayList<ScreenOnListener> mListeners = new CopyOnWriteArrayList<>();
 
     private final Context mContext;
@@ -44,8 +46,20 @@
     private ScreenOnTracker(Context context) {
         // Assume that the screen is on to begin with
         mContext = context;
+        mReceiver = new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onReceive);
+        init();
+    }
+
+    @VisibleForTesting
+    ScreenOnTracker(Context context, SimpleBroadcastReceiver receiver) {
+        mContext = context;
+        mReceiver = receiver;
+        init();
+    }
+
+    private void init() {
         mIsScreenOn = true;
-        mReceiver.register(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
+        mReceiver.register(mContext, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
     }
 
     @Override
@@ -53,7 +67,8 @@
         mReceiver.unregisterReceiverSafely(mContext);
     }
 
-    private void onReceive(Intent intent) {
+    @VisibleForTesting
+    void onReceive(Intent intent) {
         String action = intent.getAction();
         if (ACTION_SCREEN_ON.equals(action)) {
             mIsScreenOn = true;
diff --git a/src/com/android/launcher3/util/ShortcutUtil.java b/src/com/android/launcher3/util/ShortcutUtil.java
index 07b7941..aa4f8af 100644
--- a/src/com/android/launcher3/util/ShortcutUtil.java
+++ b/src/com/android/launcher3/util/ShortcutUtil.java
@@ -54,14 +54,6 @@
                 ? ((WorkspaceItemInfo) info).getPersonKeys() : Utilities.EMPTY_STRING_ARRAY;
     }
 
-    /**
-     * Returns true if the item is a deep shortcut.
-     */
-    public static boolean isDeepShortcut(ItemInfo info) {
-        return info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
-                && info instanceof WorkspaceItemInfo;
-    }
-
     private static boolean isActive(ItemInfo info) {
         boolean isLoading = info instanceof WorkspaceItemInfo
                 && ((WorkspaceItemInfo) info).hasPromiseIconUi();
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index 51749a7..6bae1ba 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -31,6 +31,7 @@
 import android.provider.Settings;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Utilities;
 
@@ -49,14 +50,14 @@
 
     public static final VibrationEffect EFFECT_CLICK =
             createPredefined(VibrationEffect.EFFECT_CLICK);
-    private static final Uri HAPTIC_FEEDBACK_URI =
-            Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED);
+    @VisibleForTesting
+    static final Uri HAPTIC_FEEDBACK_URI = Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED);
 
-    private static final float LOW_TICK_SCALE = 0.9f;
-    private static final float DRAG_TEXTURE_SCALE = 0.03f;
-    private static final float DRAG_COMMIT_SCALE = 0.5f;
-    private static final float DRAG_BUMP_SCALE = 0.4f;
-    private static final int DRAG_TEXTURE_EFFECT_SIZE = 200;
+    @VisibleForTesting static final float LOW_TICK_SCALE = 0.9f;
+    @VisibleForTesting static final float DRAG_TEXTURE_SCALE = 0.03f;
+    @VisibleForTesting static final float DRAG_COMMIT_SCALE = 0.5f;
+    @VisibleForTesting static final float DRAG_BUMP_SCALE = 0.4f;
+    @VisibleForTesting static final int DRAG_TEXTURE_EFFECT_SIZE = 200;
 
     @Nullable
     private final VibrationEffect mDragEffect;
@@ -73,22 +74,29 @@
      */
     public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK;
 
-    private final Context mContext;
     private final Vibrator mVibrator;
     private final boolean mHasVibrator;
-    private final SettingsCache.OnChangeListener mHapticChangeListener =
+
+    private final SettingsCache mSettingsCache;
+
+    @VisibleForTesting
+    final SettingsCache.OnChangeListener mHapticChangeListener =
             isEnabled -> mIsHapticFeedbackEnabled = isEnabled;
 
     private boolean mIsHapticFeedbackEnabled;
 
     private VibratorWrapper(Context context) {
-        mContext = context;
-        mVibrator = context.getSystemService(Vibrator.class);
+        this(context.getSystemService(Vibrator.class), SettingsCache.INSTANCE.get(context));
+    }
+
+    @VisibleForTesting
+    VibratorWrapper(Vibrator vibrator, SettingsCache settingsCache) {
+        mVibrator = vibrator;
         mHasVibrator = mVibrator.hasVibrator();
+        mSettingsCache = settingsCache;
         if (mHasVibrator) {
-            SettingsCache cache = SettingsCache.INSTANCE.get(mContext);
-            cache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
-            mIsHapticFeedbackEnabled = cache.getValue(HAPTIC_FEEDBACK_URI, 0);
+            mSettingsCache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
+            mIsHapticFeedbackEnabled = mSettingsCache.getValue(HAPTIC_FEEDBACK_URI, 0);
         } else {
             mIsHapticFeedbackEnabled = false;
         }
@@ -98,12 +106,7 @@
 
             // Drag texture, Commit, and Bump should only be used for premium phones.
             // Before using these haptics make sure check if the device can use it
-            VibrationEffect.Composition dragEffect = VibrationEffect.startComposition();
-            for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) {
-                dragEffect.addPrimitive(
-                        PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE);
-            }
-            mDragEffect = dragEffect.compose();
+            mDragEffect = getDragEffect();
             mCommitEffect = VibrationEffect.startComposition().addPrimitive(
                     VibrationEffect.Composition.PRIMITIVE_TICK, DRAG_COMMIT_SCALE).compose();
             mBumpEffect = VibrationEffect.startComposition().addPrimitive(
@@ -124,8 +127,7 @@
     @Override
     public void close() {
         if (mHasVibrator) {
-            SettingsCache.INSTANCE.get(mContext)
-                    .unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
+            mSettingsCache.unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
         }
     }
 
@@ -215,4 +217,13 @@
             vibrate(primitiveLowTickEffect);
         }
     }
+
+    static VibrationEffect getDragEffect() {
+        VibrationEffect.Composition dragEffect = VibrationEffect.startComposition();
+        for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) {
+            dragEffect.addPrimitive(
+                    PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE);
+        }
+        return dragEffect.compose();
+    }
 }
diff --git a/src/com/android/launcher3/util/ViewPool.java b/src/com/android/launcher3/util/ViewPool.java
index e413d7f..2fa8bf4 100644
--- a/src/com/android/launcher3/util/ViewPool.java
+++ b/src/com/android/launcher3/util/ViewPool.java
@@ -24,6 +24,7 @@
 import androidx.annotation.AnyThread;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.util.ViewPool.Reusable;
 
@@ -43,9 +44,16 @@
 
     public ViewPool(Context context, @Nullable ViewGroup parent,
             int layoutId, int maxSize, int initialSize) {
+        this(LayoutInflater.from(context).cloneInContext(context),
+                parent, layoutId, maxSize, initialSize);
+    }
+
+    @VisibleForTesting
+    ViewPool(LayoutInflater inflater, @Nullable ViewGroup parent,
+            int layoutId, int maxSize, int initialSize) {
         mLayoutId = layoutId;
         mParent = parent;
-        mInflater = LayoutInflater.from(context);
+        mInflater = inflater;
         mPool = new Object[maxSize];
 
         if (initialSize > 0) {
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index cfac91a..d3160e0 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -81,6 +81,7 @@
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.util.ViewCache;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
 
 import java.util.List;
 
@@ -266,6 +267,14 @@
         return null;
     }
 
+    /**
+     * Returns the {@link WidgetPickerDataProvider} that can be used to read widgets for display.
+     */
+    @Nullable
+    default WidgetPickerDataProvider getWidgetPickerDataProvider() {
+        return null;
+    }
+
     @Nullable
     default StringCache getStringCache() {
         return null;
diff --git a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
index 104209e..856f4b3 100644
--- a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
@@ -110,7 +110,7 @@
         }
         View background = RoundedCornerEnforcement.findBackground(this);
         if (background == null
-                || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
+                || RoundedCornerEnforcement.hasAppWidgetOptedOut(background)) {
             resetRoundedCorners();
             return;
         }
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index c59e295..1c0d94c 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -45,13 +45,13 @@
 import com.android.launcher3.PendingAddItemInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.window.WindowManagerProxy;
 import com.android.launcher3.views.AbstractSlideInView;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider.WidgetPickerDataChangeListener;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -60,7 +60,7 @@
  */
 public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity>
         implements OnClickListener, OnLongClickListener,
-        PopupDataProvider.PopupDataChangeListener, Insettable, OnDeviceProfileChangeListener {
+        WidgetPickerDataChangeListener, Insettable, OnDeviceProfileChangeListener {
     /** The default number of cells that can fit horizontally in a widget sheet. */
     public static final int DEFAULT_MAX_HORIZONTAL_SPANS = 4;
 
@@ -106,14 +106,14 @@
         WindowInsets windowInsets = WindowManagerProxy.INSTANCE.get(getContext())
                 .normalizeWindowInsets(getContext(), getRootWindowInsets(), new Rect());
         mNavBarScrimHeight = getNavBarScrimHeight(windowInsets);
-        mActivityContext.getPopupDataProvider().setChangeListener(this);
+        mActivityContext.getWidgetPickerDataProvider().setChangeListener(this);
         mActivityContext.addOnDeviceProfileChangeListener(this);
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-        mActivityContext.getPopupDataProvider().setChangeListener(null);
+        mActivityContext.getWidgetPickerDataProvider().setChangeListener(null);
         mActivityContext.removeOnDeviceProfileChangeListener(this);
     }
 
diff --git a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
index 1e46ffd..2e5e251 100644
--- a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
+++ b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
@@ -67,7 +67,7 @@
     /**
      * Check whether the app widget has opted out of the enforcement.
      */
-    public static boolean hasAppWidgetOptedOut(@NonNull View appWidget, @NonNull View background) {
+    public static boolean hasAppWidgetOptedOut(@NonNull View background) {
         return background.getId() == android.R.id.background && background.getClipToOutline();
     }
 
diff --git a/src/com/android/launcher3/widget/WidgetManagerHelper.java b/src/com/android/launcher3/widget/WidgetManagerHelper.java
index 9132b4f..23d0585 100644
--- a/src/com/android/launcher3/widget/WidgetManagerHelper.java
+++ b/src/com/android/launcher3/widget/WidgetManagerHelper.java
@@ -32,6 +32,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -57,8 +58,13 @@
     final Context mContext;
 
     public WidgetManagerHelper(Context context) {
+        this(context, AppWidgetManager.getInstance(context));
+    }
+
+    @VisibleForTesting
+    public WidgetManagerHelper(Context context, AppWidgetManager appWidgetManager) {
         mContext = context;
-        mAppWidgetManager = AppWidgetManager.getInstance(context);
+        mAppWidgetManager = appWidgetManager;
     }
 
     /**
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index 894099d..ddbd291 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.widget;
 
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY;
+import static com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findAllWidgetsForPackageUser;
 
 import android.content.Context;
 import android.graphics.Rect;
@@ -40,6 +41,7 @@
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.picker.model.data.WidgetPickerData;
 import com.android.launcher3.widget.util.WidgetsTableUtils;
 
 import java.util.List;
@@ -124,10 +126,10 @@
 
     @Override
     public void onWidgetsBound() {
-        List<WidgetItem> widgets = mActivityContext.getPopupDataProvider().getWidgetsForPackageUser(
-                new PackageUserKey(
-                        mOriginalItemInfo.getTargetComponent().getPackageName(),
-                        mOriginalItemInfo.user));
+        final WidgetPickerData data = mActivityContext.getWidgetPickerDataProvider().get();
+        final PackageUserKey packageUserKey = PackageUserKey.fromItemInfo(mOriginalItemInfo);
+        List<WidgetItem> widgets = packageUserKey != null ? findAllWidgetsForPackageUser(data,
+                packageUserKey) : List.of();
 
         TableLayout widgetsTable = findViewById(R.id.widgets_table);
         widgetsTable.removeAllViews();
@@ -247,4 +249,7 @@
             }
         }
     }
+
+    @Override
+    public void onRecommendedWidgetsBound() {} // no op
 }
diff --git a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
index 398b1df..5ad9222 100644
--- a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
@@ -22,6 +22,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
@@ -52,6 +54,9 @@
         }
     }
 
+    @VisibleForTesting
+    CustomAppWidgetProviderInfo() {}
+
     @Override
     public void initSpans(Context context, InvariantDeviceProfile idp) {
         mIsMinSizeFulfilled = Math.min(spanX, minSpanX) <= idp.numColumns
diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
index 50012b3..faa5d12 100644
--- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
+++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
@@ -30,6 +30,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
 import com.android.launcher3.util.MainThreadInitializedObject;
@@ -45,6 +46,7 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 
@@ -62,9 +64,16 @@
     private final HashMap<ComponentName, CustomWidgetPlugin> mPlugins;
     private final List<CustomAppWidgetProviderInfo> mCustomWidgets;
     private Consumer<PackageUserKey> mWidgetRefreshCallback;
+    private final @NonNull AppWidgetManager mAppWidgetManager;
 
     private CustomWidgetManager(Context context) {
+        this(context, AppWidgetManager.getInstance(context));
+    }
+
+    @VisibleForTesting
+    CustomWidgetManager(Context context, @NonNull AppWidgetManager widgetManager) {
         mContext = context;
+        mAppWidgetManager = widgetManager;
         mPlugins = new HashMap<>();
         mCustomWidgets = new ArrayList<>();
         PluginManagerWrapper.INSTANCE.get(context)
@@ -94,7 +103,7 @@
 
     @Override
     public void onPluginConnected(CustomWidgetPlugin plugin, Context context) {
-        List<AppWidgetProviderInfo> providers = AppWidgetManager.getInstance(context)
+        List<AppWidgetProviderInfo> providers = mAppWidgetManager
                 .getInstalledProvidersForProfile(Process.myUserHandle());
         if (providers.isEmpty()) return;
         Parcel parcel = Parcel.obtain();
@@ -113,6 +122,12 @@
         mCustomWidgets.removeIf(w -> w.getComponent().equals(cn));
     }
 
+    @VisibleForTesting
+    @NonNull
+    Map<ComponentName, CustomWidgetPlugin> getPlugins() {
+        return mPlugins;
+    }
+
     /**
      * Inject a callback function to refresh the widgets.
      */
diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilder.kt b/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilder.kt
new file mode 100644
index 0000000..1abe4da
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilder.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.model
+
+import android.content.Context
+import com.android.launcher3.compat.AlphabeticIndexCompat
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.model.data.PackageItemInfo
+import java.util.function.Predicate
+
+/**
+ * A helper class that builds the list of [WidgetsListBaseEntry]s used by the UI to display widgets.
+ */
+class WidgetsListBaseEntriesBuilder(val context: Context) {
+
+    /** Builds the widgets list entries in a format understandable by the widget picking UI. */
+    @JvmOverloads
+    fun build(
+        widgetsByPackageItem: Map<PackageItemInfo, List<WidgetItem>>,
+        widgetFilter: Predicate<WidgetItem> = Predicate<WidgetItem> { true },
+    ): List<WidgetsListBaseEntry> {
+        val indexer = AlphabeticIndexCompat(context)
+
+        return buildList {
+            for ((pkgItem, widgetItems) in widgetsByPackageItem.entries) {
+                val filteredWidgetItems = widgetItems.filter { widgetFilter.test(it) }
+                if (filteredWidgetItems.isNotEmpty()) {
+                    // Enables fast scroll popup to show right characters in all locales.
+                    val sectionName = pkgItem.title?.let { indexer.computeSectionName(it) } ?: ""
+
+                    add(WidgetsListHeaderEntry.create(pkgItem, sectionName, filteredWidgetItems))
+                    add(WidgetsListContentEntry(pkgItem, sectionName, filteredWidgetItems))
+                }
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java
index 072d1d5..a68effd 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java
@@ -19,6 +19,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 
+import com.android.launcher3.R;
+
 import java.util.Objects;
 
 /**
@@ -26,6 +28,10 @@
  * option in the pop-up opened on long press of launcher workspace).
  */
 public class WidgetRecommendationCategory implements Comparable<WidgetRecommendationCategory> {
+    public static WidgetRecommendationCategory DEFAULT_WIDGET_RECOMMENDATION_CATEGORY =
+            new WidgetRecommendationCategory(
+                    R.string.others_widget_recommendation_category_label, /*order=*/0);
+
     /** Resource id that holds the user friendly label for the category. */
     @StringRes
     public final int categoryTitleRes;
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 2e36583..2af8e6f 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -69,10 +69,12 @@
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.picker.search.SearchModeListener;
 import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
+import com.android.launcher3.widget.picker.search.WidgetsSearchBar.WidgetsSearchDataProvider;
 import com.android.launcher3.workprofile.PersonalWorkPagedView;
 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -135,7 +137,7 @@
     private WidgetsRecyclerView mCurrentTouchEventRecyclerView;
     @Nullable
     PersonalWorkPagedView mViewPager;
-    private boolean mIsInSearchMode;
+    protected boolean mIsInSearchMode;
     private boolean mIsNoWidgetsViewNeeded;
     @Px
     protected int mMaxSpanPerRow;
@@ -245,8 +247,12 @@
         mSearchBarContainer = mSearchScrollView.findViewById(R.id.search_bar_container);
         mSearchBar = mSearchScrollView.findViewById(R.id.widgets_search_bar);
 
-        mSearchBar.initialize(
-                mActivityContext.getPopupDataProvider(), /* searchModeListener= */ this);
+        mSearchBar.initialize(new WidgetsSearchDataProvider() {
+            @Override
+            public List<WidgetsListBaseEntry> getWidgets() {
+                return getWidgetsToDisplay();
+            }
+        }, /* searchModeListener= */ this);
     }
 
     private void setDeviceManagementResources() {
@@ -462,22 +468,28 @@
         setTranslationShift(mTranslationShift);
     }
 
+    /**
+     * Returns all displayable widgets.
+     */
+    protected List<WidgetsListBaseEntry> getWidgetsToDisplay() {
+        return mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets();
+    }
+
     @Override
     public void onWidgetsBound() {
         if (mIsInSearchMode) {
             return;
         }
-        List<WidgetsListBaseEntry> allWidgets =
-                mActivityContext.getPopupDataProvider().getAllWidgets();
+        List<WidgetsListBaseEntry> widgets = getWidgetsToDisplay();
 
         AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY);
-        primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
+        primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets);
 
         if (mHasWorkProfile) {
             mViewPager.setVisibility(VISIBLE);
             mTabBar.setVisibility(VISIBLE);
             AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
-            workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
+            workUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets);
             onActivePageChanged(mViewPager.getCurrentPage());
         } else {
             onActivePageChanged(0);
@@ -561,12 +573,11 @@
         if (mIsInSearchMode) {
             return;
         }
-
         if (enableCategorizedWidgetSuggestions()) {
             // We avoid applying new recommendations when some are already displayed.
             if (mRecommendedWidgetsMap.isEmpty()) {
                 mRecommendedWidgetsMap =
-                        mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets();
+                        mActivityContext.getWidgetPickerDataProvider().get().getRecommendations();
             }
             mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
                     mRecommendedWidgetsMap,
@@ -578,17 +589,20 @@
             );
         } else {
             if (mRecommendedWidgets.isEmpty()) {
-                mRecommendedWidgets =
-                        mActivityContext.getPopupDataProvider().getRecommendedWidgets();
+                mRecommendedWidgets = mActivityContext.getWidgetPickerDataProvider().get()
+                        .getRecommendations()
+                        .values().stream()
+                        .flatMap(Collection::stream).toList();
+                mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
+                        mRecommendedWidgets,
+                        mDeviceProfile,
+                        /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
+                        /* availableWidth= */ mMaxSpanPerRow,
+                        /* cellPadding= */ mWidgetCellHorizontalPadding
+                );
             }
-            mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
-                    mRecommendedWidgets,
-                    mDeviceProfile,
-                    /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
-                    /* availableWidth= */ mMaxSpanPerRow,
-                    /* cellPadding= */ mWidgetCellHorizontalPadding
-            );
         }
+
         mWidgetRecommendationsContainer.setVisibility(
                 mRecommendedWidgetsCount > 0 ? VISIBLE : GONE);
     }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index c84680d..c2cd903 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -22,12 +22,15 @@
 import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree;
 import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree;
 import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.WIDGET_LIST_ITEM_APPEARANCE_DELAY;
+import static com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findContentEntryForPackageUser;
 
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Process;
 import android.util.AttributeSet;
+import android.view.Gravity;
 import android.view.LayoutInflater;
+import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -35,6 +38,7 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
+import android.widget.PopupMenu;
 import android.widget.ScrollView;
 import android.widget.TextView;
 
@@ -83,6 +87,17 @@
     private PackageUserKey mSelectedHeader;
     private TextView mHeaderDescription;
 
+    /**
+     * A menu displayed for options (e.g. "show all widgets" filter) around widget lists in the
+     * picker.
+     */
+    protected View mWidgetOptionsMenu;
+    /**
+     * State of the options in the menu (if displayed to the user).
+     */
+    @Nullable
+    protected WidgetOptionsMenuState mWidgetOptionsMenuState = null;
+
     public WidgetsTwoPaneSheet(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
     }
@@ -130,6 +145,9 @@
         mHeaderTitle = mContent.findViewById(R.id.title);
         mHeaderDescription = mContent.findViewById(R.id.widget_picker_description);
 
+        mWidgetOptionsMenu = mContent.findViewById(R.id.widget_picker_widget_options_menu);
+        setupWidgetOptionsMenu();
+
         mRightPane = mContent.findViewById(R.id.right_pane);
         mRightPaneScrollView = mContent.findViewById(R.id.right_pane_scroll_view);
         mRightPaneScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
@@ -155,6 +173,40 @@
         }
     }
 
+    protected void setupWidgetOptionsMenu() {
+        mWidgetOptionsMenu.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mWidgetOptionsMenuState != null) {
+                    PopupMenu popupMenu = new PopupMenu(mActivityContext, /*anchor=*/ v,
+                            Gravity.END);
+                    MenuItem menuItem = popupMenu.getMenu().add(
+                            R.string.widget_picker_show_all_widgets_menu_item_title);
+                    menuItem.setCheckable(true);
+                    menuItem.setChecked(mWidgetOptionsMenuState.showAllWidgets);
+                    menuItem.setOnMenuItemClickListener(
+                            item -> onShowAllWidgetsMenuItemClick(item));
+                    popupMenu.show();
+                }
+            }
+        });
+    }
+
+    private boolean onShowAllWidgetsMenuItemClick(MenuItem menuItem) {
+        mWidgetOptionsMenuState.showAllWidgets = !mWidgetOptionsMenuState.showAllWidgets;
+        menuItem.setChecked(mWidgetOptionsMenuState.showAllWidgets);
+
+        // Refresh widgets
+        onWidgetsBound();
+        if (mIsInSearchMode) {
+            mSearchBar.reset();
+        } else if (!mSuggestedWidgetsPackageUserKey.equals(mSelectedHeader)) {
+            mAdapters.get(mActivePage).mWidgetsListAdapter.selectFirstHeaderEntry();
+            mAdapters.get(mActivePage).mWidgetsRecyclerView.scrollToTop();
+        }
+        return true;
+    }
+
     @Override
     protected int getTabletHorizontalMargin(DeviceProfile deviceProfile) {
         if (enableCategorizedWidgetSuggestions()) {
@@ -234,6 +286,29 @@
     }
 
     @Override
+    protected List<WidgetsListBaseEntry> getWidgetsToDisplay() {
+        List<WidgetsListBaseEntry> allWidgets =
+                mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets();
+        List<WidgetsListBaseEntry> defaultWidgets =
+                mActivityContext.getWidgetPickerDataProvider().get().getDefaultWidgets();
+
+        if (allWidgets.isEmpty() || defaultWidgets.isEmpty()) {
+            // no menu if there are no default widgets to show
+            mWidgetOptionsMenuState = null;
+            mWidgetOptionsMenu.setVisibility(GONE);
+        } else {
+            if (mWidgetOptionsMenuState == null) {
+                mWidgetOptionsMenuState = new WidgetOptionsMenuState();
+            }
+
+            mWidgetOptionsMenu.setVisibility(VISIBLE);
+            return mWidgetOptionsMenuState.showAllWidgets ? allWidgets : defaultWidgets;
+        }
+
+        return allWidgets;
+    }
+
+    @Override
     public void onWidgetsBound() {
         super.onWidgetsBound();
         if (mRecommendedWidgetsCount == 0 && mSelectedHeader == null) {
@@ -285,7 +360,7 @@
         WidgetsListHeaderEntry widgetsListHeaderEntry = WidgetsListHeaderEntry.create(
                         packageItemInfo,
                         /*titleSectionName=*/ suggestionsHeaderTitle,
-                        /*items=*/ mActivityContext.getPopupDataProvider().getRecommendedWidgets(),
+                        /*items=*/ List.of(), // not necessary
                         /*visibleWidgetsCount=*/ 0)
                 .withWidgetListShown();
 
@@ -435,8 +510,11 @@
                 final boolean isUserClick = mSelectedHeader != null
                         && !getAccessibilityInitialFocusView().isAccessibilityFocused();
                 mSelectedHeader = selectedHeader;
-                WidgetsListContentEntry contentEntry = mActivityContext.getPopupDataProvider()
-                        .getSelectedAppWidgets(selectedHeader);
+                final boolean showDefaultWidgets = mWidgetOptionsMenuState != null
+                        && !mWidgetOptionsMenuState.showAllWidgets;
+                WidgetsListContentEntry contentEntry = findContentEntryForPackageUser(
+                        mActivityContext.getWidgetPickerDataProvider().get(),
+                        selectedHeader, showDefaultWidgets);
 
                 if (contentEntry == null || mRightPane == null) {
                     return;
@@ -570,4 +648,15 @@
          */
         void onHeaderChanged(@NonNull PackageUserKey selectedHeader);
     }
+
+    /**
+     * Holds the selection state of the options menu (if presented to the user).
+     */
+    protected static class WidgetOptionsMenuState {
+        /**
+         * UI state indicating whether to show default or all widgets.
+         * <p>If true, shows all widgets; else shows the default widgets.</p>
+         */
+        public boolean showAllWidgets = false;
+    }
 }
diff --git a/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProvider.kt b/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProvider.kt
new file mode 100644
index 0000000..46d3e7a
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProvider.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.model
+
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.widget.model.WidgetsListBaseEntry
+import com.android.launcher3.widget.picker.model.data.WidgetPickerData
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withRecommendedWidgets
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withWidgets
+import java.io.PrintWriter
+
+/**
+ * Provides [WidgetPickerData] to various views such as widget picker, app-specific widget picker,
+ * widgets shortcut.
+ */
+class WidgetPickerDataProvider {
+    /** All the widgets data provided for the views */
+    private var mWidgetPickerData: WidgetPickerData = WidgetPickerData()
+
+    private var changeListener: WidgetPickerDataChangeListener? = null
+
+    /** Sets a listener to be called back when widget data is updated. */
+    fun setChangeListener(changeListener: WidgetPickerDataChangeListener?) {
+        this.changeListener = changeListener
+    }
+
+    /** Returns the current snapshot of [WidgetPickerData]. */
+    fun get(): WidgetPickerData {
+        return mWidgetPickerData
+    }
+
+    /**
+     * Updates the widgets available to the widget picker.
+     *
+     * Generally called when the widgets model has new data.
+     */
+    @JvmOverloads
+    fun setWidgets(
+        allWidgets: List<WidgetsListBaseEntry>,
+        defaultWidgets: List<WidgetsListBaseEntry> = listOf()
+    ) {
+        mWidgetPickerData =
+            mWidgetPickerData.withWidgets(allWidgets = allWidgets, defaultWidgets = defaultWidgets)
+        changeListener?.onWidgetsBound()
+    }
+
+    /**
+     * Makes the widget recommendations available to the widget picker
+     *
+     * Generally called when new widget predictions are available.
+     */
+    fun setWidgetRecommendations(recommendations: List<ItemInfo>) {
+        mWidgetPickerData = mWidgetPickerData.withRecommendedWidgets(recommendations)
+        changeListener?.onRecommendedWidgetsBound()
+    }
+
+    /** Writes the current state to the provided writer. */
+    fun dump(prefix: String, writer: PrintWriter) {
+        writer.println(prefix + "WidgetPickerDataProvider:")
+        writer.println("$prefix\twidgetPickerData:$mWidgetPickerData")
+    }
+
+    interface WidgetPickerDataChangeListener {
+        /** A callback to get notified when widgets are bound. */
+        fun onWidgetsBound()
+
+        /** A callback to get notified when recommended widgets are bound. */
+        fun onRecommendedWidgetsBound()
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/model/data/WidgetPickerData.kt b/src/com/android/launcher3/widget/picker/model/data/WidgetPickerData.kt
new file mode 100644
index 0000000..3332ef0
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/model/data/WidgetPickerData.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.model.data
+
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.widget.PendingAddWidgetInfo
+import com.android.launcher3.widget.model.WidgetsListBaseEntry
+import com.android.launcher3.widget.model.WidgetsListContentEntry
+import com.android.launcher3.widget.picker.WidgetRecommendationCategory
+import com.android.launcher3.widget.picker.WidgetRecommendationCategory.DEFAULT_WIDGET_RECOMMENDATION_CATEGORY
+
+// This file contains WidgetPickerData and utility functions to operate on it.
+
+/** Widget data for display in the widget picker. */
+data class WidgetPickerData(
+    val allWidgets: List<WidgetsListBaseEntry> = listOf(),
+    val defaultWidgets: List<WidgetsListBaseEntry> = listOf(),
+    val recommendations: Map<WidgetRecommendationCategory, List<WidgetItem>> = mapOf(),
+)
+
+/** Provides utility methods to work with a [WidgetPickerData] object. */
+object WidgetPickerDataUtils {
+    /**
+     * Returns a [WidgetPickerData] with the provided widgets.
+     *
+     * When [defaultWidgets] is not passed, defaults from previous object are not copied over.
+     * Defaults (if supported) should be updated when all widgets are updated.
+     */
+    fun WidgetPickerData.withWidgets(
+        allWidgets: List<WidgetsListBaseEntry>,
+        defaultWidgets: List<WidgetsListBaseEntry> = listOf()
+    ): WidgetPickerData {
+        return copy(allWidgets = allWidgets, defaultWidgets = defaultWidgets)
+    }
+
+    /** Returns a [WidgetPickerData] with the given recommendations set. */
+    fun WidgetPickerData.withRecommendedWidgets(recommendations: List<ItemInfo>): WidgetPickerData {
+        val allWidgetsMap: Map<ComponentKey, WidgetItem> =
+            allWidgets
+                .filterIsInstance<WidgetsListContentEntry>()
+                .flatMap { it.mWidgets }
+                .filterNotNull()
+                .distinct()
+                .associateBy { it } // as ComponentKey
+
+        val categoriesMap =
+            recommendations
+                .filterIsInstance<PendingAddWidgetInfo>()
+                .filter { allWidgetsMap.containsKey(ComponentKey(it.targetComponent, it.user)) }
+                .groupBy { it.recommendationCategory ?: DEFAULT_WIDGET_RECOMMENDATION_CATEGORY }
+                .mapValues { (_, pendingAddWidgetInfos) ->
+                    pendingAddWidgetInfos.map {
+                        allWidgetsMap[ComponentKey(it.targetComponent, it.user)] as WidgetItem
+                    }
+                }
+
+        return copy(recommendations = categoriesMap)
+    }
+
+    /** Finds all [WidgetItem]s available for the provided package user. */
+    @JvmStatic
+    fun findAllWidgetsForPackageUser(
+        widgetPickerData: WidgetPickerData,
+        packageUserKey: PackageUserKey
+    ): List<WidgetItem> {
+        return findContentEntryForPackageUser(widgetPickerData, packageUserKey)?.mWidgets
+            ?: emptyList()
+    }
+
+    /**
+     * Finds and returns the [WidgetsListContentEntry] for the given package user.
+     *
+     * Set [fromDefaultWidgets] to true to limit the content entry to default widgets.
+     */
+    @JvmOverloads
+    @JvmStatic
+    fun findContentEntryForPackageUser(
+        widgetPickerData: WidgetPickerData,
+        packageUserKey: PackageUserKey,
+        fromDefaultWidgets: Boolean = false
+    ): WidgetsListContentEntry? {
+        val widgetsListBaseEntries =
+            if (fromDefaultWidgets) {
+                widgetPickerData.defaultWidgets
+            } else {
+                widgetPickerData.allWidgets
+            }
+
+        return widgetsListBaseEntries.filterIsInstance<WidgetsListContentEntry>().firstOrNull {
+            PackageUserKey.fromPackageItemInfo(it.mPkgItem) == packageUserKey
+        }
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java b/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java
index 65937b6..92caf3e 100644
--- a/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java
+++ b/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java
@@ -26,7 +26,6 @@
 
 import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.R;
-import com.android.launcher3.popup.PopupDataProvider;
 
 /**
  * View for a search bar with an edit text with a cancel button.
@@ -51,7 +50,8 @@
     }
 
     @Override
-    public void initialize(PopupDataProvider dataProvider, SearchModeListener searchModeListener) {
+    public void initialize(WidgetsSearchDataProvider dataProvider,
+            SearchModeListener searchModeListener) {
         mController = new WidgetsSearchBarController(
                 new SimpleWidgetsSearchAlgorithm(dataProvider),
                 mEditText, mCancelButton, searchModeListener);
diff --git a/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java
index 613066a..0e88787 100644
--- a/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java
+++ b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java
@@ -21,13 +21,13 @@
 import android.os.Handler;
 
 import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.search.SearchAlgorithm;
 import com.android.launcher3.search.SearchCallback;
 import com.android.launcher3.search.StringMatcherUtility.StringMatcher;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.picker.search.WidgetsSearchBar.WidgetsSearchDataProvider;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -39,9 +39,9 @@
 public final class SimpleWidgetsSearchAlgorithm implements SearchAlgorithm<WidgetsListBaseEntry> {
 
     private final Handler mResultHandler;
-    private final PopupDataProvider mDataProvider;
+    private final WidgetsSearchDataProvider mDataProvider;
 
-    public SimpleWidgetsSearchAlgorithm(PopupDataProvider dataProvider) {
+    public SimpleWidgetsSearchAlgorithm(WidgetsSearchDataProvider dataProvider) {
         mResultHandler = new Handler();
         mDataProvider = dataProvider;
     }
@@ -63,9 +63,9 @@
      * Returns entries for all matched widgets
      */
     public static ArrayList<WidgetsListBaseEntry> getFilteredWidgets(
-            PopupDataProvider dataProvider, String input) {
+            WidgetsSearchDataProvider dataProvider, String input) {
         ArrayList<WidgetsListBaseEntry> results = new ArrayList<>();
-        dataProvider.getAllWidgets().stream()
+        dataProvider.getWidgets().stream()
                 .filter(entry -> entry instanceof WidgetsListHeaderEntry)
                 .forEach(headerEntry -> {
                     List<WidgetItem> matchedWidgetItems = filterWidgetItems(
diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
index 44a5e80..ab504e7 100644
--- a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
@@ -16,7 +16,9 @@
 
 package com.android.launcher3.widget.picker.search;
 
-import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import java.util.List;
 
 /**
  * Interface for a widgets picker search bar.
@@ -25,7 +27,7 @@
     /**
      * Attaches a controller to the search bar which interacts with {@code searchModeListener}.
      */
-    void initialize(PopupDataProvider dataProvider, SearchModeListener searchModeListener);
+    void initialize(WidgetsSearchDataProvider dataProvider, SearchModeListener searchModeListener);
 
     /**
      * Clears search bar.
@@ -44,4 +46,15 @@
      * Sets the vertical location, in pixels, of this search bar relative to its top position.
      */
     void setTranslationY(float translationY);
+
+
+    /**
+     * Provides corpus from which search results must be returned.
+     */
+    interface WidgetsSearchDataProvider {
+        /**
+         * Returns the widgets from which the search should return the results.
+         */
+        List<WidgetsListBaseEntry> getWidgets();
+    }
 }
diff --git a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index fab3015..dc3b321 100644
--- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -157,7 +157,6 @@
             "get-overview-current-page-index";
     public static final String REQUEST_GET_SPLIT_SELECTION_ACTIVE = "get-split-selection-active";
     public static final String REQUEST_ENABLE_ROTATION = "enable_rotation";
-    public static final String REQUEST_ENABLE_SUGGESTION = "enable-suggestion";
     public static final String REQUEST_MODEL_QUEUE_CLEARED = "model-queue-cleared";
 
     public static boolean sDebugTracing = false;
diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
index 0538870..954dc8f 100644
--- a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
@@ -47,14 +47,13 @@
 abstract class FakeInvariantDeviceProfileTest {
 
     protected lateinit var context: Context
-    protected var inv: InvariantDeviceProfile? = null
-    protected val info: Info = mock()
-    protected var windowBounds: WindowBounds? = null
-    protected var isMultiWindowMode: Boolean = false
-    protected var transposeLayoutWithOrientation: Boolean = false
-    protected var useTwoPanels: Boolean = false
-    protected var isGestureMode: Boolean = true
-    protected var isTransientTaskbar: Boolean = true
+    protected lateinit var inv: InvariantDeviceProfile
+    protected val info = mock<Info>()
+    protected lateinit var windowBounds: WindowBounds
+    private var transposeLayoutWithOrientation = false
+    private var useTwoPanels = false
+    private var isGestureMode = true
+    private var isTransientTaskbar = true
 
     @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
 
@@ -73,7 +72,7 @@
             info,
             windowBounds,
             SparseArray(),
-            isMultiWindowMode,
+            /*isMultiWindowMode=*/ false,
             transposeLayoutWithOrientation,
             useTwoPanels,
             isGestureMode,
diff --git a/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt b/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt
index 60a4197..d0aa7a8 100644
--- a/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt
@@ -17,12 +17,19 @@
 package com.android.launcher3
 
 import android.content.Context
+import android.content.ContextWrapper
+import android.graphics.Rect
+import android.graphics.RectF
 import android.view.View
 import android.view.ViewGroup
 import androidx.test.core.app.ApplicationProvider.getApplicationContext
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.util.ActivityContextWrapper
-import org.junit.Assert.*
+import kotlin.random.Random
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -30,6 +37,10 @@
 @RunWith(AndroidJUnit4::class)
 class UtilitiesTest {
 
+    companion object {
+        const val SEED = 827
+    }
+
     private lateinit var mContext: Context
 
     @Before
@@ -94,4 +105,284 @@
         assertTrue(Utilities.pointInView(view, -5f, -5f, 10f)) // Inside slop
         assertFalse(Utilities.pointInView(view, 115f, 115f, 10f)) // Outside slop
     }
+
+    @Test
+    fun testNumberBounding() {
+        assertEquals(887.99f, Utilities.boundToRange(887.99f, 0f, 1000f))
+        assertEquals(2.777f, Utilities.boundToRange(887.99f, 0f, 2.777f))
+        assertEquals(900f, Utilities.boundToRange(887.99f, 900f, 1000f))
+
+        assertEquals(9383667L, Utilities.boundToRange(9383667L, -999L, 9999999L))
+        assertEquals(9383668L, Utilities.boundToRange(9383667L, 9383668L, 9999999L))
+        assertEquals(42L, Utilities.boundToRange(9383667L, -999L, 42L))
+
+        assertEquals(345, Utilities.boundToRange(345, 2, 500))
+        assertEquals(400, Utilities.boundToRange(345, 400, 500))
+        assertEquals(300, Utilities.boundToRange(345, 2, 300))
+
+        val random = Random(SEED)
+        for (i in 1..300) {
+            val value = random.nextFloat()
+            val lowerBound = random.nextFloat()
+            val higherBound = lowerBound + random.nextFloat()
+
+            assertEquals(
+                "Utilities.boundToRange doesn't match Kotlin coerceIn",
+                value.coerceIn(lowerBound, higherBound),
+                Utilities.boundToRange(value, lowerBound, higherBound)
+            )
+            assertEquals(
+                "Utilities.boundToRange doesn't match Kotlin coerceIn",
+                value.toInt().coerceIn(lowerBound.toInt(), higherBound.toInt()),
+                Utilities.boundToRange(value.toInt(), lowerBound.toInt(), higherBound.toInt())
+            )
+            assertEquals(
+                "Utilities.boundToRange doesn't match Kotlin coerceIn",
+                value.toLong().coerceIn(lowerBound.toLong(), higherBound.toLong()),
+                Utilities.boundToRange(value.toLong(), lowerBound.toLong(), higherBound.toLong())
+            )
+            assertEquals(
+                "If the lower bound is higher than lower bound, it should return the lower bound",
+                higherBound,
+                Utilities.boundToRange(value, higherBound, lowerBound)
+            )
+        }
+    }
+
+    @Test
+    fun testTranslateOverlappingView() {
+        testConcentricOverlap()
+        leftDownCornerOverlap()
+        noOverlap()
+    }
+
+    /*
+        Test Case: Rectangle Contained Within Another Rectangle
+
+           +-------------+  <-- exclusionBounds
+           |             |
+           |   +-----+   |
+           |   |     |   |  <-- targetViewBounds
+           |   |     |   |
+           |   +-----+   |
+           |             |
+           +-------------+
+    */
+    private fun testConcentricOverlap() {
+        val targetView = View(ContextWrapper(getApplicationContext()))
+        val targetViewBounds = Rect(40, 40, 60, 60)
+        val inclusionBounds = Rect(0, 0, 100, 100)
+        val exclusionBounds = Rect(30, 30, 70, 70)
+
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_RIGHT
+        )
+        assertEquals(30f, targetView.translationX)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_LEFT
+        )
+        assertEquals(-30f, targetView.translationX)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_DOWN
+        )
+        assertEquals(30f, targetView.translationY)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_UP
+        )
+        assertEquals(-30f, targetView.translationY)
+    }
+
+    /*
+    Test Case: Non-Overlapping Rectangles
+
+        +-----------------+      <-- targetViewBounds
+        |                 |
+        |                 |
+        +-----------------+
+
+                 +-----------+     <-- exclusionBounds
+                 |           |
+                 |           |
+                 +-----------+
+    */
+    private fun noOverlap() {
+        val targetView = View(ContextWrapper(getApplicationContext()))
+        val targetViewBounds = Rect(10, 10, 20, 20)
+
+        val inclusionBounds = Rect(0, 0, 100, 100)
+        val exclusionBounds = Rect(30, 30, 40, 40)
+
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_RIGHT
+        )
+        assertEquals(0f, targetView.translationX)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_LEFT
+        )
+        assertEquals(0f, targetView.translationX)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_DOWN
+        )
+        assertEquals(0f, targetView.translationY)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_UP
+        )
+        assertEquals(0f, targetView.translationY)
+    }
+
+    /*
+    Test Case: Rectangles Overlapping at Corners
+
+       +------------+         <-- exclusionBounds
+       |            |
+    +-------+       |
+    |  |    |       |          <-- targetViewBounds
+    |  +------------+
+    |       |
+    +-------+
+    */
+    private fun leftDownCornerOverlap() {
+        val targetView = View(ContextWrapper(getApplicationContext()))
+        val targetViewBounds = Rect(20, 20, 30, 30)
+
+        val inclusionBounds = Rect(0, 0, 100, 100)
+        val exclusionBounds = Rect(25, 25, 35, 35)
+
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_RIGHT
+        )
+        assertEquals(15f, targetView.translationX)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_LEFT
+        )
+        assertEquals(-5f, targetView.translationX)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_DOWN
+        )
+        assertEquals(15f, targetView.translationY)
+        Utilities.translateOverlappingView(
+            targetView,
+            targetViewBounds,
+            inclusionBounds,
+            exclusionBounds,
+            Utilities.TRANSLATE_UP
+        )
+        assertEquals(-5f, targetView.translationY)
+    }
+
+    @Test
+    fun trim() {
+        val expectedString = "Hello World"
+        assertEquals(expectedString, Utilities.trim("Hello World   "))
+        // Basic trimming
+        assertEquals(expectedString, Utilities.trim("   Hello World  "))
+        assertEquals(expectedString, Utilities.trim("   Hello World"))
+
+        // Non-breaking whitespace
+        assertEquals("Hello World", Utilities.trim("\u00A0\u00A0Hello World\u00A0\u00A0"))
+
+        // Whitespace combinations
+        assertEquals(expectedString, Utilities.trim("\t \r\n Hello World \n\r"))
+        assertEquals(expectedString, Utilities.trim("\nHello World   "))
+
+        // Null input
+        assertEquals("", Utilities.trim(null))
+
+        // Empty String
+        assertEquals("", Utilities.trim(""))
+    }
+
+    @Test
+    fun getProgress() {
+        // Basic test
+        assertEquals(0.5f, Utilities.getProgress(50f, 0f, 100f), 0.001f)
+
+        // Negative values
+        assertEquals(0.5f, Utilities.getProgress(-20f, -50f, 10f), 0.001f)
+
+        // Outside of range
+        assertEquals(1.2f, Utilities.getProgress(120f, 0f, 100f), 0.001f)
+    }
+
+    @Test
+    fun scaleRectFAboutPivot() {
+        // Enlarge
+        var rectF = RectF(10f, 20f, 50f, 80f)
+        Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 1.5f)
+        assertEquals(RectF(0f, 5f, 60f, 95f), rectF)
+
+        // Shrink
+        rectF = RectF(10f, 20f, 50f, 80f)
+        Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 0.5f)
+        assertEquals(RectF(20f, 35f, 40f, 65f), rectF)
+
+        // No scale
+        rectF = RectF(10f, 20f, 50f, 80f)
+        Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 1.0f)
+        assertEquals(RectF(10f, 20f, 50f, 80f), rectF)
+    }
+
+    @Test
+    fun rotateBounds() {
+        var rect = Rect(20, 70, 60, 80)
+        Utilities.rotateBounds(rect, 100, 100, 0)
+        assertEquals(Rect(20, 70, 60, 80), rect)
+
+        rect = Rect(20, 70, 60, 80)
+        Utilities.rotateBounds(rect, 100, 100, 1)
+        assertEquals(Rect(70, 40, 80, 80), rect)
+
+        // case removed for b/28435189
+        //        rect = Rect(20, 70, 60, 80)
+        //        Utilities.rotateBounds(rect, 100, 100, 2)
+        //        assertEquals(Rect(40, 20, 80, 30), rect)
+
+        rect = Rect(20, 70, 60, 80)
+        Utilities.rotateBounds(rect, 100, 100, 3)
+        assertEquals(Rect(20, 20, 30, 60), rect)
+    }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.kt b/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.kt
new file mode 100644
index 0000000..e03ee46
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.accessibility
+
+import android.content.Context
+import android.view.ViewGroup
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.CellLayout
+import com.android.launcher3.DropTarget.DragObject
+import com.android.launcher3.dragndrop.DragOptions
+import com.android.launcher3.util.ActivityContextWrapper
+import java.util.function.Function
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.any
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AccessibleDragListenerAdapterTest {
+
+    private lateinit var mockViewGroup: ViewGroup
+    private lateinit var mockChildOne: CellLayout
+    private lateinit var mockChildTwo: CellLayout
+    private lateinit var mDelegateFactory: Function<CellLayout, DragAndDropAccessibilityDelegate>
+    private lateinit var adapter: AccessibleDragListenerAdapter
+    private lateinit var mContext: Context
+
+    @Before
+    fun setup() {
+        mContext = ActivityContextWrapper(getApplicationContext())
+        mockViewGroup = mock(ViewGroup::class.java)
+        mockChildOne = mock(CellLayout::class.java)
+        mockChildTwo = mock(CellLayout::class.java)
+        `when`(mockViewGroup.context).thenReturn(mContext)
+        `when`(mockViewGroup.childCount).thenReturn(2)
+        `when`(mockViewGroup.getChildAt(0)).thenReturn(mockChildOne)
+        `when`(mockViewGroup.getChildAt(1)).thenReturn(mockChildTwo)
+        // Mock Delegate factory
+        mDelegateFactory =
+            mock(Function::class.java) as Function<CellLayout, DragAndDropAccessibilityDelegate>
+        `when`(mDelegateFactory.apply(any()))
+            .thenReturn(mock(DragAndDropAccessibilityDelegate::class.java))
+        adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+    }
+
+    @Test
+    fun `onDragStart enables accessible drag for all view children`() {
+        // Create mock view children
+        val mockDragObject = mock(DragObject::class.java)
+        val mockDragOptions = mock(DragOptions::class.java)
+
+        // Action
+        adapter.onDragStart(mockDragObject, mockDragOptions)
+
+        // Assertion
+        verify(mockChildOne).setDragAndDropAccessibilityDelegate(any())
+        verify(mockChildTwo).setDragAndDropAccessibilityDelegate(any())
+    }
+
+    @Test
+    fun `onDragEnd removes the accessibility delegate`() {
+        // Action
+        adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+        adapter.onDragEnd()
+
+        // Assertion
+        verify(mockChildOne).setDragAndDropAccessibilityDelegate(null)
+        verify(mockChildTwo).setDragAndDropAccessibilityDelegate(null)
+    }
+
+    @Test
+    fun `onChildViewAdded sets enabled as true for childview`() {
+        // Action
+        adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+        adapter.onChildViewAdded(mockViewGroup, mockChildOne)
+
+        // Assertion
+        verify(mockChildOne).setDragAndDropAccessibilityDelegate(any())
+    }
+
+    @Test
+    fun `onChildViewRemoved sets enabled as false for childview`() {
+        // Action
+        adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+        adapter.onChildViewRemoved(mockViewGroup, mockChildOne)
+
+        // Assertion
+        verify(mockChildOne).setDragAndDropAccessibilityDelegate(null)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/accessibility/FolderAccessibilityHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/accessibility/FolderAccessibilityHelperTest.kt
new file mode 100644
index 0000000..1cbe1df
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/accessibility/FolderAccessibilityHelperTest.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.accessibility // Use the original package
+
+// Imports
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.CellLayout
+import com.android.launcher3.folder.FolderPagedView
+import com.android.launcher3.util.ActivityContextWrapper
+import kotlin.math.min
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderAccessibilityHelperTest {
+
+    // Context
+    private lateinit var mContext: Context
+    // Mocks
+    @Mock private lateinit var mockParent: FolderPagedView
+    @Mock private lateinit var mockLayout: CellLayout
+
+    private var countX = 4
+    private var countY = 3
+    private var index = 1
+
+    // System under test
+    private lateinit var folderAccessibilityHelper: FolderAccessibilityHelper
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        mContext = ActivityContextWrapper(getApplicationContext())
+        `when`(mockLayout.parent).thenReturn(mockParent)
+        `when`(mockLayout.context).thenReturn(mContext)
+
+        // mStartPosition isn't recalculated after the constructor
+        // If you want to create new tests with different starting params,
+        // rebuild the folderAccessibilityHelper object
+        val countX = 4
+        val countY = 3
+        val index = 1
+        `when`(mockParent.indexOfChild(mockLayout)).thenReturn(index)
+        `when`(mockLayout.countX).thenReturn(countX)
+        `when`(mockLayout.countY).thenReturn(countY)
+
+        folderAccessibilityHelper = FolderAccessibilityHelper(mockLayout)
+    }
+
+    // Test for intersectsValidDropTarget()
+    @Test
+    fun testIntersectsValidDropTarget() {
+        // Setup
+        val id = 5
+        val allocatedContentSize = 20
+        // Make layout function public @VisibleForTesting
+        `when`(mockParent.allocatedContentSize).thenReturn(allocatedContentSize)
+
+        // Execute
+        val result = folderAccessibilityHelper.intersectsValidDropTarget(id)
+
+        // Verify
+        val expectedResult = min(id, allocatedContentSize - (index * countX * countY) - 1)
+        assertEquals(expectedResult, result)
+    }
+
+    // Test for getLocationDescriptionForIconDrop()
+    @Test
+    fun testGetLocationDescriptionForIconDrop() {
+        // Setup
+        val id = 5
+
+        // Execute
+        val result = folderAccessibilityHelper.getLocationDescriptionForIconDrop(id)
+
+        // Verify
+        val expectedResult = "Move to position ${id + (index * countX * countY) + 1}"
+        assertEquals(expectedResult, result)
+    }
+
+    // Test for getConfirmationForIconDrop()
+    @Test
+    fun testGetConfirmationForIconDrop() {
+        // Execute
+        val result =
+            folderAccessibilityHelper.getConfirmationForIconDrop(0) // Id doesn't matter here
+
+        // Verify
+        assertEquals("Item moved", result)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/allapps/FloatingHeaderViewTests.kt b/tests/multivalentTests/src/com/android/launcher3/allapps/FloatingHeaderViewTests.kt
new file mode 100644
index 0000000..ac2c553
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/allapps/FloatingHeaderViewTests.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.allapps
+
+import android.content.Context
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.Flags
+import com.android.launcher3.util.ActivityContextWrapper
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class FloatingHeaderViewTests {
+
+    @get:Rule val mSetFlagsRule = SetFlagsRule()
+
+    private lateinit var context: Context
+    private lateinit var vut: FloatingHeaderView
+
+    @Before
+    fun setUp() {
+        context = ActivityContextWrapper(getApplicationContext())
+        // TODO(b/352161553): Inflate FloatingHeaderView or R.layout.all_apps_content with proper
+        // FloatingHeaderView#setup
+        vut = FloatingHeaderView(context)
+        vut.onFinishInflate()
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_FLOATING_SEARCH_BAR, Flags.FLAG_MULTILINE_SEARCH_BAR)
+    fun onHeightUpdated_whenNotMultiline_thenZeroHeight() {
+        vut.setFloatingRowsCollapsed(true)
+        val beforeHeight = vut.maxTranslation
+        vut.updateSearchBarOffset(HEADER_HEIGHT_OFFSET)
+
+        vut.onHeightUpdated()
+
+        assertThat(vut.maxTranslation).isEqualTo(beforeHeight)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MULTILINE_SEARCH_BAR)
+    @DisableFlags(Flags.FLAG_FLOATING_SEARCH_BAR)
+    fun onHeightUpdated_whenMultiline_thenHeightIsOffset() {
+        vut.setFloatingRowsCollapsed(true)
+        vut.updateSearchBarOffset(HEADER_HEIGHT_OFFSET)
+
+        vut.onHeightUpdated()
+
+        assertThat(vut.maxTranslation).isEqualTo(HEADER_HEIGHT_OFFSET)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MULTILINE_SEARCH_BAR)
+    @EnableFlags(Flags.FLAG_FLOATING_SEARCH_BAR)
+    fun onHeightUpdated_whenFloatingRowsShownAndNotMultiline_thenAddsOnlyFloatingRow() {
+        // Collapse floating rows and expand to trigger header height calculation
+        vut.setFloatingRowsCollapsed(true)
+        vut.setFloatingRowsCollapsed(false)
+        val defaultHeight = vut.maxTranslation
+        vut.updateSearchBarOffset(HEADER_HEIGHT_OFFSET)
+
+        vut.onHeightUpdated()
+
+        assertThat(vut.maxTranslation).isEqualTo(defaultHeight)
+    }
+
+    companion object {
+        private const val HEADER_HEIGHT_OFFSET = 50
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutMethodsTest.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutMethodsTest.kt
new file mode 100644
index 0000000..5bc57b0
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutMethodsTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.celllayout
+
+import org.junit.Rule
+import org.junit.Test
+
+// @RunWith(AndroidJUnit4::class) b/353965234
+class CellLayoutMethodsTest {
+
+    @JvmField @Rule var cellLayoutBuilder = UnitTestCellLayoutBuilderRule()
+
+    //@Test
+    fun pointToCellExact() {
+        val width = 1000
+        val height = 1000
+        val columns = 30
+        val rows = 30
+        val cl = cellLayoutBuilder.createCellLayout(columns, rows, false, width, height)
+
+        val res = intArrayOf(0, 0)
+        for (col in 0..<columns) {
+            for (row in 0..<rows) {
+                val x = (width / columns) * col
+                val y = (height / rows) * row
+                cl.pointToCellExact(x, y, res)
+                cl.pointToCellExact(x, y, res)
+                assertValues(col, res, row, columns, rows, width, height, x, y)
+            }
+        }
+
+        cl.pointToCellExact(-10, -10, res)
+        assertValues(0, res, 0, columns, rows, width, height, -10, -10)
+        cl.pointToCellExact(width + 10, height + 10, res)
+        assertValues(columns - 1, res, rows - 1, columns, rows, width, height, -10, -10)
+    }
+
+    private fun assertValues(
+        col: Int,
+        res: IntArray,
+        row: Int,
+        columns: Int,
+        rows: Int,
+        width: Int,
+        height: Int,
+        x: Int,
+        y: Int
+    ) {
+        assert(col == res[0] && row == res[1]) {
+            "Cell Layout with values (c= $columns, r= $rows, w= $width, h= $height) didn't mapped correctly the pixels ($x, $y) to the cells ($col, $row) with result (${res[0]}, ${res[1]})"
+        }
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
index 8a9711d..30953cc 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
@@ -144,8 +144,8 @@
 
     public ItemConfiguration solve(CellLayoutBoard board, int x, int y, int spanX,
             int spanY, int minSpanX, int minSpanY, boolean isMulti) {
-        CellLayout cl = mCellLayoutBuilder.createCellLayout(board.getWidth(), board.getHeight(),
-                isMulti);
+        CellLayout cl = mCellLayoutBuilder.createCellLayoutDefaultSize(board.getWidth(),
+                board.getHeight(), isMulti);
 
         // The views have to be sorted or the result can vary
         board.getIcons()
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt
index b63966d..f624be1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt
@@ -64,11 +64,21 @@
         dp.inv.numRows = prevNumRows
     }
 
-    fun createCellLayout(width: Int, height: Int, isMulti: Boolean): CellLayout {
+    fun createCellLayoutDefaultSize(columns: Int, rows: Int, isMulti: Boolean): CellLayout {
+        return createCellLayout(columns, rows, isMulti)
+    }
+
+    fun createCellLayout(
+        columns: Int,
+        rows: Int,
+        isMulti: Boolean,
+        width: Int = 1000,
+        height: Int = 1000
+    ): CellLayout {
         val dp = getDeviceProfile()
         // modify the device profile.
-        dp.inv.numColumns = if (isMulti) width / 2 else width
-        dp.inv.numRows = height
+        dp.inv.numColumns = if (isMulti) columns / 2 else columns
+        dp.inv.numRows = rows
         dp.cellLayoutBorderSpacePx = Point(0, 0)
         val cl =
             if (isMulti) MultipageCellLayout(getWrappedContext(applicationContext, dp))
@@ -76,8 +86,8 @@
         // I put a very large number for width and height so that all the items can fit, it doesn't
         // need to be exact, just bigger than the sum of cell border
         cl.measure(
-            View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY),
-            View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY)
+            View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
+            View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
         )
         return cl
     }
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
new file mode 100644
index 0000000..71f7d47
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model
+
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
+import android.content.Context
+import android.os.UserHandle
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.LimitDevicesRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.data.PackageItemInfo
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.IntSet
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.android.launcher3.widget.WidgetSections
+import com.android.launcher3.widget.WidgetSections.NO_CATEGORY
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.whenever
+
+@AllowedDevices(allowed = [DeviceProduct.ROBOLECTRIC])
+@RunWith(AndroidJUnit4::class)
+class WidgetsModelTest {
+    @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+    @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var appWidgetManager: AppWidgetManager
+    @Mock private lateinit var app: LauncherAppState
+    @Mock private lateinit var iconCacheMock: IconCache
+
+    private lateinit var context: Context
+    private lateinit var idp: InvariantDeviceProfile
+    private lateinit var underTest: WidgetsModel
+
+    private var widgetSectionCategory: Int = 0
+    private lateinit var appAPackage: String
+
+    @Before
+    fun setUp() {
+        val appContext: Context = ApplicationProvider.getApplicationContext()
+        idp = InvariantDeviceProfile.INSTANCE[appContext]
+
+        context =
+            object : ActivityContextWrapper(ApplicationProvider.getApplicationContext()) {
+                override fun getSystemService(name: String): Any? {
+                    if (name == "appwidget") {
+                        return appWidgetManager
+                    }
+                    return super.getSystemService(name)
+                }
+
+                override fun getDeviceProfile(): DeviceProfile {
+                    return idp.getDeviceProfile(applicationContext).copy(applicationContext)
+                }
+            }
+
+        whenever(iconCacheMock.getTitleNoCache(any<LauncherAppWidgetProviderInfo>()))
+            .thenReturn("title")
+        whenever(app.iconCache).thenReturn(iconCacheMock)
+        whenever(app.context).thenReturn(context)
+        whenever(app.invariantDeviceProfile).thenReturn(idp)
+
+        val widgetToCategoryEntry: Map.Entry<ComponentName, IntSet> =
+            WidgetSections.getWidgetsToCategory(context).entries.first()
+        widgetSectionCategory = widgetToCategoryEntry.value.first()
+        val appAWidgetComponent = widgetToCategoryEntry.key
+        appAPackage = appAWidgetComponent.packageName
+
+        whenever(appWidgetManager.getInstalledProvidersForProfile(any()))
+            .thenReturn(
+                listOf(
+                    // First widget from widget sections xml
+                    createAppWidgetProviderInfo(appAWidgetComponent),
+                    // A widget that belongs to same package as the widget from widget sections
+                    // xml, but, because it's not mentioned in xml, it would be included in its
+                    // own package section.
+                    createAppWidgetProviderInfo(
+                        ComponentName.createRelative(appAPackage, APP_A_TEST_WIDGET_NAME)
+                    ),
+                    // A widget in different package (none of that app's widgets are in widget
+                    // sections xml)
+                    createAppWidgetProviderInfo(AppBTestWidgetComponent),
+                )
+            )
+
+        val userCache = spy(UserCache.INSTANCE.get(context))
+        whenever(userCache.userProfiles).thenReturn(listOf(UserHandle.CURRENT))
+
+        underTest = WidgetsModel()
+    }
+
+    @Test
+    fun widgetsByPackage_treatsWidgetSectionsAsSeparatePackageItems() {
+        loadWidgets()
+
+        val packages: Map<PackageItemInfo, List<WidgetItem>> = underTest.widgetsByPackageItem
+
+        // expect 3 package items
+        // one for the custom section with widget from appA
+        // one for package section for second widget from appA (that wasn't listed in xml)
+        // and one for package section for appB
+        assertThat(packages).hasSize(3)
+
+        // Each package item when used as a key is distinct (i.e. even if appA is split into custom
+        // package and owner package section, each of them is a distinct key). This ensures that
+        // clicking on a custom widget section doesn't take user to app package section.
+        val distinctPackageUserKeys =
+            packages.map { PackageUserKey.fromPackageItemInfo(it.key) }.distinct()
+        assertThat(distinctPackageUserKeys).hasSize(3)
+
+        val customSections = packages.filter { it.key.widgetCategory == widgetSectionCategory }
+        assertThat(customSections).hasSize(1)
+        val widgetsInCustomSection = customSections.entries.first().value
+        assertThat(widgetsInCustomSection).hasSize(1)
+
+        val packageSections = packages.filter { it.key.widgetCategory == NO_CATEGORY }
+        assertThat(packageSections).hasSize(2)
+
+        // App A's package section
+        val appAPackageSection = packageSections.filter { it.key.packageName == appAPackage }
+        assertThat(appAPackageSection).hasSize(1)
+        val widgetsInAppASection = appAPackageSection.entries.first().value
+        assertThat(widgetsInAppASection).hasSize(1)
+
+        // App B's package section
+        val appBPackageSection =
+            packageSections.filter { it.key.packageName == AppBTestWidgetComponent.packageName }
+        assertThat(appBPackageSection).hasSize(1)
+        val widgetsInAppBSection = appBPackageSection.entries.first().value
+        assertThat(widgetsInAppBSection).hasSize(1)
+    }
+
+    @Test
+    fun widgetComponentMap_returnsWidgets() {
+        loadWidgets()
+
+        val widgetsByComponentKey: Map<ComponentKey, WidgetItem> = underTest.widgetsByComponentKey
+
+        assertThat(widgetsByComponentKey).hasSize(3)
+        widgetsByComponentKey.forEach { entry ->
+            assertThat(entry.key).isEqualTo(entry.value as ComponentKey)
+        }
+    }
+
+    @Test
+    fun widgets_noData_returnsEmpty() {
+        // no loadWidgets()
+
+        assertThat(underTest.widgetsByComponentKey).isEmpty()
+    }
+
+    private fun loadWidgets() {
+        val latch = CountDownLatch(1)
+        Executors.MODEL_EXECUTOR.execute {
+            underTest.update(app, /* packageUser= */ null)
+            latch.countDown()
+        }
+        if (!latch.await(LOAD_WIDGETS_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+            fail("Timed out waiting widgets to load")
+        }
+    }
+
+    companion object {
+        // Another widget within app A
+        private const val APP_A_TEST_WIDGET_NAME = "MyProvider"
+
+        private val AppBTestWidgetComponent: ComponentName =
+            ComponentName.createRelative("com.test.package", "TestProvider")
+
+        private const val LOAD_WIDGETS_TIMEOUT_SECONDS = 2L
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
new file mode 100644
index 0000000..3dd8dbc
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.pm
+
+import android.content.pm.ApplicationInfo
+import android.content.pm.ApplicationInfo.FLAG_INSTALLED
+import android.content.pm.LauncherApps
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.PROMISE_ICON_IDS
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.IntArray
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.TestUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InstallSessionHelperTest {
+
+    private val launcherModelHelper = LauncherModelHelper()
+    private val sandboxContext = spy(launcherModelHelper.sandboxContext)
+    private val packageManager = sandboxContext.packageManager
+    private val expectedAppPackage = "expectedAppPackage"
+    private val expectedInstallerPackage = "expectedInstallerPackage"
+    private val mockPackageInstaller: PackageInstaller = mock()
+
+    private lateinit var installSessionHelper: InstallSessionHelper
+    private lateinit var launcherApps: LauncherApps
+
+    @Before
+    fun setup() {
+        whenever(packageManager.packageInstaller).thenReturn(mockPackageInstaller)
+        whenever(sandboxContext.packageName).thenReturn(expectedInstallerPackage)
+        launcherApps = sandboxContext.spyService(LauncherApps::class.java)
+        installSessionHelper = InstallSessionHelper(sandboxContext)
+    }
+
+    @Test
+    fun `getActiveSessions fetches verified install sessions from LauncherApps`() {
+        // Given
+        val expectedVerifiedSession1 =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 0
+                installerPackageName = expectedInstallerPackage
+                appPackageName = expectedAppPackage
+                userId = 0
+            }
+        val expectedVerifiedSession2 =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "app2"
+                userId = 0
+            }
+        val expectedSessions = listOf(expectedVerifiedSession1, expectedVerifiedSession2)
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(expectedSessions)
+        // When
+        val actualSessions = installSessionHelper.getActiveSessions()
+        // Then
+        assertThat(actualSessions.values.toList()).isEqualTo(expectedSessions)
+    }
+
+    @Test
+    fun `getActiveSessionInfo fetches verified install sessions for given user and pkg`() {
+        // Given
+        val expectedVerifiedSession =
+            PackageInstaller.SessionInfo().apply {
+                installerPackageName = expectedInstallerPackage
+                appPackageName = expectedAppPackage
+                userId = 0
+            }
+        whenever(launcherApps.allPackageInstallerSessions)
+            .thenReturn(listOf(expectedVerifiedSession))
+        // When
+        val actualSession =
+            installSessionHelper.getActiveSessionInfo(UserHandle(0), expectedAppPackage)
+        // Then
+        assertThat(actualSession).isEqualTo(expectedVerifiedSession)
+    }
+
+    @Test
+    fun `getVerifiedSessionInfo verifies and returns session for given id`() {
+        // Given
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = expectedAppPackage
+                userId = 0
+            }
+        whenever(mockPackageInstaller.getSessionInfo(1)).thenReturn(expectedSession)
+        // When
+        val actualSession = installSessionHelper.getVerifiedSessionInfo(1)
+        // Then
+        assertThat(actualSession).isEqualTo(expectedSession)
+    }
+
+    @Test
+    fun `isTrustedPackage returns true if LauncherApps finds ApplicationInfo`() {
+        // Given
+        val expectedApplicationInfo =
+            ApplicationInfo().apply {
+                flags = flags or FLAG_INSTALLED
+                enabled = true
+            }
+        doReturn(expectedApplicationInfo)
+            .whenever(launcherApps)
+            .getApplicationInfo(expectedAppPackage, ApplicationInfo.FLAG_SYSTEM, UserHandle(0))
+        // When
+        val actualResult = installSessionHelper.isTrustedPackage(expectedAppPackage, UserHandle(0))
+        // Then
+        assertThat(actualResult).isTrue()
+    }
+
+    @Test
+    fun `getAllVerifiedSessions verifies and returns all active install sessions`() {
+        // Given
+        val expectedVerifiedSession1 =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 0
+                installerPackageName = expectedInstallerPackage
+                appPackageName = expectedAppPackage
+                userId = 0
+            }
+        val expectedVerifiedSession2 =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "app2"
+                userId = 0
+            }
+        val expectedSessions = listOf(expectedVerifiedSession1, expectedVerifiedSession2)
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(expectedSessions)
+        // When
+        val actualSessions = installSessionHelper.allVerifiedSessions
+        // Then
+        assertThat(actualSessions).isEqualTo(expectedSessions)
+    }
+
+    @Test
+    fun `promiseIconAddedForId returns true if there is a promiseIcon with the session id`() {
+        // Given
+        val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+        LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "app2"
+                userId = 0
+            }
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+        // When
+        var actualResult = false
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            actualResult = installSessionHelper.promiseIconAddedForId(1)
+        }
+        // Then
+        assertThat(actualResult).isTrue()
+    }
+
+    @Test
+    fun `removePromiseIconId removes promiseIconId for given Session id`() {
+        // Given
+        val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+        LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "app2"
+                userId = 0
+            }
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+        // When
+        var actualResult = true
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            installSessionHelper.removePromiseIconId(1)
+            actualResult = installSessionHelper.promiseIconAddedForId(1)
+        }
+        // Then
+        assertThat(actualResult).isFalse()
+    }
+
+    @Test
+    fun `tryQueuePromiseAppIcon will update promise icon ids from eligible sessions`() {
+        // Given
+        val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+        LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "appPackage"
+                userId = 0
+                appIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8)
+                appLabel = "appLabel"
+                installReason = PackageManager.INSTALL_REASON_USER
+            }
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+        // When
+        var wasPromiseIconAdded = false
+        var actualPromiseIconIds = ""
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            installSessionHelper.removePromiseIconId(1)
+            installSessionHelper.tryQueuePromiseAppIcon(expectedSession)
+            wasPromiseIconAdded = installSessionHelper.promiseIconAddedForId(1)
+            actualPromiseIconIds = LauncherPrefs.get(sandboxContext).get(PROMISE_ICON_IDS)
+        }
+        // Then
+        assertThat(wasPromiseIconAdded).isTrue()
+        assertThat(actualPromiseIconIds).isEqualTo(expectedIdString)
+    }
+
+    @Test
+    fun `verifySessionInfo is true if can verify given SessionInfo`() {
+        // Given
+        val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+        LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "appPackage"
+                userId = 0
+                appIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8)
+                appLabel = "appLabel"
+                installReason = PackageManager.INSTALL_REASON_USER
+            }
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+        // When
+        var actualResult = false
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            actualResult = installSessionHelper.verifySessionInfo(expectedSession)
+        }
+        // Then
+        assertThat(actualResult).isTrue()
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
new file mode 100644
index 0000000..8204313
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.recyclerview
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.Executors
+import com.android.launcher3.views.ActivityContext
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AllAppsRecyclerViewPoolTest<T> where T : Context, T : ActivityContext {
+
+    private lateinit var underTest: AllAppsRecyclerViewPool<T>
+    private lateinit var adapter: RecyclerView.Adapter<*>
+
+    @Mock private lateinit var parent: ViewGroup
+    @Mock private lateinit var itemView: View
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest = spy(AllAppsRecyclerViewPool())
+        adapter =
+            object : RecyclerView.Adapter<ViewHolder>() {
+                override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
+                    object : ViewHolder(itemView) {}
+
+                override fun getItemCount() = 0
+
+                override fun onBindViewHolder(holder: ViewHolder, position: Int) {}
+            }
+        underTest.setMaxRecycledViews(VIEW_TYPE, 20)
+    }
+
+    @Test
+    fun preinflate_success() {
+        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 }
+
+        awaitTasksCompleted()
+        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(10)
+    }
+
+    @Test
+    fun preinflate_not_triggered() {
+        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 0 }
+
+        awaitTasksCompleted()
+        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+    }
+
+    @Test
+    fun preinflate_cancel_before_runOnMainThread() {
+        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 }
+        assertThat(underTest.mCancellableTask!!.canceled).isFalse()
+
+        underTest.clear()
+
+        awaitTasksCompleted()
+        verify(underTest, never()).putRecycledView(any(ViewHolder::class.java))
+        assertThat(underTest.mCancellableTask!!.canceled).isTrue()
+        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+    }
+
+    @Test
+    fun preinflate_cancel_after_run() {
+        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 }
+        assertThat(underTest.mCancellableTask!!.canceled).isFalse()
+        awaitTasksCompleted()
+
+        underTest.clear()
+
+        verify(underTest, times(10)).putRecycledView(any(ViewHolder::class.java))
+        assertThat(underTest.mCancellableTask!!.canceled).isTrue()
+        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+    }
+
+    private fun awaitTasksCompleted() {
+        Executors.VIEW_PREINFLATION_EXECUTOR.submit<Any> { null }.get()
+        Executors.MAIN_EXECUTOR.submit<Any> { null }.get()
+    }
+
+    companion object {
+        private const val VIEW_TYPE: Int = 4
+    }
+}
diff --git a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java b/tests/multivalentTests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
similarity index 95%
rename from tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
rename to tests/multivalentTests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
index 260f556..6cfa6ee 100644
--- a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
@@ -22,9 +22,9 @@
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
 
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyFloat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
@@ -41,6 +41,8 @@
 
 import com.android.launcher3.testcomponent.TouchEventGenerator;
 
+import com.google.errorprone.annotations.FormatMethod;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,7 +54,8 @@
 public class SingleAxisSwipeDetectorTest {
 
     private static final String TAG = SingleAxisSwipeDetectorTest.class.getSimpleName();
-    public static void L(String s, Object... parts) {
+    @FormatMethod
+    public static void logD(String s, Object... parts) {
         Log.d(TAG, (parts.length == 0) ? s : String.format(s, parts));
     }
 
@@ -82,7 +85,7 @@
         mTouchSlop = orgConfig.getScaledTouchSlop();
         doReturn(mTouchSlop).when(mMockConfig).getScaledTouchSlop();
 
-        L("mTouchSlop=", mTouchSlop);
+        logD("mTouchSlop= %s", mTouchSlop);
     }
 
     @Test
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java b/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java
index 191d284..4217d22 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.util;
 
+import android.R;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.view.ContextThemeWrapper;
@@ -39,11 +40,16 @@
     private final MyDragLayer mMyDragLayer;
 
     public ActivityContextWrapper(Context base) {
-        super(base, android.R.style.Theme_DeviceDefault);
+        this(base, R.style.Theme_DeviceDefault);
+    }
+
+    public ActivityContextWrapper(Context base, int theme) {
+        super(base, theme);
         mProfile = InvariantDeviceProfile.INSTANCE.get(base).getDeviceProfile(base).copy(base);
         mMyDragLayer = new MyDragLayer(this);
     }
 
+
     @Override
     public BaseDragLayer getDragLayer() {
         return mMyDragLayer;
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ItemInfoMatcherTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ItemInfoMatcherTest.kt
new file mode 100644
index 0000000..daba024
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ItemInfoMatcherTest.kt
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.content.ComponentName
+import android.content.Intent
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.ItemInfoMatcher.ofShortcutKeys
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ItemInfoMatcherTest {
+
+    @Test
+    fun `ofUser returns Predicate for ItemInfo containing given UserHandle`() {
+        val expectedItemInfo = ItemInfo().apply { user = UserHandle(11) }
+        val unexpectedItemInfo = ItemInfo().apply { user = UserHandle(0) }
+        val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+
+        val predicate = ItemInfoMatcher.ofUser(UserHandle(11))
+        val actualResults = itemInfoStream.filter(predicate).toList()
+
+        assertThat(actualResults).containsExactly(expectedItemInfo)
+    }
+
+    @Test
+    fun `ofComponents returns Predicate for ItemInfo containing target Component and UserHandle`() {
+        // Given
+        val expectedUserHandle = UserHandle(0)
+        val expectedComponentName = ComponentName("expectedPackage", "expectedClass")
+        val expectedItemInfo = spy(ItemInfo())
+        expectedItemInfo.user = expectedUserHandle
+        whenever(expectedItemInfo.targetComponent).thenReturn(expectedComponentName)
+
+        val unexpectedComponentName = ComponentName("unexpectedPackage", "unexpectedClass")
+        val unexpectedItemInfo1 = spy(ItemInfo())
+        unexpectedItemInfo1.user = expectedUserHandle
+        whenever(unexpectedItemInfo1.targetComponent).thenReturn(unexpectedComponentName)
+
+        val unexpectedItemInfo2 = spy(ItemInfo())
+        unexpectedItemInfo2.user = UserHandle(10)
+        whenever(unexpectedItemInfo2.targetComponent).thenReturn(expectedComponentName)
+
+        val itemInfoStream =
+            listOf(expectedItemInfo, unexpectedItemInfo1, unexpectedItemInfo2).stream()
+
+        // When
+        val predicate =
+            ItemInfoMatcher.ofComponents(hashSetOf(expectedComponentName), expectedUserHandle)
+        val actualResults = itemInfoStream.filter(predicate).toList()
+
+        // Then
+        assertThat(actualResults).containsExactly(expectedItemInfo)
+    }
+
+    @Test
+    fun `ofPackages returns Predicate for ItemInfo containing UserHandle and target package`() {
+        // Given
+        val expectedUserHandle = UserHandle(0)
+        val expectedPackage = "expectedPackage"
+        val expectedComponentName = ComponentName(expectedPackage, "expectedClass")
+        val expectedItemInfo = spy(ItemInfo())
+        expectedItemInfo.user = expectedUserHandle
+        whenever(expectedItemInfo.targetComponent).thenReturn(expectedComponentName)
+
+        val unexpectedPackage = "unexpectedPackage"
+        val unexpectedComponentName = ComponentName(unexpectedPackage, "unexpectedClass")
+        val unexpectedItemInfo1 = spy(ItemInfo())
+        unexpectedItemInfo1.user = expectedUserHandle
+        whenever(unexpectedItemInfo1.targetComponent).thenReturn(unexpectedComponentName)
+
+        val unexpectedItemInfo2 = spy(ItemInfo())
+        unexpectedItemInfo2.user = UserHandle(10)
+        whenever(unexpectedItemInfo2.targetComponent).thenReturn(expectedComponentName)
+
+        val itemInfoStream =
+            listOf(expectedItemInfo, unexpectedItemInfo1, unexpectedItemInfo2).stream()
+
+        // When
+        val predicate = ItemInfoMatcher.ofPackages(setOf(expectedPackage), expectedUserHandle)
+        val actualResults = itemInfoStream.filter(predicate).toList()
+
+        // Then
+        assertThat(actualResults).containsExactly(expectedItemInfo)
+    }
+
+    @Test
+    fun `ofShortcutKeys returns Predicate for Deep Shortcut Info containing given ShortcutKey`() {
+        // Given
+        val expectedItemInfo = spy(ItemInfo())
+        expectedItemInfo.itemType = ITEM_TYPE_DEEP_SHORTCUT
+        val expectedIntent =
+            Intent().apply {
+                putExtra("shortcut_id", "expectedShortcut")
+                `package` = "expectedPackage"
+            }
+        whenever(expectedItemInfo.intent).thenReturn(expectedIntent)
+
+        val unexpectedIntent =
+            Intent().apply {
+                putExtra("shortcut_id", "unexpectedShortcut")
+                `package` = "unexpectedPackage"
+            }
+        val unexpectedItemInfo = spy(ItemInfo())
+        unexpectedItemInfo.itemType = ITEM_TYPE_DEEP_SHORTCUT
+        whenever(unexpectedItemInfo.intent).thenReturn(unexpectedIntent)
+
+        val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+        val expectedShortcutKey = ShortcutKey.fromItemInfo(expectedItemInfo)
+
+        // When
+        val predicate = ItemInfoMatcher.ofShortcutKeys(setOf(expectedShortcutKey))
+        val actualResults = itemInfoStream.filter(predicate).toList()
+
+        // Then
+        assertThat(actualResults).containsExactly(expectedItemInfo)
+    }
+
+    @Test
+    fun `forFolderMatch returns Predicate to match against children within Folder ItemInfo`() {
+        // Given
+        val expectedItemInfo = spy(FolderInfo())
+        expectedItemInfo.itemType = ITEM_TYPE_FOLDER
+        val expectedIntent =
+            Intent().apply {
+                putExtra("shortcut_id", "expectedShortcut")
+                `package` = "expectedPackage"
+            }
+        val expectedChildInfo = spy(ItemInfo())
+        expectedChildInfo.itemType = ITEM_TYPE_DEEP_SHORTCUT
+        whenever(expectedChildInfo.intent).thenReturn(expectedIntent)
+        whenever(expectedItemInfo.getContents()).thenReturn(arrayListOf(expectedChildInfo))
+
+        val unexpectedItemInfo = spy(FolderInfo())
+        unexpectedItemInfo.itemType = ITEM_TYPE_FOLDER
+
+        val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+        val expectedShortcutKey = ShortcutKey.fromItemInfo(expectedChildInfo)
+
+        // When
+        val predicate = ItemInfoMatcher.forFolderMatch(ofShortcutKeys(setOf(expectedShortcutKey)))
+        val actualResults = itemInfoStream.filter(predicate).toList()
+
+        // Then
+        assertThat(actualResults).containsExactly(expectedItemInfo)
+    }
+
+    @Test
+    fun `ofItemIds returns Predicate to match ItemInfo that contains given ids`() {
+        // Given
+        val expectedItemInfo = spy(ItemInfo())
+        expectedItemInfo.id = 1
+
+        val unexpectedItemInfo = spy(ItemInfo())
+        unexpectedItemInfo.id = 2
+
+        val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+
+        // When
+        val expectedIds = IntSet().apply { add(1) }
+        val predicate = ItemInfoMatcher.ofItemIds(expectedIds)
+        val actualResults = itemInfoStream.filter(predicate).toList()
+
+        // Then
+        assertThat(actualResults).containsExactly(expectedItemInfo)
+    }
+
+    @Test
+    fun `ofItems returns Predicate matching against provided ItemInfo`() {
+        // Given
+        val expectedItemInfo = spy(ItemInfo())
+        expectedItemInfo.id = 1
+
+        val unexpectedItemInfo = spy(ItemInfo())
+        unexpectedItemInfo.id = 2
+
+        val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+
+        // When
+        val expectedItems = setOf(expectedItemInfo)
+        val predicate = ItemInfoMatcher.ofItems(expectedItems)
+        val actualResults = itemInfoStream.filter(predicate).toList()
+
+        // Then
+        assertThat(actualResults).containsExactly(expectedItemInfo)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/RunnableListTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/RunnableListTest.kt
new file mode 100644
index 0000000..94bd7c9
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/RunnableListTest.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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 androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RunnableListTest {
+
+    @Mock private lateinit var runnable1: Runnable
+    @Mock private lateinit var runnable2: Runnable
+
+    private val underTest = RunnableList()
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun not_destroyedByDefault() {
+        assertThat(underTest.isDestroyed).isFalse()
+    }
+
+    @Test
+    fun add_and_run() {
+        underTest.add(runnable1)
+        underTest.add(runnable2)
+
+        underTest.executeAllAndDestroy()
+
+        verify(runnable1).run()
+        verify(runnable2).run()
+        assertThat(underTest.isDestroyed).isTrue()
+    }
+
+    @Test
+    fun add_to_destroyed_runnableList_run_immediately() {
+        underTest.executeAllAndDestroy()
+
+        underTest.add(runnable1)
+
+        verify(runnable1).run()
+    }
+
+    @Test
+    fun second_executeAllAndDestroy_noOp() {
+        underTest.executeAllAndDestroy()
+        underTest.add(runnable1)
+        reset(runnable1)
+
+        underTest.executeAllAndDestroy()
+
+        verifyNoMoreInteractions(runnable1)
+    }
+
+    @Test
+    fun executeAllAndClear_run_not_destroy() {
+        underTest.add(runnable1)
+        underTest.add(runnable2)
+
+        underTest.executeAllAndClear()
+
+        verify(runnable1).run()
+        verify(runnable2).run()
+        assertThat(underTest.isDestroyed).isFalse()
+    }
+
+    @Test
+    fun executeAllAndClear_not_destroy() {
+        underTest.executeAllAndClear()
+        underTest.add(runnable1)
+        reset(runnable1)
+
+        underTest.executeAllAndClear()
+
+        verify(runnable1).run()
+    }
+
+    @Test
+    fun remove_and_run_not_executed() {
+        underTest.add(runnable1)
+        underTest.add(runnable2)
+
+        underTest.remove(runnable1)
+        underTest.executeAllAndClear()
+
+        verifyNoMoreInteractions(runnable1)
+        verify(runnable2).run()
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt
new file mode 100644
index 0000000..430aad2
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_SCREEN_OFF
+import android.content.Intent.ACTION_SCREEN_ON
+import android.content.Intent.ACTION_USER_PRESENT
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ScreenOnTrackerTest {
+
+    @Mock private lateinit var receiver: SimpleBroadcastReceiver
+    @Mock private lateinit var context: Context
+    @Mock private lateinit var listener: ScreenOnTracker.ScreenOnListener
+
+    private lateinit var underTest: ScreenOnTracker
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest = ScreenOnTracker(context, receiver)
+    }
+
+    @Test
+    fun test_default_state() {
+        verify(receiver).register(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT)
+        assertThat(underTest.isScreenOn).isTrue()
+    }
+
+    @Test
+    fun close_unregister_receiver() {
+        underTest.close()
+
+        verify(receiver).unregisterReceiverSafely(context)
+    }
+
+    @Test
+    fun add_listener_then_receive_screen_on_intent_notify_listener() {
+        underTest.addListener(listener)
+
+        underTest.onReceive(Intent(ACTION_SCREEN_ON))
+
+        verify(listener).onScreenOnChanged(true)
+        assertThat(underTest.isScreenOn).isTrue()
+    }
+
+    @Test
+    fun add_listener_then_receive_screen_off_intent_notify_listener() {
+        underTest.addListener(listener)
+
+        underTest.onReceive(Intent(ACTION_SCREEN_OFF))
+
+        verify(listener).onScreenOnChanged(false)
+        assertThat(underTest.isScreenOn).isFalse()
+    }
+
+    @Test
+    fun add_listener_then_receive_user_present_intent_notify_listener() {
+        underTest.addListener(listener)
+
+        underTest.onReceive(Intent(ACTION_USER_PRESENT))
+
+        verify(listener).onUserPresent()
+        assertThat(underTest.isScreenOn).isTrue()
+    }
+
+    @Test
+    fun remove_listener_then_receive_intent_noOp() {
+        underTest.addListener(listener)
+
+        underTest.removeListener(listener)
+
+        underTest.onReceive(Intent(ACTION_USER_PRESENT))
+        verifyNoMoreInteractions(listener)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt
new file mode 100644
index 0000000..c43e563
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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 androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Utilities
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShortcutUtilTest {
+
+    @Test
+    fun `supportsShortcuts returns true if the item is active and it is an app`() {
+        // A blank workspace item info should be active and should be ITEM_TYPE_APPLICATION
+        val itemInfo = WorkspaceItemInfo()
+        // Action
+        val result = ShortcutUtil.supportsShortcuts(itemInfo)
+        // Verify
+        assertEquals(true, result)
+    }
+
+    @Test
+    fun `supportsDeepShortcuts returns true if the app is active and an app and widgets are enabled`() {
+        // Setup
+        val itemInfo = WorkspaceItemInfo()
+        // Action
+        val result = ShortcutUtil.supportsDeepShortcuts(itemInfo)
+        // Verify
+        assertEquals(true, result)
+    }
+
+    @Test
+    fun `getShortcutIdIfPinnedShortcut returns null if the item is an app`() {
+        // Setup
+        val itemInfo = WorkspaceItemInfo()
+        // Action
+        val result = ShortcutUtil.getShortcutIdIfPinnedShortcut(itemInfo)
+        // Verify
+        assertNull(result)
+    }
+
+    @Test
+    fun `getPersonKeysIfPinnedShortcut returns empty string array if item type is an app`() {
+        // Setup
+        val itemInfo = WorkspaceItemInfo()
+        // Action
+        val result = ShortcutUtil.getPersonKeysIfPinnedShortcut(itemInfo)
+        // Verify
+        assertArrayEquals(Utilities.EMPTY_STRING_ARRAY, result)
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
similarity index 96%
rename from tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
index 1de99c5..d3e27b6 100644
--- a/tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
@@ -23,6 +23,7 @@
 import android.os.Looper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
 import com.google.common.truth.Truth.assertThat
 import java.util.function.Consumer
@@ -114,6 +115,7 @@
         underTest = SimpleBroadcastReceiver(Handler(Looper.getMainLooper()), intentConsumer)
 
         underTest.register(context, completionRunnable, 1, "test_action_1", "test_action_2")
+        getInstrumentation().waitForIdleSync()
 
         verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture(), eq(1))
         verify(completionRunnable).run()
@@ -136,6 +138,7 @@
         underTest = SimpleBroadcastReceiver(Handler(Looper.getMainLooper()), intentConsumer)
 
         underTest.unregisterReceiverSafely(context)
+        getInstrumentation().waitForIdleSync()
 
         verify(context).unregisterReceiver(same(underTest))
     }
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt
new file mode 100644
index 0000000..612fcd4
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.view.View
+import android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+import android.view.Window
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_STATUS
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS
+import com.android.launcher3.util.SystemUiController.UI_STATE_BASE_WINDOW
+import com.android.launcher3.util.SystemUiController.UI_STATE_SCRIM_VIEW
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SystemUiControllerTest {
+
+    @Mock private lateinit var window: Window
+    @Mock private lateinit var decorView: View
+
+    private lateinit var underTest: SystemUiController
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        `when`(window.decorView).thenReturn(decorView)
+        underTest = SystemUiController(window)
+    }
+
+    @Test
+    fun test_default_state() {
+        assertThat(underTest.toString()).isEqualTo("mStates=[0, 0, 0, 0, 0]")
+    }
+
+    @Test
+    fun update_state_base_window_light() {
+        underTest.updateUiState(UI_STATE_BASE_WINDOW, /* isLight= */ true)
+
+        val visibility =
+            View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+        verify(decorView).systemUiVisibility = eq(visibility)
+        assertThat(underTest.baseSysuiVisibility).isEqualTo(visibility)
+        val flag = FLAG_LIGHT_NAV or FLAG_LIGHT_STATUS
+        assertThat(underTest.toString()).isEqualTo("mStates=[$flag, 0, 0, 0, 0]")
+    }
+
+    @Test
+    fun update_state_scrim_view_light() {
+        underTest.updateUiState(UI_STATE_SCRIM_VIEW, /* isLight= */ true)
+
+        verify(decorView).systemUiVisibility =
+            eq(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
+        assertThat(underTest.baseSysuiVisibility).isEqualTo(0)
+        val flag = FLAG_LIGHT_NAV or FLAG_LIGHT_STATUS
+        assertThat(underTest.toString()).isEqualTo("mStates=[0, $flag, 0, 0, 0]")
+    }
+
+    @Test
+    fun update_state_base_window_dark() {
+        underTest.updateUiState(UI_STATE_BASE_WINDOW, /* isLight= */ false)
+
+        verify(decorView, never()).systemUiVisibility = anyInt()
+        assertThat(underTest.baseSysuiVisibility).isEqualTo(0)
+        val flag = FLAG_DARK_NAV or FLAG_DARK_STATUS
+        assertThat(underTest.toString()).isEqualTo("mStates=[$flag, 0, 0, 0, 0]")
+    }
+
+    @Test
+    fun update_state_scrim_view_dark() {
+        underTest.updateUiState(UI_STATE_SCRIM_VIEW, /* isLight= */ false)
+
+        verify(decorView, never()).systemUiVisibility = anyInt()
+        assertThat(underTest.baseSysuiVisibility).isEqualTo(0)
+        val flag = FLAG_DARK_NAV or FLAG_DARK_STATUS
+        assertThat(underTest.toString()).isEqualTo("mStates=[0, $flag, 0, 0, 0]")
+    }
+
+    @Test
+    fun get_base_sysui_visibility() {
+        `when`(decorView.systemUiVisibility).thenReturn(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+
+        assertThat(underTest.baseSysuiVisibility).isEqualTo(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+    }
+
+    @Test
+    fun update_state_base_window_light_with_existing_visibility() {
+        `when`(decorView.systemUiVisibility).thenReturn(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+
+        underTest.updateUiState(UI_STATE_BASE_WINDOW, /* isLight= */ true)
+
+        val visibility =
+            View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or
+                View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or
+                SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+        assertThat(underTest.baseSysuiVisibility).isEqualTo(visibility)
+        verify(decorView).systemUiVisibility = eq(visibility)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java b/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java
index 3f37563..71637f1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java
@@ -32,6 +32,7 @@
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
 
 import java.util.Map;
 import java.util.concurrent.CountDownLatch;
@@ -57,6 +58,8 @@
     protected ActivityAllAppsContainerView<ActivityContextWrapper> mAppsView;
 
     private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
+    private final WidgetPickerDataProvider mWidgetPickerDataProvider =
+            new WidgetPickerDataProvider();
     protected final UserCache mUserCache;
 
     public TestSandboxModelContextWrapper(SandboxContext base) {
@@ -76,12 +79,19 @@
         mAppsList = mAppsView.getPersonalAppList();
         mAllAppsStore = mAppsView.getAppsStore();
     }
+
     @Nullable
     @Override
     public PopupDataProvider getPopupDataProvider() {
         return mPopupDataProvider;
     }
 
+    @Nullable
+    @Override
+    public WidgetPickerDataProvider getWidgetPickerDataProvider() {
+        return mWidgetPickerDataProvider;
+    }
+
     @Override
     public ActivityAllAppsContainerView<ActivityContextWrapper> getAppsView() {
         return mAppsView;
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt
new file mode 100644
index 0000000..330c394
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.media.AudioAttributes
+import android.os.SystemClock
+import android.os.VibrationEffect
+import android.os.VibrationEffect.Composition.PRIMITIVE_LOW_TICK
+import android.os.VibrationEffect.Composition.PRIMITIVE_TICK
+import android.os.Vibrator
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.VibratorWrapper.HAPTIC_FEEDBACK_URI
+import com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC
+import com.android.launcher3.util.VibratorWrapper.VIBRATION_ATTRS
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.never
+import org.mockito.kotlin.same
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class VibratorWrapperTest {
+
+    @Mock private lateinit var settingsCache: SettingsCache
+    @Mock private lateinit var vibrator: Vibrator
+    @Captor private lateinit var vibrationEffectCaptor: ArgumentCaptor<VibrationEffect>
+
+    private lateinit var underTest: VibratorWrapper
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        `when`(settingsCache.getValue(HAPTIC_FEEDBACK_URI, 0)).thenReturn(true)
+        `when`(vibrator.hasVibrator()).thenReturn(true)
+        `when`(vibrator.areAllPrimitivesSupported(PRIMITIVE_TICK)).thenReturn(true)
+        `when`(vibrator.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)).thenReturn(true)
+        `when`(vibrator.getPrimitiveDurations(PRIMITIVE_LOW_TICK)).thenReturn(intArrayOf(10))
+
+        underTest = VibratorWrapper(vibrator, settingsCache)
+    }
+
+    @Test
+    fun init_register_onChangeListener() {
+        verify(settingsCache).register(HAPTIC_FEEDBACK_URI, underTest.mHapticChangeListener)
+    }
+
+    @Test
+    fun close_unregister_onChangeListener() {
+        underTest.close()
+
+        verify(settingsCache).unregister(HAPTIC_FEEDBACK_URI, underTest.mHapticChangeListener)
+    }
+
+    @Test
+    fun vibrate() {
+        underTest.vibrate(OVERVIEW_HAPTIC)
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(OVERVIEW_HAPTIC, VIBRATION_ATTRS)
+    }
+
+    @Test
+    fun vibrate_primitive_id() {
+        underTest.vibrate(PRIMITIVE_TICK, 1f, OVERVIEW_HAPTIC)
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+        val expectedEffect =
+            VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, 1f).compose()
+        assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+    }
+
+    @Test
+    fun vibrate_with_invalid_primitive_id_use_fallback_effect() {
+        underTest.vibrate(-1, 1f, OVERVIEW_HAPTIC)
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(OVERVIEW_HAPTIC, VIBRATION_ATTRS)
+    }
+
+    @Test
+    fun vibrate_for_taskbar_unstash() {
+        underTest.vibrateForTaskbarUnstash()
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+        val expectedEffect =
+            VibrationEffect.startComposition()
+                .addPrimitive(PRIMITIVE_LOW_TICK, VibratorWrapper.LOW_TICK_SCALE)
+                .compose()
+        assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+    }
+
+    @Test
+    fun vibrate_for_drag_bump() {
+        underTest.vibrateForDragBump()
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+        val expectedEffect =
+            VibrationEffect.startComposition()
+                .addPrimitive(PRIMITIVE_LOW_TICK, VibratorWrapper.DRAG_BUMP_SCALE)
+                .compose()
+        assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+    }
+
+    @Test
+    fun vibrate_for_drag_commit() {
+        underTest.vibrateForDragCommit()
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+        val expectedEffect =
+            VibrationEffect.startComposition()
+                .addPrimitive(PRIMITIVE_TICK, VibratorWrapper.DRAG_COMMIT_SCALE)
+                .compose()
+        assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+    }
+
+    @Test
+    fun vibrate_for_drag_texture() {
+        SystemClock.setCurrentTimeMillis(40000)
+
+        underTest.vibrateForDragTexture()
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+        assertThat(vibrationEffectCaptor.value).isEqualTo(VibratorWrapper.getDragEffect())
+    }
+
+    @Test
+    fun vibrate_for_drag_texture_within_time_window_noOp() {
+        SystemClock.setCurrentTimeMillis(40000)
+        underTest.vibrateForDragTexture()
+        awaitTasksCompleted()
+        reset(vibrator)
+
+        underTest.vibrateForDragTexture()
+
+        verifyNoMoreInteractions(vibrator)
+    }
+
+    @Test
+    fun haptic_feedback_disabled_no_vibrate() {
+        `when`(vibrator.hasVibrator()).thenReturn(false)
+        underTest = VibratorWrapper(vibrator, settingsCache)
+
+        underTest.vibrate(OVERVIEW_HAPTIC)
+
+        awaitTasksCompleted()
+        verify(vibrator, never())
+            .vibrate(any(VibrationEffect::class.java), any(AudioAttributes::class.java))
+    }
+
+    @Test
+    fun cancel_vibrate() {
+        underTest.cancelVibrate()
+
+        awaitTasksCompleted()
+        verify(vibrator).cancel()
+    }
+
+    private fun awaitTasksCompleted() {
+        Executors.UI_HELPER_EXECUTOR.submit<Any> { null }.get()
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/ViewCacheTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ViewCacheTest.kt
similarity index 98%
rename from tests/src/com/android/launcher3/util/ViewCacheTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/util/ViewCacheTest.kt
index bad21c9..d82818d 100644
--- a/tests/src/com/android/launcher3/util/ViewCacheTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ViewCacheTest.kt
@@ -18,6 +18,7 @@
 
 import android.view.View
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.R
 import com.android.launcher3.util.ViewCache.CacheEntry
@@ -26,6 +27,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@SmallTest
 @RunWith(AndroidJUnit4::class)
 class ViewCacheTest {
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ViewPoolTest.kt
new file mode 100644
index 0000000..e535656
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ViewPoolTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.same
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit::class)
+@UiThreadTest
+class ViewPoolTest {
+
+    @Mock private lateinit var viewParent: ViewGroup
+    @Mock private lateinit var view: ReusableView
+    @Mock private lateinit var inflater: LayoutInflater
+
+    private lateinit var underTest: ViewPool<ReusableView>
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        `when`(inflater.cloneInContext(any())).thenReturn(inflater)
+        underTest = ViewPool(inflater, viewParent, LAYOUT_ID, 10, 0)
+    }
+
+    @Test
+    fun get_view_from_empty_pool_inflate_new_view() {
+        underTest.view
+
+        verify(inflater).inflate(eq(LAYOUT_ID), same(viewParent), eq(false))
+    }
+
+    @Test
+    fun recycle_view() {
+        underTest.recycle(view)
+
+        val returnedView = underTest.view
+
+        verify(view).onRecycle()
+        assertThat(returnedView).isSameInstanceAs(view)
+        verifyNoMoreInteractions(inflater)
+    }
+
+    @Test
+    fun get_view_twice_from_view_pool_with_one_view() {
+        underTest.recycle(view)
+        underTest.view
+        verifyNoMoreInteractions(inflater)
+
+        underTest.view
+
+        verify(inflater).inflate(eq(LAYOUT_ID), same(viewParent), eq(false))
+    }
+
+    companion object {
+        private const val LAYOUT_ID = 1000
+    }
+
+    private inner class ReusableView(context: Context) : View(context), ViewPool.Reusable {
+        override fun onRecycle() {
+            TODO("Not yet implemented")
+        }
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/ListenableHostViewTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/ListenableHostViewTest.kt
new file mode 100644
index 0000000..6c71f36
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/ListenableHostViewTest.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget
+
+import android.content.Context
+import android.view.accessibility.AccessibilityNodeInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.util.ActivityContextWrapper
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ListenableHostViewTest {
+
+    private val context: Context
+        get() = ActivityContextWrapper(InstrumentationRegistry.getInstrumentation().targetContext)
+
+    @Test
+    fun updateAppWidget_notifiesListeners() {
+        val hostView = ListenableHostView(context)
+        var wasNotifiedOfUpdate = false
+        val updateListener = Runnable { wasNotifiedOfUpdate = true }
+        hostView.addUpdateListener(updateListener)
+        hostView.beginDeferringUpdates()
+        hostView.updateAppWidget(null)
+        Truth.assertThat(wasNotifiedOfUpdate).isTrue()
+    }
+
+    @Test
+    fun onInitializeAccessibilityNodeInfo_correctlySetsClassName() {
+        val hostView = ListenableHostView(context)
+        val nodeInfo = AccessibilityNodeInfo()
+        hostView.onInitializeAccessibilityNodeInfo(nodeInfo)
+        Truth.assertThat(nodeInfo.className).isEqualTo(LauncherAppWidgetHostView::class.java.name)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
new file mode 100644
index 0000000..db77702
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.R
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RoundedCornerEnforcementTest {
+
+    @Test
+    fun `Widget view has one background`() {
+        val mockWidgetView = mock(LauncherAppWidgetHostView::class.java)
+
+        doReturn(android.R.id.background).whenever(mockWidgetView).id
+
+        assertSame(RoundedCornerEnforcement.findBackground(mockWidgetView), mockWidgetView)
+    }
+
+    @Test
+    fun `Widget opted out of rounded corner enforcement`() {
+        val mockView = mock(View::class.java)
+
+        doReturn(android.R.id.background).whenever(mockView).id
+        doReturn(true).whenever(mockView).clipToOutline
+
+        assertTrue(RoundedCornerEnforcement.hasAppWidgetOptedOut(mockView))
+    }
+
+    @Test
+    fun `Compute rect based on widget view with background`() {
+        val mockBackgroundView = mock(View::class.java)
+        val mockWidgetView = mock(ViewGroup::class.java)
+        val testRect = Rect(0, 0, 0, 0)
+
+        doReturn(WIDTH).whenever(mockBackgroundView).width
+        doReturn(HEIGHT).whenever(mockBackgroundView).height
+        doReturn(LEFT).whenever(mockBackgroundView).left
+        doReturn(TOP).whenever(mockBackgroundView).top
+        doReturn(mockWidgetView).whenever(mockBackgroundView).parent
+
+        RoundedCornerEnforcement.computeRoundedRectangle(
+            mockWidgetView,
+            mockBackgroundView,
+            testRect
+        )
+
+        assertEquals(Rect(50, 75, 250, 275), testRect)
+    }
+
+    @Test
+    fun `Compute system radius`() {
+        val mockContext = mock(Context::class.java)
+        val mockRes = mock(Resources::class.java)
+
+        doReturn(mockRes).whenever(mockContext).resources
+        doReturn(RADIUS)
+            .whenever(mockRes)
+            .getDimension(eq(android.R.dimen.system_app_widget_background_radius))
+        doReturn(LAUNCHER_RADIUS)
+            .whenever(mockRes)
+            .getDimension(eq(R.dimen.enforced_rounded_corner_max_radius))
+
+        assertEquals(RADIUS, RoundedCornerEnforcement.computeEnforcedRadius(mockContext))
+    }
+
+    companion object {
+        const val WIDTH = 200
+        const val HEIGHT = 200
+        const val LEFT = 50
+        const val TOP = 75
+
+        const val RADIUS = 8f
+        const val LAUNCHER_RADIUS = 16f
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/WidgetAddFlowHandlerTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/WidgetAddFlowHandlerTest.kt
new file mode 100644
index 0000000..242ec7c
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/WidgetAddFlowHandlerTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget
+
+import android.content.Context
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.Launcher
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.util.ActivityContextWrapper
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WidgetAddFlowHandlerTest {
+
+    private val context: Context
+        get() = ActivityContextWrapper(InstrumentationRegistry.getInstrumentation().targetContext)
+
+    private val providerInfo =
+        LauncherAppWidgetProviderInfo().apply {
+            configure = InstrumentationRegistry.getInstrumentation().componentName
+        }
+    private val appWidgetHolder: LauncherWidgetHolder = mock<LauncherWidgetHolder>()
+    private val launcher: Launcher =
+        mock<Launcher>().also { whenever(it.appWidgetHolder).thenReturn(appWidgetHolder) }
+    private val appWidgetInfo = LauncherAppWidgetInfo().apply { appWidgetId = 123 }
+    private val requestCode = 123
+    private val flowHandler = WidgetAddFlowHandler(providerInfo)
+
+    @Test
+    fun valuesShouldRemainTheSame_beforeAndAfter_parcelization() {
+        with(Bundle()) {
+            val testKey = "testKey"
+            putParcelable(testKey, flowHandler)
+            Truth.assertThat(getParcelable(testKey, WidgetAddFlowHandler::class.java))
+                .isEqualTo(flowHandler)
+        }
+    }
+
+    @Test
+    fun describeContents_shouldReturn_0() {
+        Truth.assertThat(flowHandler.describeContents()).isEqualTo(0)
+    }
+
+    @Test
+    fun startBindFlow_shouldCorrectly_startLauncherFlowBinding() {
+        flowHandler.startBindFlow(launcher, appWidgetInfo.appWidgetId, appWidgetInfo, requestCode)
+        verify(launcher).setWaitingForResult(any())
+        verify(appWidgetHolder)
+            .startBindFlow(launcher, appWidgetInfo.appWidgetId, providerInfo, requestCode)
+    }
+
+    @Test
+    fun startConfigActivityWithCustomAppWidgetId_shouldAskLauncherToStartConfigActivity() {
+        flowHandler.startConfigActivity(
+            launcher,
+            appWidgetInfo.appWidgetId,
+            ItemInfo(),
+            requestCode
+        )
+        verify(launcher).setWaitingForResult(any())
+        verify(appWidgetHolder)
+            .startConfigActivity(launcher, appWidgetInfo.appWidgetId, requestCode)
+    }
+
+    @Test
+    fun startConfigActivity_shouldAskLauncherToStartConfigActivity() {
+        flowHandler.startConfigActivity(launcher, appWidgetInfo, requestCode)
+        verify(launcher).setWaitingForResult(any())
+        verify(appWidgetHolder)
+            .startConfigActivity(launcher, appWidgetInfo.appWidgetId, requestCode)
+    }
+
+    @Test
+    fun needsConfigure_returnsTrue_ifFlagsAndProviderInfoDetermineSo() {
+        Truth.assertThat(flowHandler.needsConfigure()).isTrue()
+    }
+
+    @Test
+    fun getProviderInfo_returnCorrectProviderInfo() {
+        Truth.assertThat(flowHandler.getProviderInfo(context)).isSameInstanceAs(providerInfo)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/WidgetManagerHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/WidgetManagerHelperTest.kt
new file mode 100644
index 0000000..f1cfb79
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/WidgetManagerHelperTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.os.Bundle
+import android.os.Process
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.PackageUserKey
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WidgetManagerHelperTest {
+
+    private val context: Context
+        get() = ActivityContextWrapper(InstrumentationRegistry.getInstrumentation().targetContext)
+
+    private val info =
+        LauncherAppWidgetProviderInfo().apply {
+            provider = InstrumentationRegistry.getInstrumentation().componentName
+            providerInfo =
+                mock(ActivityInfo::class.java).apply { applicationInfo = context.applicationInfo }
+        }
+
+    @Mock private lateinit var appWidgetManager: AppWidgetManager
+
+    private lateinit var underTest: WidgetManagerHelper
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest = WidgetManagerHelper(context, appWidgetManager)
+    }
+
+    @Test
+    fun getAllProviders_returnsCorrectWidgetProviderInfo() {
+        val packageUserKey =
+            mock(PackageUserKey::class.java).apply {
+                mPackageName = context.packageName
+                mUser = Process.myUserHandle()
+            }
+        val desiredResult = listOf(info)
+        whenever(
+                appWidgetManager.getInstalledProvidersForPackage(
+                    packageUserKey.mPackageName,
+                    packageUserKey.mUser
+                )
+            )
+            .thenReturn(desiredResult)
+        Truth.assertThat(underTest.getAllProviders(packageUserKey)).isSameInstanceAs(desiredResult)
+    }
+
+    @Test
+    fun getLauncherAppWidgetInfo_returnsCorrectInfo_ifWidgetExists() {
+        val id = 123
+        whenever(appWidgetManager.getAppWidgetInfo(id)).thenReturn(info)
+        val componentName = InstrumentationRegistry.getInstrumentation().componentName
+        Truth.assertThat(underTest.getLauncherAppWidgetInfo(id, componentName))
+            .isSameInstanceAs(info)
+    }
+
+    @Test
+    fun bindAppWidgetIdIfAllowed_correctly_forwardsBindCommandToAppWidgetManager() {
+        val id = 124
+        val options = Bundle()
+        underTest.bindAppWidgetIdIfAllowed(id, info, options)
+        verify(appWidgetManager).bindAppWidgetIdIfAllowed(id, info.profile, info.provider, options)
+    }
+
+    @Test
+    fun findProvider_returnsNull_ifNoProviderExists() {
+        val info =
+            underTest.getLauncherAppWidgetInfo(
+                1,
+                InstrumentationRegistry.getInstrumentation().componentName
+            )
+        Truth.assertThat(info).isNull()
+    }
+
+    @Test
+    fun isAppWidgetRestored_returnsTrue_ifWidgetIsRestored() {
+        val id = 126
+        whenever(appWidgetManager.getAppWidgetOptions(id))
+            .thenReturn(
+                Bundle().apply {
+                    putBoolean(WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED, true)
+                }
+            )
+        Truth.assertThat(underTest.isAppWidgetRestored(id)).isTrue()
+    }
+
+    @Test
+    fun loadGeneratedPreview_returnsWidgetPreview_fromAppWidgetManager() {
+        val widgetCategory = 130
+        with(info) {
+            underTest.loadGeneratedPreview(this, widgetCategory)
+            verify(appWidgetManager).getWidgetPreview(provider, profile, widgetCategory)
+        }
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfoTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfoTest.kt
new file mode 100644
index 0000000..0a3035a
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfoTest.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.custom
+
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class CustomAppWidgetProviderInfoTest {
+
+    private lateinit var underTest: CustomAppWidgetProviderInfo
+
+    @Before
+    fun setup() {
+        underTest = CustomAppWidgetProviderInfo()
+        underTest.provider = PROVIDER_NAME
+    }
+
+    @Test
+    fun info_to_string() {
+        assertEquals("WidgetProviderInfo($PROVIDER_NAME)", underTest.toString())
+    }
+
+    @Test
+    fun get_label() {
+        underTest.label = "  TEST_LABEL"
+        assertEquals(LABEL_NAME, underTest.getLabel(mock(PackageManager::class.java)))
+    }
+
+    companion object {
+        private val PROVIDER_NAME =
+            ComponentName(getInstrumentation().targetContext.packageName, "TEST_PACKAGE")
+        private const val LABEL_NAME = "TEST_LABEL"
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt
new file mode 100644
index 0000000..4b5710d
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.custom
+
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
+import android.os.Process
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import com.android.launcher3.util.PluginManagerWrapper
+import com.android.launcher3.util.WidgetUtils
+import com.android.launcher3.widget.LauncherAppWidgetHostView
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.android.systemui.plugins.CustomWidgetPlugin
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.same
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CustomWidgetManagerTest {
+
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
+    private val context = SandboxModelContext()
+    private lateinit var underTest: CustomWidgetManager
+
+    @Mock private lateinit var pluginManager: PluginManagerWrapper
+    @Mock private lateinit var mockAppWidgetManager: AppWidgetManager
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        context.putObject(PluginManagerWrapper.INSTANCE, pluginManager)
+        underTest = CustomWidgetManager(context, mockAppWidgetManager)
+    }
+
+    @After
+    fun tearDown() {
+        underTest.close()
+    }
+
+    @Test
+    fun plugin_manager_added_after_initialization() {
+        verify(pluginManager)
+            .addPluginListener(same(underTest), same(CustomWidgetPlugin::class.java), eq(true))
+    }
+
+    @Test
+    fun close_widget_manager_should_remove_plugin_listener() {
+        underTest.close()
+        verify(pluginManager).removePluginListener(same(underTest))
+    }
+
+    @Test
+    fun on_plugin_connected_no_provider_info() {
+        doReturn(emptyList<LauncherAppWidgetProviderInfo>())
+            .whenever(mockAppWidgetManager)
+            .getInstalledProvidersForProfile(any())
+        val mockPlugin = mock(CustomWidgetPlugin::class.java)
+        underTest.onPluginConnected(mockPlugin, context)
+        assertEquals(0, underTest.plugins.size)
+    }
+
+    @Test
+    fun on_plugin_connected_exist_provider_info() {
+        doReturn(listOf(WidgetUtils.createAppWidgetProviderInfo(TEST_COMPONENT_NAME)))
+            .whenever(mockAppWidgetManager)
+            .getInstalledProvidersForProfile(eq(Process.myUserHandle()))
+        val mockPlugin = mock(CustomWidgetPlugin::class.java)
+        underTest.onPluginConnected(mockPlugin, context)
+        assertEquals(1, underTest.plugins.size)
+    }
+
+    @Test
+    fun on_plugin_disconnected() {
+        doReturn(listOf(WidgetUtils.createAppWidgetProviderInfo(TEST_COMPONENT_NAME)))
+            .whenever(mockAppWidgetManager)
+            .getInstalledProvidersForProfile(eq(Process.myUserHandle()))
+        val mockPlugin = mock(CustomWidgetPlugin::class.java)
+        underTest.onPluginConnected(mockPlugin, context)
+        underTest.onPluginDisconnected(mockPlugin)
+        assertEquals(0, underTest.plugins.size)
+    }
+
+    @Test
+    fun on_view_created() {
+        val mockPlugin = mock(CustomWidgetPlugin::class.java)
+        val mockWidgetView = mock(LauncherAppWidgetHostView::class.java)
+        val mockProviderInfo = mock(CustomAppWidgetProviderInfo::class.java)
+        doReturn(mockProviderInfo).whenever(mockWidgetView).appWidgetInfo
+        mockProviderInfo.provider = TEST_COMPONENT_NAME
+        underTest.plugins.put(TEST_COMPONENT_NAME, mockPlugin)
+        underTest.onViewCreated(mockWidgetView)
+        verify(mockPlugin).onViewCreated(eq(mockWidgetView))
+    }
+
+    @Test
+    fun generate_stream() {
+        assertTrue(underTest.stream().toList().isEmpty())
+        doReturn(listOf(WidgetUtils.createAppWidgetProviderInfo(TEST_COMPONENT_NAME)))
+            .whenever(mockAppWidgetManager)
+            .getInstalledProvidersForProfile(eq(Process.myUserHandle()))
+        val mockPlugin = mock(CustomWidgetPlugin::class.java)
+        underTest.onPluginConnected(mockPlugin, context)
+        assertEquals(1, underTest.stream().toList().size)
+    }
+
+    companion object {
+        private const val TEST_CLASS = "TEST_CLASS"
+        private val TEST_COMPONENT_NAME =
+            ComponentName(getInstrumentation().targetContext.packageName, TEST_CLASS)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt
new file mode 100644
index 0000000..5df7caa
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.model
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.UserHandle
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.LimitDevicesRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.icons.ComponentWithLabel
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.model.data.PackageItemInfo
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.WidgetUtils
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+
+@RunWith(AndroidJUnit4::class)
+@AllowedDevices(allowed = [DeviceProduct.ROBOLECTRIC])
+class WidgetsListBaseEntriesBuilderTest {
+    @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+    @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var iconCache: IconCache
+
+    private lateinit var userHandle: UserHandle
+    private lateinit var context: Context
+    private lateinit var testInvariantProfile: InvariantDeviceProfile
+    private lateinit var allWidgets: Map<PackageItemInfo, List<WidgetItem>>
+    private lateinit var underTest: WidgetsListBaseEntriesBuilder
+
+    @Before
+    fun setUp() {
+        userHandle = UserHandle.CURRENT
+        context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+        testInvariantProfile = LauncherAppState.getIDP(context)
+
+        doAnswer { invocation: InvocationOnMock ->
+                val componentWithLabel = invocation.getArgument<Any>(0) as ComponentWithLabel
+                componentWithLabel.getComponent().shortClassName
+            }
+            .`when`(iconCache)
+            .getTitleNoCache(any<ComponentWithLabel>())
+        underTest = WidgetsListBaseEntriesBuilder(context)
+
+        allWidgets =
+            mapOf(
+                // app 1
+                packageItemInfoWithTitle(APP_1_PACKAGE_NAME, APP_1_PACKAGE_TITLE) to
+                    listOf(
+                        createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_1_CLASS_NAME),
+                        createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_2_CLASS_NAME)
+                    ),
+                // app 2
+                packageItemInfoWithTitle(APP_2_PACKAGE_NAME, APP_2_PACKAGE_TITLE) to
+                    listOf(createWidgetItem(APP_2_PACKAGE_NAME, APP_2_PROVIDER_1_CLASS_NAME)),
+                // app 3
+                packageItemInfoWithTitle(APP_3_PACKAGE_NAME, APP_3_PACKAGE_TITLE) to
+                    listOf(createWidgetItem(APP_3_PACKAGE_NAME, APP_3_PROVIDER_1_CLASS_NAME))
+            )
+    }
+
+    @Test
+    fun widgetsListBaseEntriesBuilder_addsHeaderAndContentEntries_withCorrectSectionName() {
+        val expectedWidgetsCountBySection =
+            listOf(
+                APP_1_EXPECTED_SECTION_NAME to 2,
+                APP_2_EXPECTED_SECTION_NAME to 1,
+                APP_3_EXPECTED_SECTION_NAME to 1
+            )
+
+        val entries = underTest.build(allWidgets)
+
+        assertThat(entries).hasSize(6)
+        val headerEntrySectionAndWidgetSizes =
+            entries.filterIsInstance<WidgetsListHeaderEntry>().map {
+                it.mTitleSectionName to it.mWidgets.size
+            }
+        val contentEntrySectionAndWidgetSizes =
+            entries.filterIsInstance<WidgetsListContentEntry>().map {
+                it.mTitleSectionName to it.mWidgets.size
+            }
+        assertThat(headerEntrySectionAndWidgetSizes)
+            .containsExactlyElementsIn(expectedWidgetsCountBySection)
+        assertThat(contentEntrySectionAndWidgetSizes)
+            .containsExactlyElementsIn(expectedWidgetsCountBySection)
+    }
+
+    @Test
+    fun widgetsListBaseEntriesBuilder_withFilter_addsFilteredHeaderAndContentEntries() {
+        val allowList = listOf(APP_1_PROVIDER_1_CLASS_NAME, APP_3_PROVIDER_1_CLASS_NAME)
+        val expectedWidgetsCountBySection =
+            listOf(
+                APP_1_EXPECTED_SECTION_NAME to 1, // one widget filtered out
+                APP_3_EXPECTED_SECTION_NAME to 1
+            )
+
+        val entries =
+            underTest.build(allWidgets) { w -> allowList.contains(w.componentName.shortClassName) }
+
+        assertThat(entries).hasSize(4) // app 2 filtered out
+        val headerEntrySectionAndWidgetSizes =
+            entries.filterIsInstance<WidgetsListHeaderEntry>().map {
+                it.mTitleSectionName to it.mWidgets.size
+            }
+        val contentEntrySectionAndWidgetSizes =
+            entries.filterIsInstance<WidgetsListContentEntry>().map {
+                it.mTitleSectionName to it.mWidgets.size
+            }
+        assertThat(headerEntrySectionAndWidgetSizes)
+            .containsExactlyElementsIn(expectedWidgetsCountBySection)
+        assertThat(contentEntrySectionAndWidgetSizes)
+            .containsExactlyElementsIn(expectedWidgetsCountBySection)
+    }
+
+    private fun packageItemInfoWithTitle(packageName: String, title: String): PackageItemInfo {
+        val packageItemInfo = PackageItemInfo(packageName, userHandle)
+        packageItemInfo.title = title
+        return packageItemInfo
+    }
+
+    private fun createWidgetItem(packageName: String, widgetProviderName: String): WidgetItem {
+        val providerInfo =
+            WidgetUtils.createAppWidgetProviderInfo(
+                ComponentName.createRelative(packageName, widgetProviderName)
+            )
+        val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo)
+        return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context)
+    }
+
+    companion object {
+        const val APP_1_PACKAGE_NAME = "com.example.app1"
+        const val APP_1_PACKAGE_TITLE = "App1"
+        const val APP_1_EXPECTED_SECTION_NAME = "A" // for fast popup
+        const val APP_1_PROVIDER_1_CLASS_NAME = "app1Provider1"
+        const val APP_1_PROVIDER_2_CLASS_NAME = "app1Provider2"
+
+        const val APP_2_PACKAGE_NAME = "com.example.app2"
+        const val APP_2_PACKAGE_TITLE = "SomeApp2"
+        const val APP_2_EXPECTED_SECTION_NAME = "S" // for fast popup
+        const val APP_2_PROVIDER_1_CLASS_NAME = "app2Provider1"
+
+        const val APP_3_PACKAGE_NAME = "com.example.app3"
+        const val APP_3_PACKAGE_TITLE = "OtherApp3"
+        const val APP_3_EXPECTED_SECTION_NAME = "O" // for fast popup
+        const val APP_3_PROVIDER_1_CLASS_NAME = "app3Provider1"
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProviderTest.kt
new file mode 100644
index 0000000..1822639
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProviderTest.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.model
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.UserHandle
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.LimitDevicesRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.icons.ComponentWithLabel
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.model.data.PackageItemInfo
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.WidgetUtils
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.android.launcher3.widget.PendingAddWidgetInfo
+import com.android.launcher3.widget.model.WidgetsListBaseEntry
+import com.android.launcher3.widget.model.WidgetsListContentEntry
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider.WidgetPickerDataChangeListener
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+// Tests for the WidgetPickerDataProvider class
+
+@RunWith(AndroidJUnit4::class)
+@AllowedDevices(allowed = [DeviceProduct.ROBOLECTRIC])
+class WidgetPickerDataProviderTest {
+    @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+    @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var changeListener: WidgetPickerDataChangeListener
+
+    @Mock private lateinit var iconCache: IconCache
+
+    private lateinit var userHandle: UserHandle
+    private lateinit var context: Context
+    private lateinit var testInvariantProfile: InvariantDeviceProfile
+
+    private lateinit var appWidgetItem: WidgetItem
+
+    private var underTest = WidgetPickerDataProvider()
+
+    @Before
+    fun setUp() {
+        userHandle = UserHandle.CURRENT
+        context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+        testInvariantProfile = LauncherAppState.getIDP(context)
+
+        doAnswer { invocation: InvocationOnMock ->
+                val componentWithLabel = invocation.getArgument<Any>(0) as ComponentWithLabel
+                componentWithLabel.getComponent().shortClassName
+            }
+            .`when`(iconCache)
+            .getTitleNoCache(any<ComponentWithLabel>())
+
+        appWidgetItem = createWidgetItem()
+    }
+
+    @Test
+    fun setWidgets_invokesTheListener_andUpdatedWidgetsAvailable() {
+        assertThat(underTest.get().allWidgets).isEmpty()
+
+        underTest.setChangeListener(changeListener)
+        val allWidgets = appWidgetListBaseEntries()
+        underTest.setWidgets(allWidgets = allWidgets)
+
+        assertThat(underTest.get().allWidgets).containsExactlyElementsIn(allWidgets)
+        verify(changeListener, times(1)).onWidgetsBound()
+        verifyNoMoreInteractions(changeListener)
+    }
+
+    @Test
+    fun setWidgetRecommendations_callsBackTheListener_andUpdatedRecommendationsAvailable() {
+        underTest.setWidgets(allWidgets = appWidgetListBaseEntries())
+        assertThat(underTest.get().recommendations).isEmpty()
+
+        underTest.setChangeListener(changeListener)
+        val recommendations =
+            listOf(
+                PendingAddWidgetInfo(
+                    appWidgetItem.widgetInfo,
+                    LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION
+                ),
+            )
+        underTest.setWidgetRecommendations(recommendations)
+
+        assertThat(underTest.get().recommendations).hasSize(1)
+        verify(changeListener, times(1)).onRecommendedWidgetsBound()
+        verifyNoMoreInteractions(changeListener)
+    }
+
+    @Test
+    fun setChangeListener_null_noCallback() {
+        underTest.setChangeListener(changeListener)
+        underTest.setChangeListener(null) // reset
+
+        underTest.setWidgets(allWidgets = appWidgetListBaseEntries())
+        val recommendations =
+            listOf(
+                PendingAddWidgetInfo(
+                    appWidgetItem.widgetInfo,
+                    LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION
+                ),
+            )
+        underTest.setWidgetRecommendations(recommendations)
+
+        verifyNoMoreInteractions(changeListener)
+    }
+
+    private fun createWidgetItem(): WidgetItem {
+        val providerInfo =
+            WidgetUtils.createAppWidgetProviderInfo(
+                ComponentName.createRelative(APP_PACKAGE_NAME, APP_PROVIDER_1_CLASS_NAME)
+            )
+        val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo)
+        return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context)
+    }
+
+    private fun appWidgetListBaseEntries(): List<WidgetsListBaseEntry> {
+        val packageItemInfo = PackageItemInfo(APP_PACKAGE_NAME, userHandle)
+        packageItemInfo.title = APP_PACKAGE_TITLE
+        val widgets = listOf(appWidgetItem)
+
+        return buildList {
+            add(WidgetsListHeaderEntry.create(packageItemInfo, APP_SECTION_NAME, widgets))
+            add(WidgetsListContentEntry(packageItemInfo, APP_SECTION_NAME, widgets))
+        }
+    }
+
+    companion object {
+        const val APP_PACKAGE_NAME = "com.example.app"
+        const val APP_PACKAGE_TITLE = "SomeApp"
+        const val APP_SECTION_NAME = "S" // for fast popup
+        const val APP_PROVIDER_1_CLASS_NAME = "appProvider1"
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/data/WidgetPickerDataTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/data/WidgetPickerDataTest.kt
new file mode 100644
index 0000000..e59e211
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/data/WidgetPickerDataTest.kt
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.model.data
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.UserHandle
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.LimitDevicesRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION
+import com.android.launcher3.icons.ComponentWithLabel
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.PackageItemInfo
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.util.WidgetUtils
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.android.launcher3.widget.PendingAddWidgetInfo
+import com.android.launcher3.widget.model.WidgetsListBaseEntry
+import com.android.launcher3.widget.model.WidgetsListContentEntry
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry
+import com.android.launcher3.widget.picker.WidgetRecommendationCategory
+import com.android.launcher3.widget.picker.WidgetRecommendationCategory.DEFAULT_WIDGET_RECOMMENDATION_CATEGORY
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findAllWidgetsForPackageUser
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findContentEntryForPackageUser
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withRecommendedWidgets
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withWidgets
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+
+// Tests for code / classes in WidgetPickerData file.
+
+@RunWith(AndroidJUnit4::class)
+@AllowedDevices(allowed = [DeviceProduct.ROBOLECTRIC])
+class WidgetPickerDataTest {
+    @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+    @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var iconCache: IconCache
+
+    private lateinit var userHandle: UserHandle
+    private lateinit var context: Context
+    private lateinit var testInvariantProfile: InvariantDeviceProfile
+
+    private lateinit var app1PackageItemInfo: PackageItemInfo
+    private lateinit var app2PackageItemInfo: PackageItemInfo
+
+    private lateinit var app1WidgetItem1: WidgetItem
+    private lateinit var app1WidgetItem2: WidgetItem
+    private lateinit var app2WidgetItem1: WidgetItem
+
+    @Before
+    fun setUp() {
+        userHandle = UserHandle.CURRENT
+        context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+        testInvariantProfile = LauncherAppState.getIDP(context)
+
+        doAnswer { invocation: InvocationOnMock ->
+                val componentWithLabel = invocation.getArgument<Any>(0) as ComponentWithLabel
+                componentWithLabel.getComponent().shortClassName
+            }
+            .`when`(iconCache)
+            .getTitleNoCache(any<ComponentWithLabel>())
+
+        app1PackageItemInfo = packageItemInfoWithTitle(APP_1_PACKAGE_NAME, APP_1_PACKAGE_TITLE)
+        app2PackageItemInfo = packageItemInfoWithTitle(APP_2_PACKAGE_NAME, APP_2_PACKAGE_TITLE)
+
+        app1WidgetItem1 = createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_1_CLASS_NAME)
+        app1WidgetItem2 = createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_2_CLASS_NAME)
+        app2WidgetItem1 = createWidgetItem(APP_2_PACKAGE_NAME, APP_2_PROVIDER_1_CLASS_NAME)
+    }
+
+    @Test
+    fun withWidgets_returnsACopyWithProvidedWidgets() {
+        // only app two
+        val widgetPickerData = WidgetPickerData(allWidgets = appTwoWidgetsListBaseEntries())
+
+        // update: only app 1 and default list set
+        val newAllWidgets: List<WidgetsListBaseEntry> =
+            appOneWidgetsListBaseEntries(includeWidgetTwo = true)
+        val newDefaultWidgets: List<WidgetsListBaseEntry> =
+            appOneWidgetsListBaseEntries(includeWidgetTwo = false)
+
+        val newWidgetData = widgetPickerData.withWidgets(newAllWidgets, newDefaultWidgets)
+
+        assertThat(newWidgetData.allWidgets).containsExactlyElementsIn(newAllWidgets)
+        assertThat(newWidgetData.defaultWidgets).containsExactlyElementsIn(newDefaultWidgets)
+    }
+
+    @Test
+    fun withWidgets_noExplicitDefaults_unsetsOld() {
+        // only app two
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets = appTwoWidgetsListBaseEntries(),
+                defaultWidgets = appTwoWidgetsListBaseEntries()
+            )
+
+        val newWidgetData =
+            widgetPickerData.withWidgets(allWidgets = appOneWidgetsListBaseEntries())
+
+        assertThat(newWidgetData.allWidgets)
+            .containsExactlyElementsIn(appOneWidgetsListBaseEntries())
+        assertThat(newWidgetData.defaultWidgets).isEmpty() // previous values cleared.
+    }
+
+    @Test
+    fun withRecommendedWidgets_returnsACopyWithProvidedRecommendedWidgets() {
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets =
+                    buildList {
+                        addAll(appOneWidgetsListBaseEntries())
+                        addAll(appTwoWidgetsListBaseEntries())
+                    },
+                defaultWidgets = buildList { appTwoWidgetsListBaseEntries() }
+            )
+        val recommendations: List<ItemInfo> =
+            listOf(
+                PendingAddWidgetInfo(
+                    app1WidgetItem1.widgetInfo,
+                    CONTAINER_WIDGETS_PREDICTION,
+                    CATEGORY_1
+                ),
+                PendingAddWidgetInfo(
+                    app2WidgetItem1.widgetInfo,
+                    CONTAINER_WIDGETS_PREDICTION,
+                    CATEGORY_2
+                ),
+            )
+
+        val updatedData = widgetPickerData.withRecommendedWidgets(recommendations)
+
+        assertThat(updatedData.recommendations.keys).containsExactly(CATEGORY_1, CATEGORY_2)
+        assertThat(updatedData.recommendations[CATEGORY_1]).containsExactly(app1WidgetItem1)
+        assertThat(updatedData.recommendations[CATEGORY_2]).containsExactly(app2WidgetItem1)
+    }
+
+    @Test
+    fun withRecommendedWidgets_noCategory_usesDefault() {
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets =
+                    buildList {
+                        addAll(appOneWidgetsListBaseEntries())
+                        addAll(appTwoWidgetsListBaseEntries())
+                    },
+                defaultWidgets = buildList { appTwoWidgetsListBaseEntries() }
+            )
+        val recommendations: List<ItemInfo> =
+            listOf(
+                PendingAddWidgetInfo(app1WidgetItem1.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
+                PendingAddWidgetInfo(app2WidgetItem1.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
+            )
+
+        val updatedData = widgetPickerData.withRecommendedWidgets(recommendations)
+
+        assertThat(updatedData.recommendations.keys)
+            .containsExactly(DEFAULT_WIDGET_RECOMMENDATION_CATEGORY)
+        assertThat(updatedData.recommendations[DEFAULT_WIDGET_RECOMMENDATION_CATEGORY])
+            .containsExactly(app1WidgetItem1, app2WidgetItem1)
+    }
+
+    @Test
+    fun withRecommendedWidgets_emptyRecommendations_clearsOld() {
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets =
+                    buildList {
+                        addAll(appOneWidgetsListBaseEntries())
+                        addAll(appTwoWidgetsListBaseEntries())
+                    },
+                defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
+                recommendations = mapOf(CATEGORY_1 to listOf(app1WidgetItem1))
+            )
+
+        val updatedData = widgetPickerData.withRecommendedWidgets(listOf())
+
+        assertThat(updatedData.recommendations).isEmpty()
+    }
+
+    @Test
+    fun withRecommendedWidgets_widgetNotInAllWidgets_filteredOut() {
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets =
+                    buildList {
+                        addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false))
+                        addAll(appTwoWidgetsListBaseEntries())
+                    },
+                defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
+            )
+
+        val recommendations: List<ItemInfo> =
+            listOf(
+                PendingAddWidgetInfo(app1WidgetItem2.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
+                PendingAddWidgetInfo(app2WidgetItem1.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
+            )
+        val updatedData = widgetPickerData.withRecommendedWidgets(recommendations)
+
+        assertThat(updatedData.recommendations).hasSize(1)
+        // no app1widget2
+        assertThat(updatedData.recommendations.values.first()).containsExactly(app2WidgetItem1)
+    }
+
+    @Test
+    fun findContentEntryForPackageUser_returnsCorrectEntry() {
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets =
+                    buildList {
+                        addAll(appOneWidgetsListBaseEntries())
+                        addAll(appTwoWidgetsListBaseEntries())
+                    },
+                defaultWidgets = buildList { addAll(appTwoWidgetsListBaseEntries()) }
+            )
+        val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
+
+        val contentEntry = findContentEntryForPackageUser(widgetPickerData, app1PackageUserKey)
+
+        assertThat(contentEntry).isNotNull()
+        assertThat(contentEntry?.mPkgItem).isEqualTo(app1PackageItemInfo)
+        assertThat(contentEntry?.mWidgets).hasSize(2)
+    }
+
+    @Test
+    fun findContentEntryForPackageUser_fromDefaults_returnsEntryFromDefaultWidgets() {
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets =
+                    buildList {
+                        addAll(appOneWidgetsListBaseEntries())
+                        addAll(appTwoWidgetsListBaseEntries())
+                    },
+                defaultWidgets =
+                    buildList { addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false)) }
+            )
+        val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
+
+        val contentEntry =
+            findContentEntryForPackageUser(
+                widgetPickerData = widgetPickerData,
+                packageUserKey = app1PackageUserKey,
+                fromDefaultWidgets = true
+            )
+
+        assertThat(contentEntry).isNotNull()
+        assertThat(contentEntry?.mPkgItem).isEqualTo(app1PackageItemInfo)
+        // only one widget (since default widgets had only one widget for app A
+        assertThat(contentEntry?.mWidgets).hasSize(1)
+    }
+
+    @Test
+    fun findContentEntryForPackageUser_noMatch_returnsNull() {
+        val app2PackageUserKey = PackageUserKey.fromPackageItemInfo(app2PackageItemInfo)
+        val widgetPickerData =
+            WidgetPickerData(allWidgets = buildList { addAll(appOneWidgetsListBaseEntries()) })
+
+        val contentEntry = findContentEntryForPackageUser(widgetPickerData, app2PackageUserKey)
+
+        assertThat(contentEntry).isNull()
+    }
+
+    @Test
+    fun findAllWidgetsForPackageUser_returnsListOfWidgets() {
+        val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets =
+                    buildList {
+                        addAll(appOneWidgetsListBaseEntries())
+                        addAll(appTwoWidgetsListBaseEntries())
+                    },
+                defaultWidgets =
+                    buildList { addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false)) }
+            )
+
+        val widgets = findAllWidgetsForPackageUser(widgetPickerData, app1PackageUserKey)
+
+        // both widgets returned irrespective of default widgets list
+        assertThat(widgets).hasSize(2)
+    }
+
+    @Test
+    fun findAllWidgetsForPackageUser_noMatch_returnsEmptyList() {
+        val widgetPickerData =
+            WidgetPickerData(
+                allWidgets = buildList { addAll(appTwoWidgetsListBaseEntries()) },
+            )
+        val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
+
+        val widgets = findAllWidgetsForPackageUser(widgetPickerData, app1PackageUserKey)
+
+        assertThat(widgets).isEmpty()
+    }
+
+    private fun packageItemInfoWithTitle(packageName: String, title: String): PackageItemInfo {
+        val packageItemInfo = PackageItemInfo(packageName, userHandle)
+        packageItemInfo.title = title
+        return packageItemInfo
+    }
+
+    private fun createWidgetItem(packageName: String, widgetProviderName: String): WidgetItem {
+        val providerInfo =
+            WidgetUtils.createAppWidgetProviderInfo(
+                ComponentName.createRelative(packageName, widgetProviderName)
+            )
+        val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo)
+        return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context)
+    }
+
+    private fun appTwoWidgetsListBaseEntries(): List<WidgetsListBaseEntry> = buildList {
+        val widgets = listOf(app2WidgetItem1)
+        add(WidgetsListHeaderEntry.create(app2PackageItemInfo, APP_2_SECTION_NAME, widgets))
+        add(WidgetsListContentEntry(app2PackageItemInfo, APP_2_SECTION_NAME, widgets))
+    }
+
+    private fun appOneWidgetsListBaseEntries(
+        includeWidgetTwo: Boolean = true
+    ): List<WidgetsListBaseEntry> = buildList {
+        val widgets =
+            if (includeWidgetTwo) {
+                listOf(app1WidgetItem1, app1WidgetItem2)
+            } else {
+                listOf(app1WidgetItem1)
+            }
+
+        add(WidgetsListHeaderEntry.create(app1PackageItemInfo, APP_1_SECTION_NAME, widgets))
+        add(WidgetsListContentEntry(app1PackageItemInfo, APP_1_SECTION_NAME, widgets))
+    }
+
+    companion object {
+        private const val APP_1_PACKAGE_NAME = "com.example.app1"
+        private const val APP_1_PACKAGE_TITLE = "App1"
+        private const val APP_1_SECTION_NAME = "A" // for fast popup
+        private const val APP_1_PROVIDER_1_CLASS_NAME = "app1Provider1"
+        private const val APP_1_PROVIDER_2_CLASS_NAME = "app1Provider2"
+
+        private const val APP_2_PACKAGE_NAME = "com.example.app2"
+        private const val APP_2_PACKAGE_TITLE = "SomeApp2"
+        private const val APP_2_SECTION_NAME = "S" // for fast popup
+        private const val APP_2_PROVIDER_1_CLASS_NAME = "app2Provider1"
+
+        private val CATEGORY_1 =
+            WidgetRecommendationCategory(/* categoryTitleRes= */ 0, /* order= */ 0)
+        private val CATEGORY_2 =
+            WidgetRecommendationCategory(/* categoryTitleRes= */ 1, /* order= */ 1)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
index 0370a6b..24d66a3 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
@@ -45,12 +45,12 @@
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
-import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.search.SearchCallback;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.picker.search.WidgetsSearchBar.WidgetsSearchDataProvider;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -79,7 +79,7 @@
 
     private SimpleWidgetsSearchAlgorithm mSimpleWidgetsSearchAlgorithm;
     @Mock
-    private PopupDataProvider mDataProvider;
+    private WidgetsSearchDataProvider mDataProvider;
     @Mock
     private SearchCallback<WidgetsListBaseEntry> mSearchCallback;
 
@@ -106,7 +106,7 @@
 
         mSimpleWidgetsSearchAlgorithm = MAIN_EXECUTOR.submit(
                 () -> new SimpleWidgetsSearchAlgorithm(mDataProvider)).get();
-        doReturn(Collections.EMPTY_LIST).when(mDataProvider).getAllWidgets();
+        doReturn(Collections.EMPTY_LIST).when(mDataProvider).getWidgets();
     }
 
     @Test
@@ -114,7 +114,7 @@
         doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
                 mCameraContentEntry, mClockHeaderEntry, mClockContentEntry))
                 .when(mDataProvider)
-                .getAllWidgets();
+                .getWidgets();
 
         assertEquals(List.of(
                 WidgetsListHeaderEntry.createForSearch(
@@ -135,7 +135,7 @@
         doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
                 mCameraContentEntry))
                 .when(mDataProvider)
-                .getAllWidgets();
+                .getWidgets();
 
         assertEquals(List.of(
                 WidgetsListHeaderEntry.createForSearch(
@@ -162,7 +162,7 @@
         doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
                 mCameraContentEntry, mClockHeaderEntry, mClockContentEntry))
                 .when(mDataProvider)
-                .getAllWidgets();
+                .getWidgets();
         mSimpleWidgetsSearchAlgorithm.doSearch("Ca", mSearchCallback);
         getInstrumentation().waitForIdleSync();
         verify(mSearchCallback).onSearchResult(
diff --git a/tests/src/com/android/launcher3/allapps/FloatingMaskViewTest.kt b/tests/src/com/android/launcher3/allapps/FloatingMaskViewTest.kt
new file mode 100644
index 0000000..cf03adc
--- /dev/null
+++ b/tests/src/com/android/launcher3/allapps/FloatingMaskViewTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps
+
+import android.content.Context
+import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
+import android.widget.ImageView
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.util.ActivityContextWrapper
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+class FloatingMaskViewTest {
+    @Mock
+    private val mockAllAppsRecyclerView: AllAppsRecyclerView? = null
+
+    @Mock
+    private val mockBottomBox: ImageView? = null
+    private var mVut: FloatingMaskView? = null
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        val context: Context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+        mVut = FloatingMaskView(context)
+        mVut!!.layoutParams = MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT)
+    }
+
+    @Test
+    fun setParameters_paramsMarginEqualRecyclerViewPadding() {
+        val floatingMaskView = Mockito.spy(mVut)
+        Mockito.`when`(mockAllAppsRecyclerView!!.paddingLeft).thenReturn(PADDING_PX)
+        Mockito.`when`(mockAllAppsRecyclerView.paddingRight).thenReturn(PADDING_PX)
+        Mockito.`when`(mockAllAppsRecyclerView.paddingBottom).thenReturn(PADDING_PX)
+        Mockito.`when`(floatingMaskView!!.bottomBox).thenReturn(mockBottomBox)
+        val lp = floatingMaskView.layoutParams as MarginLayoutParams
+
+        floatingMaskView.setParameters(lp, mockAllAppsRecyclerView)
+
+        Truth.assertThat(lp.leftMargin).isEqualTo(PADDING_PX)
+        Truth.assertThat(lp.rightMargin).isEqualTo(PADDING_PX)
+        Mockito.verify(mockBottomBox)?.minimumHeight = PADDING_PX
+    }
+
+    companion object {
+        private const val PADDING_PX = 15
+    }
+}
diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
index eac7f63..de48432 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
+++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
@@ -116,7 +116,8 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mContext = new ActivityContextWrapper(getApplicationContext());
+        mContext = new ActivityContextWrapper(getApplicationContext(),
+                R.style.DynamicColorsBaseLauncherTheme);
         when(mAllApps.getContext()).thenReturn(mContext);
         when(mUserCache.getUserInfo(PRIVATE_HANDLE)).thenReturn(PRIVATE_ICON_INFO);
         when(mUserCache.getUserProfiles())
diff --git a/tests/src/com/android/launcher3/dragging/TaplDragTest.java b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
index 41abcf8..76c1948 100644
--- a/tests/src/com/android/launcher3/dragging/TaplDragTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
@@ -65,6 +65,7 @@
     @Test
     @PortraitLandscape
     @PlatinumTest(focusArea = "launcher")
+    @ScreenRecordRule.ScreenRecord // b/353600888
     public void testDragToFolder() {
         // TODO: add the use case to drag an icon to an existing folder. Currently it either fails
         // on tablets or phones due to difference in resolution.
@@ -97,6 +98,7 @@
      * icon left.
      */
     @Test
+    @ScreenRecordRule.ScreenRecord // b/353600888
     public void testDragOutOfFolder() {
         final HomeAppIcon playStoreIcon = createShortcutIfNotExist(STORE_APP_NAME, 0, 1);
         final HomeAppIcon photosIcon = createShortcutInCenterIfNotExist(PHOTOS_APP_NAME);
@@ -174,13 +176,13 @@
     public void testDragAndCancelAppIcon() {
         final HomeAppIcon homeAppIcon = createShortcutInCenterIfNotExist(GMAIL_APP_NAME);
         Point positionBeforeDrag =
-                mLauncher.getWorkspace().getWorkspaceIconsPositions().get(GMAIL_APP_NAME);
+                mLauncher.getWorkspace().getWorkspaceIconPosition(GMAIL_APP_NAME);
         assertNotNull("App not found in Workspace before dragging.", positionBeforeDrag);
 
         mLauncher.getWorkspace().dragAndCancelAppIcon(homeAppIcon);
 
         Point positionAfterDrag =
-                mLauncher.getWorkspace().getWorkspaceIconsPositions().get(GMAIL_APP_NAME);
+                mLauncher.getWorkspace().getWorkspaceIconPosition(GMAIL_APP_NAME);
         assertNotNull("App not found in Workspace after dragging.", positionAfterDrag);
         assertEquals("App not returned to same position in Workspace after drag & cancel",
                 positionBeforeDrag, positionAfterDrag);
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 46cafa7..907aa50 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -46,7 +46,6 @@
 
 import java.io.IOException;
 import java.util.Arrays;
-import java.util.Map;
 
 /**
  * Test runs in Out of process (Oop) and In process (Ipc)
@@ -60,6 +59,7 @@
      */
     @Test
     @PortraitLandscape
+    @ScreenRecordRule.ScreenRecord // b/349439239
     public void testDeleteFromWorkspace() {
         for (String appName : new String[]{GMAIL_APP_NAME, STORE_APP_NAME, TEST_APP_NAME}) {
             final HomeAppIcon homeAppIcon = createShortcutInCenterIfNotExist(appName);
@@ -155,18 +155,14 @@
                 createShortcutIfNotExist(appNames[i], gridPositions[i]);
             }
 
-            Map<String, Point> initialPositions =
-                    mLauncher.getWorkspace().getWorkspaceIconsPositions();
-            assertThat(initialPositions.keySet()).containsAtLeastElementsIn(appNames);
+            Point initialPosition =
+                    mLauncher.getWorkspace().getWorkspaceIconPosition(DUMMY_APP_NAME);
+            assertThat(initialPosition).isNotNull();
 
             final Workspace workspace = mLauncher.getWorkspace().getWorkspaceAppIcon(
                     DUMMY_APP_NAME).uninstall();
             workspace.verifyWorkspaceAppIconIsGone(
                     DUMMY_APP_NAME + " was expected to disappear after uninstall.", DUMMY_APP_NAME);
-
-            Log.d(UIOBJECT_STALE_ELEMENT, "second getWorkspaceIconsPositions()");
-            Map<String, Point> finalPositions = workspace.getWorkspaceIconsPositions();
-            assertThat(finalPositions).doesNotContainKey(DUMMY_APP_NAME);
         } finally {
             TestUtil.uninstallDummyApp();
         }
diff --git a/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt b/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt
new file mode 100644
index 0000000..b491f17
--- /dev/null
+++ b/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.folder
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.folder.FolderNameInfos.*
+import org.junit.Test
+import org.junit.runner.RunWith
+
+data class Label(val index: Int, val label: String, val score: Float)
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderNameInfosTest {
+
+    companion object {
+        val statusList =
+            listOf(
+                SUCCESS,
+                HAS_PRIMARY,
+                HAS_SUGGESTIONS,
+                ERROR_NO_PROVIDER,
+                ERROR_APP_LOOKUP_FAILED,
+                ERROR_ALL_APP_LOOKUP_FAILED,
+                ERROR_NO_LABELS_GENERATED,
+                ERROR_LABEL_LOOKUP_FAILED,
+                ERROR_ALL_LABEL_LOOKUP_FAILED,
+                ERROR_NO_PACKAGES,
+            )
+    }
+
+    @Test
+    fun status() {
+        assertStatus(statusList)
+        assertStatus(
+            listOf(
+                ERROR_NO_PROVIDER,
+                ERROR_APP_LOOKUP_FAILED,
+                ERROR_ALL_APP_LOOKUP_FAILED,
+                ERROR_NO_LABELS_GENERATED,
+                ERROR_LABEL_LOOKUP_FAILED,
+                ERROR_ALL_LABEL_LOOKUP_FAILED,
+                ERROR_NO_PACKAGES,
+            )
+        )
+        assertStatus(
+            listOf(
+                SUCCESS,
+                HAS_PRIMARY,
+                HAS_SUGGESTIONS,
+            )
+        )
+        assertStatus(
+            listOf(
+                SUCCESS,
+                HAS_PRIMARY,
+                HAS_SUGGESTIONS,
+            )
+        )
+    }
+
+    fun assertStatus(statusList: List<Int>) {
+        var infos = FolderNameInfos()
+        statusList.forEach { infos.setStatus(it) }
+        assert(infos.status() == statusList.sum()) {
+            "There is an overlap on the status constants!"
+        }
+    }
+
+    @Test
+    fun hasPrimary() {
+        assertHasPrimary(
+            createNameInfos(listOf(Label(0, "label", 1f)), statusList),
+            hasPrimary = true
+        )
+        assertHasPrimary(
+            createNameInfos(listOf(Label(1, "label", 1f)), statusList),
+            hasPrimary = false
+        )
+        assertHasPrimary(
+            createNameInfos(
+                listOf(Label(0, "label", 1f)),
+                listOf(
+                    ERROR_NO_PROVIDER,
+                    ERROR_APP_LOOKUP_FAILED,
+                    ERROR_ALL_APP_LOOKUP_FAILED,
+                    ERROR_NO_LABELS_GENERATED,
+                    ERROR_LABEL_LOOKUP_FAILED,
+                    ERROR_ALL_LABEL_LOOKUP_FAILED,
+                    ERROR_NO_PACKAGES,
+                )
+            ),
+            hasPrimary = false
+        )
+    }
+
+    private fun assertHasPrimary(nameInfos: FolderNameInfos, hasPrimary: Boolean) =
+        assert(nameInfos.hasPrimary() == hasPrimary)
+
+    private fun createNameInfos(labels: List<Label>?, statusList: List<Int>?): FolderNameInfos {
+        val infos = FolderNameInfos()
+        labels?.forEach { infos.setLabel(it.index, it.label, it.score) }
+        statusList?.forEach { infos.setStatus(it) }
+        return infos
+    }
+
+    @Test
+    fun hasSuggestions() {
+        assertHasSuggestions(
+            createNameInfos(listOf(Label(0, "label", 1f)), null),
+            hasSuggestions = true
+        )
+        assertHasSuggestions(createNameInfos(null, null), hasSuggestions = false)
+        // There is a max of 4 suggestions
+        assertHasSuggestions(
+            createNameInfos(listOf(Label(5, "label", 1f)), null),
+            hasSuggestions = false
+        )
+        assertHasSuggestions(
+            createNameInfos(
+                listOf(
+                    Label(0, "label", 1f),
+                    Label(1, "label", 1f),
+                    Label(2, "label", 1f),
+                    Label(3, "label", 1f)
+                ),
+                null
+            ),
+            hasSuggestions = true
+        )
+    }
+
+    private fun assertHasSuggestions(nameInfos: FolderNameInfos, hasSuggestions: Boolean) =
+        assert(nameInfos.hasSuggestions() == hasSuggestions)
+
+    @Test
+    fun hasContains() {
+        assertContains(
+            createNameInfos(
+                listOf(
+                    Label(0, "label1", 1f),
+                    Label(1, "label2", 1f),
+                    Label(2, "label3", 1f),
+                    Label(3, "label4", 1f)
+                ),
+                null
+            ),
+            label = Label(-1, "label3", -1f),
+            contains = true
+        )
+        assertContains(
+            createNameInfos(
+                listOf(
+                    Label(0, "label1", 1f),
+                    Label(1, "label2", 1f),
+                    Label(2, "label3", 1f),
+                    Label(3, "label4", 1f)
+                ),
+                null
+            ),
+            label = Label(-1, "label5", -1f),
+            contains = false
+        )
+        assertContains(
+            createNameInfos(null, null),
+            label = Label(-1, "label1", -1f),
+            contains = false
+        )
+        assertContains(
+            createNameInfos(
+                listOf(
+                    Label(0, "label1", 1f),
+                    Label(1, "label2", 1f),
+                    Label(2, "lAbel3", 1f),
+                    Label(3, "lEbel4", 1f)
+                ),
+                null
+            ),
+            label = Label(-1, "LaBEl3", -1f),
+            contains = true
+        )
+    }
+
+    private fun assertContains(nameInfos: FolderNameInfos, label: Label, contains: Boolean) =
+        assert(nameInfos.contains(label.label) == contains)
+}
diff --git a/tests/src/com/android/launcher3/folder/FolderPagedViewTest.kt b/tests/src/com/android/launcher3/folder/FolderPagedViewTest.kt
new file mode 100644
index 0000000..e8681dc
--- /dev/null
+++ b/tests/src/com/android/launcher3/folder/FolderPagedViewTest.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.folder
+
+import android.graphics.Point
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+data class TestCase(val maxCountX: Int, val maxCountY: Int, val totalItems: Int)
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderPagedViewTest {
+
+    companion object {
+        private fun makeFolderGridOrganizer(testCase: TestCase): FolderGridOrganizer {
+            val folderGridOrganizer = FolderGridOrganizer(testCase.maxCountX, testCase.maxCountY)
+            folderGridOrganizer.setContentSize(testCase.totalItems)
+            return folderGridOrganizer
+        }
+    }
+
+    @Test
+    fun setContentSize() {
+        assertCountXandY(
+            TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+            expectedCountX = 4,
+            expectedCountY = 3
+        )
+        assertCountXandY(
+            TestCase(maxCountX = 4, maxCountY = 3, totalItems = 8),
+            expectedCountX = 3,
+            expectedCountY = 3
+        )
+        assertCountXandY(
+            TestCase(maxCountX = 4, maxCountY = 3, totalItems = 3),
+            expectedCountX = 2,
+            expectedCountY = 2
+        )
+    }
+
+    private fun assertCountXandY(testCase: TestCase, expectedCountX: Int, expectedCountY: Int) {
+        val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+        assert(folderGridOrganizer.countX == expectedCountX) {
+            "Error on expected countX $expectedCountX got ${folderGridOrganizer.countX} using test case $testCase"
+        }
+        assert(folderGridOrganizer.countY == expectedCountY) {
+            "Error on expected countY $expectedCountY got ${folderGridOrganizer.countY} using test case $testCase"
+        }
+    }
+
+    @Test
+    fun getPosForRank() {
+        assertFolderRank(
+            TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+            expectedPos = Point(0, 0),
+            rank = 0
+        )
+        assertFolderRank(
+            TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+            expectedPos = Point(1, 0),
+            rank = 1
+        )
+        assertFolderRank(
+            TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+            expectedPos = Point(3, 0),
+            rank = 3
+        )
+        assertFolderRank(
+            TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+            expectedPos = Point(2, 1),
+            rank = 6
+        )
+        val testCase = TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22)
+        // Rank 16 and 38 should yield the same point since 38 % 12 == 2
+        val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+        assertFolderRank(testCase, expectedPos = folderGridOrganizer.getPosForRank(2), rank = 38)
+    }
+
+    private fun assertFolderRank(testCase: TestCase, expectedPos: Point, rank: Int) {
+        val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+        val pos = folderGridOrganizer.getPosForRank(rank)
+        assert(pos == expectedPos) {
+            "Expected pos = $expectedPos  doesn't match pos = $pos for the given rank $rank and the give test case $testCase"
+        }
+    }
+
+    @Test
+    fun isItemInPreview() {
+        val folderGridOrganizer = FolderGridOrganizer(5, 8)
+        folderGridOrganizer.setContentSize(ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW - 1)
+        // Very few items
+        for (i in 0..3) {
+            assertItemsInPreview(
+                TestCase(
+                    maxCountX = 5,
+                    maxCountY = 8,
+                    totalItems = ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW - 1
+                ),
+                expectedIsInPreview = true,
+                page = 0,
+                rank = i
+            )
+        }
+        for (i in 4..40) {
+            assertItemsInPreview(
+                TestCase(
+                    maxCountX = 5,
+                    maxCountY = 8,
+                    totalItems = ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW - 1
+                ),
+                expectedIsInPreview = false,
+                page = 0,
+                rank = i
+            )
+        }
+        // Full of items
+        assertItemsInPreview(
+            TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+            expectedIsInPreview = false,
+            page = 0,
+            rank = 2
+        )
+        assertItemsInPreview(
+            TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+            expectedIsInPreview = false,
+            page = 0,
+            rank = 2
+        )
+        assertItemsInPreview(
+            TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+            expectedIsInPreview = true,
+            page = 0,
+            rank = 5
+        )
+        assertItemsInPreview(
+            TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+            expectedIsInPreview = true,
+            page = 0,
+            rank = 6
+        )
+    }
+
+    private fun assertItemsInPreview(
+        testCase: TestCase,
+        expectedIsInPreview: Boolean,
+        page: Int,
+        rank: Int
+    ) {
+        val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+        val isInPreview = folderGridOrganizer.isItemInPreview(page, rank)
+        assert(isInPreview == expectedIsInPreview) {
+            "Item preview state should be $expectedIsInPreview but got $isInPreview, for page $page and rank $rank, for test case $testCase"
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/folder/FolderTest.kt b/tests/src/com/android/launcher3/folder/FolderTest.kt
index e1daa74..4eb335e 100644
--- a/tests/src/com/android/launcher3/folder/FolderTest.kt
+++ b/tests/src/com/android/launcher3/folder/FolderTest.kt
@@ -18,23 +18,56 @@
 
 import android.content.Context
 import android.graphics.Point
+import android.view.KeyEvent
 import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.widget.TextView
+import androidx.core.view.isVisible
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.launcher3.Alarm
+import com.android.launcher3.DragSource
 import com.android.launcher3.DropTarget.DragObject
 import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.OnAlarmListener
+import com.android.launcher3.R
 import com.android.launcher3.celllayout.board.FolderPoint
 import com.android.launcher3.celllayout.board.TestWorkspaceBuilder
+import com.android.launcher3.dragndrop.DragController
+import com.android.launcher3.dragndrop.DragOptions
+import com.android.launcher3.dragndrop.DragView
+import com.android.launcher3.folder.Folder.MIN_CONTENT_DIMEN
+import com.android.launcher3.folder.Folder.ON_EXIT_CLOSE_DELAY
+import com.android.launcher3.folder.Folder.SCROLL_LEFT
+import com.android.launcher3.folder.Folder.SCROLL_NONE
+import com.android.launcher3.folder.Folder.STATE_ANIMATING
+import com.android.launcher3.folder.Folder.STATE_CLOSED
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.util.ActivityContextWrapper
 import com.android.launcher3.util.ModelTestExtensions.clearModelDb
+import java.util.ArrayList
 import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertNull
+import junit.framework.TestCase.assertTrue
 import org.junit.After
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito
+import org.mockito.Mockito.spy
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.whenever
 
 /** Tests for [Folder] */
 @SmallTest
@@ -44,7 +77,7 @@
     private val context: Context =
         ActivityContextWrapper(ApplicationProvider.getApplicationContext())
     private val workspaceBuilder = TestWorkspaceBuilder(context)
-    private val folder: Folder = Mockito.spy(Folder(context, null))
+    private val folder: Folder = spy(Folder(context, null))
 
     @After
     fun tearDown() {
@@ -60,8 +93,10 @@
         folder.mContent = Mockito.mock(FolderPagedView::class.java)
         val dragLayout = Mockito.mock(View::class.java)
         val dragObject = Mockito.mock(DragObject::class.java)
-        assertEquals(folder.deleteFolderOnDropCompleted, false)
+        folder.deleteFolderOnDropCompleted = false
+
         folder.onDropCompleted(dragLayout, dragObject, true)
+
         verify(folder, times(1)).replaceFolderWithFinalItem()
         assertEquals(folder.deleteFolderOnDropCompleted, false)
     }
@@ -74,12 +109,819 @@
         folder.mContent = Mockito.mock(FolderPagedView::class.java)
         val dragLayout = Mockito.mock(View::class.java)
         val dragObject = Mockito.mock(DragObject::class.java)
-        assertEquals(folder.deleteFolderOnDropCompleted, false)
+        folder.deleteFolderOnDropCompleted = false
+
         folder.onDropCompleted(dragLayout, dragObject, true)
+
         verify(folder, times(0)).replaceFolderWithFinalItem()
         assertEquals(folder.deleteFolderOnDropCompleted, false)
     }
 
+    @Test
+    fun `Test that we accept valid item type ITEM_TYPE_APPLICATION`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APPLICATION
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertTrue(willAcceptResult)
+    }
+
+    @Test
+    fun `Test that we accept valid item type ITEM_TYPE_DEEP_SHORTCUT`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_DEEP_SHORTCUT
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertTrue(willAcceptResult)
+    }
+
+    @Test
+    fun `Test that we accept valid item type ITEM_TYPE_APP_PAIR`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APP_PAIR
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertTrue(willAcceptResult)
+    }
+
+    @Test
+    fun `Test that we do not accept invalid item type ITEM_TYPE_APPWIDGET`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APPWIDGET
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertFalse(willAcceptResult)
+    }
+
+    @Test
+    fun `Test that we do not accept invalid item type ITEM_TYPE_FOLDER`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_FOLDER
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertFalse(willAcceptResult)
+    }
+
+    @Test
+    fun `We should not animate open if items is null or less than or equal to 1`() {
+        folder.mInfo = Mockito.mock(FolderInfo::class.java)
+        val shouldAnimateOpenResult = folder.shouldAnimateOpen(null)
+
+        assertFalse(shouldAnimateOpenResult)
+        assertFalse(
+            folder.shouldAnimateOpen(arrayListOf<ItemInfo>(Mockito.mock(ItemInfo::class.java)))
+        )
+    }
+
+    @Test
+    fun `We should animate open if items greater than 1`() {
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = folderInfo
+
+        val shouldAnimateOpenResult = folder.shouldAnimateOpen(folder.mInfo.getContents())
+
+        assertTrue(shouldAnimateOpenResult)
+    }
+
+    @Test
+    fun `Should be true if there is an open folder`() {
+        val closeOpenFolderResult = folder.closeOpenFolder(Mockito.mock(Folder::class.java))
+
+        assertTrue(closeOpenFolderResult)
+    }
+
+    @Test
+    fun `Should be false if the open folder is this folder`() {
+        val closeOpenFolderResult = folder.closeOpenFolder(folder)
+
+        assertFalse(closeOpenFolderResult)
+    }
+
+    @Test
+    fun `Should be false if there is not an open folder`() {
+        val closeOpenFolderResult = folder.closeOpenFolder(null)
+
+        assertFalse(closeOpenFolderResult)
+    }
+
+    @Test
+    fun `If drag is in progress we should set mItemAddedBackToSelfViaIcon to true`() {
+        folder.itemAddedBackToSelfViaIcon = false
+        folder.isDragInProgress = true
+
+        folder.notifyDrop()
+
+        assertTrue(folder.itemAddedBackToSelfViaIcon)
+    }
+
+    @Test
+    fun `If drag is not in progress we should not set mItemAddedBackToSelfViaIcon to true`() {
+        folder.itemAddedBackToSelfViaIcon = false
+        folder.isDragInProgress = false
+
+        folder.notifyDrop()
+
+        assertFalse(folder.itemAddedBackToSelfViaIcon)
+    }
+
+    @Test
+    fun `If launcher dragging is not enabled onLongClick should return true`() {
+        `when`(folder.isLauncherDraggingEnabled).thenReturn(false)
+
+        val onLongClickResult = folder.onLongClick(Mockito.mock(View::class.java))
+
+        assertTrue(onLongClickResult)
+    }
+
+    @Test
+    fun `If launcher dragging is enabled we should return startDrag result`() {
+        `when`(folder.isLauncherDraggingEnabled).thenReturn(true)
+        val viewMock = Mockito.mock(View::class.java)
+        val dragOptions = Mockito.mock(DragOptions::class.java)
+
+        val onLongClickResult = folder.onLongClick(viewMock)
+
+        assertEquals(onLongClickResult, folder.startDrag(viewMock, dragOptions))
+        verify(folder, times(1)).startDrag(viewMock, dragOptions)
+    }
+
+    @Test
+    fun `Verify start drag works as intended when view is instanceof ItemInfo`() {
+        val itemInfo = ItemInfo()
+        itemInfo.rank = 5
+        val viewMock = Mockito.mock(View::class.java)
+        val dragOptions = DragOptions()
+        `when`(viewMock.tag).thenReturn(itemInfo)
+        folder.dragController = Mockito.mock(DragController::class.java)
+
+        folder.startDrag(viewMock, dragOptions)
+
+        assertEquals(folder.mEmptyCellRank, 5)
+        assertEquals(folder.currentDragView, viewMock)
+        verify(folder, times(1)).addDragListener(dragOptions)
+        verify(folder, times(1)).callBeginDragShared(viewMock, dragOptions)
+    }
+
+    @Test
+    fun `Verify start drag works as intended when view is not instanceof ItemInfo`() {
+        val viewMock = Mockito.mock(View::class.java)
+        val dragOptions = DragOptions()
+
+        folder.startDrag(viewMock, dragOptions)
+
+        verify(folder, times(0)).addDragListener(dragOptions)
+        verify(folder, times(0)).callBeginDragShared(viewMock, dragOptions)
+    }
+
+    @Test
+    fun `Verify that onDragStart has an effect if dragSource is this folder`() {
+        folder.itemsInvalidated = false
+        folder.isDragInProgress = false
+        folder.itemAddedBackToSelfViaIcon = true
+        folder.currentDragView = Mockito.mock(View::class.java)
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = spy(folderInfo)
+        val dragObject = DragObject(context)
+        dragObject.dragInfo = Mockito.mock(ItemInfo::class.java)
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        dragObject.dragSource = folder
+
+        folder.onDragStart(dragObject, DragOptions())
+
+        verify(folder.mContent, times(1)).removeItem(folder.currentDragView)
+        verify(folder.mInfo, times(1)).remove(dragObject.dragInfo, true)
+        assertTrue(folder.itemsInvalidated)
+        assertTrue(folder.isDragInProgress)
+        assertFalse(folder.itemAddedBackToSelfViaIcon)
+    }
+
+    @Test
+    fun `Verify that onDragStart has no effects if dragSource is not this folder`() {
+        folder.itemsInvalidated = false
+        folder.isDragInProgress = false
+        folder.itemAddedBackToSelfViaIcon = true
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val dragObject = DragObject(context)
+        dragObject.dragSource = Mockito.mock(DragSource::class.java)
+
+        folder.onDragStart(dragObject, DragOptions())
+
+        verify(folder.mContent, times(0)).removeItem(folder.currentDragView)
+        assertFalse(folder.itemsInvalidated)
+        assertFalse(folder.isDragInProgress)
+        assertTrue(folder.itemAddedBackToSelfViaIcon)
+    }
+
+    @Test
+    fun `Verify onDragEnd that we call completeDragExit and set drag in progress false`() {
+        doNothing().`when`(folder).completeDragExit()
+        folder.isExternalDrag = true
+        folder.isDragInProgress = true
+        folder.dragController = Mockito.mock(DragController::class.java)
+
+        folder.onDragEnd()
+
+        verify(folder, times(1)).completeDragExit()
+        verify(folder.dragController, times(1)).removeDragListener(folder)
+        assertFalse(folder.isDragInProgress)
+    }
+
+    @Test
+    fun `Verify onDragEnd that we do not call completeDragExit and set drag in progress false`() {
+        folder.isExternalDrag = false
+        folder.isDragInProgress = true
+        folder.dragController = Mockito.mock(DragController::class.java)
+
+        folder.onDragEnd()
+
+        verify(folder, times(0)).completeDragExit()
+        verify(folder.dragController, times(1)).removeDragListener(folder)
+        assertFalse(folder.isDragInProgress)
+    }
+
+    @Test
+    fun `startEditingFolderName should set hint to empty and showLabelSuggestions`() {
+        doNothing().`when`(folder).showLabelSuggestions()
+        folder.isEditingName = false
+        folder.folderName = FolderNameEditText(context)
+        folder.folderName.hint = "hello"
+
+        folder.startEditingFolderName()
+
+        verify(folder, times(1)).showLabelSuggestions()
+        assertEquals("", folder.folderName.hint)
+        assertTrue(folder.isEditingName)
+    }
+
+    @Test
+    fun `Ensure we set the title and hint correctly onBackKey when we have a new title`() {
+        val expectedHint = null
+        val expectedTitle = "hello"
+        folder.isEditingName = true
+        folder.folderName = spy(FolderNameEditText(context))
+        folder.folderName.setText(expectedTitle)
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = spy(folderInfo)
+        folder.mInfo.title = "world"
+        folder.mFolderIcon = Mockito.mock(FolderIcon::class.java)
+
+        folder.onBackKey()
+
+        assertEquals(expectedTitle, folder.mInfo.title)
+        verify(folder.mFolderIcon, times(1)).onTitleChanged(expectedTitle)
+        assertEquals(expectedHint, folder.folderName.hint)
+        assertFalse(folder.isEditingName)
+        verify(folder.folderName, times(1)).clearFocus()
+    }
+
+    @Test
+    fun `Ensure we set the title and hint correctly onBackKey when we do not have a new title`() {
+        val expectedHint = context.getString(R.string.folder_hint_text)
+        val expectedTitle = ""
+        folder.isEditingName = true
+        folder.folderName = spy(FolderNameEditText(context))
+        folder.folderName.setText(expectedTitle)
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = spy(folderInfo)
+        folder.mInfo.title = "world"
+        folder.mFolderIcon = Mockito.mock(FolderIcon::class.java)
+
+        folder.onBackKey()
+
+        assertEquals(expectedTitle, folder.mInfo.title)
+        verify(folder.mFolderIcon, times(1)).onTitleChanged(expectedTitle)
+        assertEquals(expectedHint, folder.folderName.hint)
+        assertFalse(folder.isEditingName)
+        verify(folder.folderName, times(1)).clearFocus()
+    }
+
+    @Test
+    fun `ensure onEditorAction calls dispatchBackKey when actionId is IME_ACTION_DONE`() {
+        folder.folderName = Mockito.mock(FolderNameEditText::class.java)
+
+        val result =
+            folder.onEditorAction(
+                Mockito.mock(TextView::class.java),
+                EditorInfo.IME_ACTION_DONE,
+                Mockito.mock(KeyEvent::class.java)
+            )
+
+        assertTrue(result)
+        verify(folder.folderName, times(1)).dispatchBackKey()
+    }
+
+    @Test
+    fun `ensure onEditorAction does not call dispatchBackKey when actionId is not IME_ACTION_DONE`() {
+        folder.folderName = Mockito.mock(FolderNameEditText::class.java)
+
+        val result =
+            folder.onEditorAction(
+                Mockito.mock(TextView::class.java),
+                EditorInfo.IME_ACTION_NONE,
+                Mockito.mock(KeyEvent::class.java)
+            )
+
+        assertFalse(result)
+        verify(folder.folderName, times(0)).dispatchBackKey()
+    }
+
+    @Test
+    fun `in completeDragExit we close the folder when mIsOpen`() {
+        doNothing().`when`(folder).close(true)
+        folder.setIsOpen(true)
+        folder.rearrangeOnClose = false
+
+        folder.completeDragExit()
+
+        verify(folder, times(1)).close(true)
+        assertTrue(folder.rearrangeOnClose)
+    }
+
+    @Test
+    fun `in completeDragExit we want to rearrange on close when it is animating`() {
+        folder.setIsOpen(false)
+        folder.rearrangeOnClose = false
+        folder.state = STATE_ANIMATING
+
+        folder.completeDragExit()
+
+        verify(folder, times(0)).close(true)
+        assertTrue(folder.rearrangeOnClose)
+    }
+
+    @Test
+    fun `in completeDragExit we want to call rearrangeChildren and clearDragInfo when not open and not animating`() {
+        doNothing().`when`(folder).rearrangeChildren()
+        doNothing().`when`(folder).clearDragInfo()
+        folder.setIsOpen(false)
+        folder.rearrangeOnClose = false
+        folder.state = STATE_CLOSED
+
+        folder.completeDragExit()
+
+        verify(folder, times(0)).close(true)
+        assertFalse(folder.rearrangeOnClose)
+        verify(folder, times(1)).rearrangeChildren()
+        verify(folder, times(1)).clearDragInfo()
+    }
+
+    @Test
+    fun `clearDragInfo should set current drag view to null and isExternalDrag to false`() {
+        folder.currentDragView = Mockito.mock(DragView::class.java)
+        folder.isExternalDrag = true
+
+        folder.clearDragInfo()
+
+        assertNull(folder.currentDragView)
+        assertFalse(folder.isExternalDrag)
+    }
+
+    @Test
+    fun `onDragExit should set alarm if drag is not complete`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        dragObject.dragComplete = false
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.onExitAlarm, times(1)).setOnAlarmListener(folder.mOnExitAlarmListener)
+        verify(folder.onExitAlarm, times(1)).setAlarm(ON_EXIT_CLOSE_DELAY.toLong())
+    }
+
+    @Test
+    fun `onDragExit should not set alarm if drag is complete`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        dragObject.dragComplete = true
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.onExitAlarm, times(0)).setOnAlarmListener(folder.mOnExitAlarmListener)
+        verify(folder.onExitAlarm, times(0)).setAlarm(ON_EXIT_CLOSE_DELAY.toLong())
+    }
+
+    @Test
+    fun `onDragExit should not clear scroll hint if already SCROLL_NONE`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        folder.scrollHintDir = SCROLL_NONE
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.mContent, times(0)).clearScrollHint()
+    }
+
+    @Test
+    fun `onDragExit should clear scroll hint if not SCROLL_NONE and then set scroll hint to scroll none`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        folder.scrollHintDir = SCROLL_LEFT
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.mContent, times(1)).clearScrollHint()
+        assertEquals(folder.scrollHintDir, SCROLL_NONE)
+    }
+
+    @Test
+    fun `onDragExit we should cancel reorder pause and hint alarms`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        folder.scrollHintDir = SCROLL_NONE
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+        folder.onScrollHintAlarm = Mockito.mock(Alarm::class.java)
+        folder.scrollPauseAlarm = Mockito.mock(Alarm::class.java)
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.reorderAlarm, times(1)).cancelAlarm()
+        verify(folder.onScrollHintAlarm, times(1)).cancelAlarm()
+        verify(folder.scrollPauseAlarm, times(1)).cancelAlarm()
+        assertEquals(folder.scrollHintDir, SCROLL_NONE)
+    }
+
+    @Test
+    fun `when calling prepareAccessibilityDrop we should cancel pending reorder alarm and call onAlarm`() {
+        folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+        folder.mReorderAlarmListener = Mockito.mock(OnAlarmListener::class.java)
+        `when`(folder.reorderAlarm.alarmPending()).thenReturn(true)
+
+        folder.prepareAccessibilityDrop()
+
+        verify(folder.reorderAlarm, times(1)).cancelAlarm()
+        verify(folder.mReorderAlarmListener, times(1)).onAlarm(folder.reorderAlarm)
+    }
+
+    @Test
+    fun `when calling prepareAccessibilityDrop we should not do anything if there is no pending alarm`() {
+        folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+        folder.mReorderAlarmListener = Mockito.mock(OnAlarmListener::class.java)
+        `when`(folder.reorderAlarm.alarmPending()).thenReturn(false)
+
+        folder.prepareAccessibilityDrop()
+
+        verify(folder.reorderAlarm, times(0)).cancelAlarm()
+        verify(folder.mReorderAlarmListener, times(0)).onAlarm(folder.reorderAlarm)
+    }
+
+    @Test
+    fun `isDropEnabled should be true as long as state is not STATE_ANIMATING`() {
+        folder.state = STATE_CLOSED
+
+        val isDropEnabled = folder.isDropEnabled
+
+        assertTrue(isDropEnabled)
+    }
+
+    @Test
+    fun `isDropEnabled should be false if state is STATE_ANIMATING`() {
+        folder.state = STATE_ANIMATING
+
+        val isDropEnabled = folder.isDropEnabled
+
+        assertFalse(isDropEnabled)
+    }
+
+    @Test
+    fun `getItemCount should return the number of items in the folder`() {
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = folderInfo
+
+        val itemCount = folder.itemCount
+
+        assertEquals(itemCount, 2)
+    }
+
+    @Test
+    fun `hideItem should set the visibility of the corresponding ItemInfo to invisible`() {
+        val itemInfo = ItemInfo()
+        val view = View(context)
+        view.isVisible = true
+        doReturn(view).whenever(folder).getViewForInfo(itemInfo)
+
+        folder.hideItem(itemInfo)
+
+        assertFalse(view.isVisible)
+    }
+
+    @Test
+    fun `showItem should set the visibility of the corresponding ItemInfo to visible`() {
+        val itemInfo = ItemInfo()
+        val view = View(context)
+        view.isVisible = false
+        doReturn(view).whenever(folder).getViewForInfo(itemInfo)
+
+        folder.showItem(itemInfo)
+
+        assertTrue(view.isVisible)
+    }
+
+    @Test
+    fun `onDragEnter should cancel exit alarm and set the scroll area offset to dragRegionWidth divided by two minus xOffset`() {
+        folder.mPrevTargetRank = 1
+        val dragObject = Mockito.mock(DragObject::class.java)
+        val dragView = Mockito.mock(DragView::class.java)
+        dragObject.dragView = dragView
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        `when`(dragObject.dragView.getDragRegionWidth()).thenReturn(100)
+        dragObject.xOffset = 20
+
+        folder.onDragEnter(dragObject)
+
+        verify(folder.onExitAlarm, times(1)).cancelAlarm()
+        assertEquals(-1, folder.mPrevTargetRank)
+        assertEquals(30, folder.scrollAreaOffset)
+    }
+
+    @Test
+    fun `acceptDrop should return true with the correct item type as a parameter`() {
+        val dragObject = Mockito.mock(DragObject::class.java)
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APP_PAIR
+        dragObject.dragInfo = itemInfo
+
+        val result = folder.acceptDrop(dragObject)
+
+        assertTrue(result)
+    }
+
+    @Test
+    fun `acceptDrop should return false with the incorrect item type as a parameter`() {
+        val dragObject = Mockito.mock(DragObject::class.java)
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APPWIDGET
+        dragObject.dragInfo = itemInfo
+
+        val result = folder.acceptDrop(dragObject)
+
+        assertFalse(result)
+    }
+
+    @Test
+    fun `rearrangeChildren should return early if content view are not bound`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.itemsInvalidated = false
+        doReturn(false).whenever(folder.mContent).areViewsBound()
+
+        folder.rearrangeChildren()
+
+        verify(folder.mContent, times(0)).arrangeChildren(folder.iconsInReadingOrder)
+        assertFalse(folder.itemsInvalidated)
+    }
+
+    @Test
+    fun `rearrangeChildren should call arrange children and invalidate items`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.itemsInvalidated = false
+        doReturn(true).whenever(folder.mContent).areViewsBound()
+        val iconsInReadingOrderList = ArrayList<View>()
+        `when`(folder.iconsInReadingOrder).thenReturn(iconsInReadingOrderList)
+        doNothing().`when`(folder.mContent).arrangeChildren(iconsInReadingOrderList)
+
+        folder.rearrangeChildren()
+
+        verify(folder.mContent, times(1)).arrangeChildren(folder.iconsInReadingOrder)
+        assertTrue(folder.itemsInvalidated)
+    }
+
+    @Test
+    fun `getItemCount should return the size of info getContents size`() {
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = folderInfo
+
+        val itemCount = folder.itemCount
+
+        assertEquals(2, itemCount)
+    }
+
+    @Test
+    fun `replaceFolderWithFinalItem should set mDestroyed to true if we replace folder with final item`() {
+        val launcherDelegate = Mockito.mock(LauncherDelegate::class.java)
+        folder.mLauncherDelegate = launcherDelegate
+        `when`(folder.mLauncherDelegate.replaceFolderWithFinalItem(folder)).thenReturn(true)
+
+        folder.replaceFolderWithFinalItem()
+
+        assertTrue(folder.isDestroyed)
+    }
+
+    @Test
+    fun `replaceFolderWithFinalItem should set mDestroyed to false if we do not replace folder with final item`() {
+        val launcherDelegate = Mockito.mock(LauncherDelegate::class.java)
+        folder.mLauncherDelegate = launcherDelegate
+        `when`(folder.mLauncherDelegate.replaceFolderWithFinalItem(folder)).thenReturn(false)
+
+        folder.replaceFolderWithFinalItem()
+
+        assertFalse(folder.isDestroyed)
+    }
+
+    @Test
+    fun `getContentAreaHeight should return maxContentAreaHeight`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredHeight).thenReturn(100)
+        `when`(folder.maxContentAreaHeight).thenReturn(50)
+
+        val height = folder.contentAreaHeight
+
+        assertEquals(50, height)
+    }
+
+    @Test
+    fun `getContentAreaHeight should return desiredHeight`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredHeight).thenReturn(50)
+        `when`(folder.maxContentAreaHeight).thenReturn(100)
+
+        val height = folder.contentAreaHeight
+
+        assertEquals(50, height)
+    }
+
+    @Test
+    fun `getContentAreaHeight should return MIN_CONTENT_DIMEN`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredHeight).thenReturn(1)
+        `when`(folder.maxContentAreaHeight).thenReturn(2)
+
+        val height = folder.contentAreaHeight
+
+        assertEquals(MIN_CONTENT_DIMEN, height)
+    }
+
+    @Test
+    fun `getContentAreaWidth should return desired width`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredWidth).thenReturn(50)
+
+        val width = folder.contentAreaWidth
+
+        assertEquals(50, width)
+    }
+
+    @Test
+    fun `getContentAreaWidth should return MIN_CONTENT_DIMEN`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredWidth).thenReturn(1)
+
+        val width = folder.contentAreaWidth
+
+        assertEquals(MIN_CONTENT_DIMEN, width)
+    }
+
+    @Test
+    fun `getFolderWidth should return padding left plus padding right plus desired width`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredWidth).thenReturn(1)
+        `when`(folder.paddingLeft).thenReturn(10)
+        `when`(folder.paddingRight).thenReturn(10)
+
+        val width = folder.folderWidth
+
+        assertEquals(21, width)
+    }
+
+    @Test
+    fun `getFolderHeight with no params should return getFolderHeight`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.contentAreaHeight).thenReturn(100)
+        `when`(folder.getFolderHeight(folder.contentAreaHeight)).thenReturn(120)
+
+        val height = folder.folderHeight
+
+        assertEquals(120, height)
+    }
+
+    @Test
+    fun `getFolderWidth with contentAreaHeight should return padding top plus padding bottom plus contentAreaHeight plus footer height`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.footerHeight).thenReturn(100)
+        `when`(folder.paddingTop).thenReturn(10)
+        `when`(folder.paddingBottom).thenReturn(10)
+
+        val height = folder.getFolderHeight(100)
+
+        assertEquals(220, height)
+    }
+
+    @Test
+    fun `onRemove should call removeItem with the correct views`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(2).whenever(folder).itemCount
+
+        folder.onRemove(items)
+
+        verify(folder.mContent, times(1)).removeItem(view1)
+        verify(folder.mContent, times(1)).removeItem(view2)
+    }
+
+    @Test
+    fun `onRemove should set mRearrangeOnClose to true and not call rearrangeChildren if animating`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.state = STATE_ANIMATING
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(2).whenever(folder).itemCount
+
+        folder.onRemove(items)
+
+        assertTrue(folder.rearrangeOnClose)
+        verify(folder, times(0)).rearrangeChildren()
+    }
+
+    @Test
+    fun `onRemove should set not change mRearrangeOnClose and not call rearrangeChildren if not animating`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.state = STATE_CLOSED
+        folder.rearrangeOnClose = false
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(2).whenever(folder).itemCount
+
+        folder.onRemove(items)
+
+        assertFalse(folder.rearrangeOnClose)
+        verify(folder, times(1)).rearrangeChildren()
+    }
+
+    @Test
+    fun `onRemove should call close if mIsOpen is true and item count is less than or equal to one`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(1).whenever(folder).itemCount
+        folder.setIsOpen(true)
+        doNothing().`when`(folder).close(true)
+
+        folder.onRemove(items)
+
+        verify(folder, times(1)).close(true)
+    }
+
+    @Test
+    fun `onRemove should call replaceFolderWithFinalItem if mIsOpen is false and item count is less than or equal to one`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(1).whenever(folder).itemCount
+        folder.setIsOpen(false)
+
+        folder.onRemove(items)
+
+        verify(folder, times(1)).replaceFolderWithFinalItem()
+    }
+
     companion object {
         const val TWO_ICON_FOLDER_TYPE = 'A'
     }
diff --git a/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt b/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
index da14425..5516f45 100644
--- a/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
+++ b/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
@@ -23,7 +23,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
-import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.THEMED_ICONS
 import com.android.launcher3.LauncherPrefs.Companion.get
 import com.android.launcher3.icons.BaseIconFactory
 import com.android.launcher3.icons.FastBitmapDrawable
@@ -51,6 +51,8 @@
     private lateinit var modelHelper: LauncherModelHelper
     private lateinit var folderIcon: FolderIcon
 
+    private var defaultThemedIcons = false
+
     @Before
     fun setup() {
         getInstrumentation().runOnMainSync {
@@ -127,16 +129,20 @@
                     previewItemManager.mIconSize
                 )
             )
+
+        defaultThemedIcons = get(context).get(THEMED_ICONS)
     }
+
     @After
     @Throws(Exception::class)
     fun tearDown() {
+        get(context).put(THEMED_ICONS, defaultThemedIcons)
         modelHelper.destroy()
     }
 
     @Test
     fun checkThemedIconWithThemingOn_iconShouldBeThemed() {
-        get(context).put(LauncherPrefs.THEMED_ICONS, true)
+        get(context).put(THEMED_ICONS, true)
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[0])
@@ -146,7 +152,7 @@
 
     @Test
     fun checkThemedIconWithThemingOff_iconShouldNotBeThemed() {
-        get(context).put(LauncherPrefs.THEMED_ICONS, false)
+        get(context).put(THEMED_ICONS, false)
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[0])
@@ -156,7 +162,7 @@
 
     @Test
     fun checkUnthemedIconWithThemingOn_iconShouldNotBeThemed() {
-        get(context).put(LauncherPrefs.THEMED_ICONS, true)
+        get(context).put(THEMED_ICONS, true)
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[1])
@@ -166,7 +172,7 @@
 
     @Test
     fun checkUnthemedIconWithThemingOff_iconShouldNotBeThemed() {
-        get(context).put(LauncherPrefs.THEMED_ICONS, false)
+        get(context).put(THEMED_ICONS, false)
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[1])
@@ -176,7 +182,7 @@
 
     @Test
     fun checkThemedIconWithBadgeWithThemingOn_iconAndBadgeShouldBeThemed() {
-        get(context).put(LauncherPrefs.THEMED_ICONS, true)
+        get(context).put(THEMED_ICONS, true)
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[2])
@@ -189,7 +195,7 @@
 
     @Test
     fun checkUnthemedIconWithBadgeWithThemingOn_badgeShouldBeThemed() {
-        get(context).put(LauncherPrefs.THEMED_ICONS, true)
+        get(context).put(THEMED_ICONS, true)
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[3])
@@ -202,7 +208,7 @@
 
     @Test
     fun checkUnthemedIconWithBadgeWithThemingOff_iconAndBadgeShouldNotBeThemed() {
-        get(context).put(LauncherPrefs.THEMED_ICONS, false)
+        get(context).put(THEMED_ICONS, false)
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[3])
diff --git a/tests/src/com/android/launcher3/model/PackageUpdatedTaskTest.kt b/tests/src/com/android/launcher3/model/PackageUpdatedTaskTest.kt
new file mode 100644
index 0000000..d9af07a
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/PackageUpdatedTaskTest.kt
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model
+
+import android.content.ComponentName
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.LauncherActivityInfo
+import android.content.pm.LauncherApps
+import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.AppFilter
+import com.android.launcher3.Flags.FLAG_ENABLE_PRIVATE_SPACE
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.PackageUpdatedTask.OP_ADD
+import com.android.launcher3.model.PackageUpdatedTask.OP_REMOVE
+import com.android.launcher3.model.PackageUpdatedTask.OP_SUSPEND
+import com.android.launcher3.model.PackageUpdatedTask.OP_UNAVAILABLE
+import com.android.launcher3.model.PackageUpdatedTask.OP_UNSUSPEND
+import com.android.launcher3.model.PackageUpdatedTask.OP_UPDATE
+import com.android.launcher3.model.PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE
+import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import com.android.launcher3.util.TestUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class PackageUpdatedTaskTest {
+
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
+    private val mUser = UserHandle(0)
+    private val mDataModel: BgDataModel = BgDataModel()
+    private val mLauncherModelHelper = LauncherModelHelper()
+    private val mContext: SandboxModelContext = spy(mLauncherModelHelper.sandboxContext)
+    private val mAppState: LauncherAppState = spy(LauncherAppState.getInstance(mContext))
+
+    private val expectedPackage = "Test.Package"
+    private val expectedComponent = ComponentName(expectedPackage, "TestClass")
+    private val expectedActivityInfo: LauncherActivityInfo = mock<LauncherActivityInfo>()
+    private val expectedWorkspaceItem = spy(WorkspaceItemInfo())
+
+    private val mockIconCache: IconCache = mock()
+    private val mockTaskController: ModelTaskController = mock<ModelTaskController>()
+    private val mockAppFilter: AppFilter = mock<AppFilter>()
+    private val mockApplicationInfo: ApplicationInfo = mock<ApplicationInfo>()
+    private val mockActivityInfo: ActivityInfo = mock<ActivityInfo>()
+
+    private lateinit var mAllAppsList: AllAppsList
+
+    @Before
+    fun setup() {
+        mAllAppsList = spy(AllAppsList(mockIconCache, mockAppFilter))
+        mLauncherModelHelper.sandboxContext.spyService(LauncherApps::class.java).apply {
+            whenever(getActivityList(expectedPackage, mUser))
+                .thenReturn(listOf(expectedActivityInfo))
+        }
+        whenever(mAppState.iconCache).thenReturn(mockIconCache)
+        whenever(mockTaskController.app).thenReturn(mAppState)
+        whenever(mockAppFilter.shouldShowApp(expectedComponent)).thenReturn(true)
+        mockApplicationInfo.apply {
+            uid = 1
+            isArchived = false
+        }
+        mockActivityInfo.isArchived = false
+        expectedActivityInfo.apply {
+            whenever(applicationInfo).thenReturn(mockApplicationInfo)
+            whenever(activityInfo).thenReturn(mockActivityInfo)
+            whenever(componentName).thenReturn(expectedComponent)
+        }
+        expectedWorkspaceItem.apply {
+            itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+            container = LauncherSettings.Favorites.CONTAINER_DESKTOP
+            user = mUser
+            whenever(targetPackage).thenReturn(expectedPackage)
+            whenever(targetComponent).thenReturn(expectedComponent)
+        }
+    }
+
+    @After
+    fun tearDown() {
+        mLauncherModelHelper.destroy()
+    }
+
+    @Test
+    fun `OP_ADD triggers model callbacks and adds new items to AllAppsList`() {
+        // Given
+        val taskUnderTest = PackageUpdatedTask(OP_ADD, mUser, expectedPackage)
+        // When
+        mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
+        }
+        mLauncherModelHelper.loadModelSync()
+        // Then
+        verify(mockIconCache).updateIconsForPkg(expectedPackage, mUser)
+        verify(mAllAppsList).addPackage(mContext, expectedPackage, mUser)
+        verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWorkspaceItem))
+        verify(mockTaskController).bindUpdatedWidgets(mDataModel)
+        assertThat(mAllAppsList.data.firstOrNull()?.componentName)
+            .isEqualTo(AppInfo(mContext, expectedActivityInfo, mUser).componentName)
+    }
+
+    @Test
+    fun `OP_UPDATE triggers model callbacks and updates items in AllAppsList`() {
+        // Given
+        val taskUnderTest = PackageUpdatedTask(OP_UPDATE, mUser, expectedPackage)
+        // When
+        mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
+        }
+        mLauncherModelHelper.loadModelSync()
+        // Then
+        verify(mockIconCache).updateIconsForPkg(expectedPackage, mUser)
+        verify(mAllAppsList).updatePackage(mContext, expectedPackage, mUser)
+        verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWorkspaceItem))
+        assertThat(mAllAppsList.data.firstOrNull()?.componentName)
+            .isEqualTo(AppInfo(mContext, expectedActivityInfo, mUser).componentName)
+    }
+
+    @Test
+    fun `OP_REMOVE triggers model callbacks and removes packages and icons`() {
+        // Given
+        val taskUnderTest = PackageUpdatedTask(OP_REMOVE, mUser, expectedPackage)
+        // When
+        mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
+        }
+        mLauncherModelHelper.loadModelSync()
+        // Then
+        verify(mockIconCache).removeIconsForPkg(expectedPackage, mUser)
+        verify(mAllAppsList).removePackage(expectedPackage, mUser)
+        verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWorkspaceItem))
+        assertThat(mAllAppsList.data).isEmpty()
+    }
+
+    @Test
+    fun `OP_UNAVAILABLE triggers model callbacks and removes package from AllAppsList`() {
+        // Given
+        val taskUnderTest = PackageUpdatedTask(OP_UNAVAILABLE, mUser, expectedPackage)
+        // When
+        mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
+        }
+        mLauncherModelHelper.loadModelSync()
+        // Then
+        verify(mAllAppsList).removePackage(expectedPackage, mUser)
+        verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWorkspaceItem))
+        assertThat(mAllAppsList.data).isEmpty()
+    }
+
+    @Test
+    fun `OP_SUSPEND triggers model callbacks and updates flags in AllAppsList`() {
+        // Given
+        val taskUnderTest = PackageUpdatedTask(OP_SUSPEND, mUser, expectedPackage)
+        // When
+        mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        mAllAppsList.add(AppInfo(mContext, expectedActivityInfo, mUser), expectedActivityInfo)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
+        }
+        mLauncherModelHelper.loadModelSync()
+        // Then
+        verify(mAllAppsList).updateDisabledFlags(any(), any())
+        verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWorkspaceItem))
+        assertThat(mAllAppsList.getAndResetChangeFlag()).isTrue()
+    }
+
+    @Test
+    fun `OP_UNSUSPEND triggers no callbacks when app not suspended`() {
+        // Given
+        val taskUnderTest = PackageUpdatedTask(OP_UNSUSPEND, mUser, expectedPackage)
+        // When
+        mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
+        }
+        mLauncherModelHelper.loadModelSync()
+        // Then
+        verify(mAllAppsList).updateDisabledFlags(any(), any())
+        verify(mockTaskController).bindUpdatedWorkspaceItems(emptyList())
+        assertThat(mAllAppsList.getAndResetChangeFlag()).isFalse()
+    }
+
+    @EnableFlags(FLAG_ENABLE_PRIVATE_SPACE)
+    @Test
+    fun `OP_USER_AVAILABILITY_CHANGE triggers no callbacks if current user not work or private`() {
+        // Given
+        val taskUnderTest = PackageUpdatedTask(OP_USER_AVAILABILITY_CHANGE, mUser, expectedPackage)
+        // When
+        mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
+        }
+        mLauncherModelHelper.loadModelSync()
+        // Then
+        verify(mAllAppsList).updateDisabledFlags(any(), any())
+        verify(mockTaskController).bindUpdatedWorkspaceItems(emptyList())
+        assertThat(mAllAppsList.data).isEmpty()
+    }
+}
diff --git a/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt b/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt
new file mode 100644
index 0000000..9a8f957
--- /dev/null
+++ b/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.pageindicators
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.util.ActivityContextWrapper
+import junit.framework.TestCase.assertEquals
+import org.junit.Test
+import org.mockito.Mockito
+
+class PageIndicatorDotsTest {
+
+    private val context: Context =
+        ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+    private val pageIndicatorDots: PageIndicatorDots = Mockito.spy(PageIndicatorDots(context))
+
+    @Test
+    fun `setActiveMarker should set the active page to the parameter passed`() {
+        pageIndicatorDots.setActiveMarker(2)
+
+        assertEquals(2, pageIndicatorDots.activePage)
+    }
+
+    @Test
+    fun `setActiveMarker should set the active page to the parameter passed divided by two in two panel layouts`() {
+        pageIndicatorDots.mIsTwoPanels = true
+
+        pageIndicatorDots.setActiveMarker(5)
+
+        assertEquals(2, pageIndicatorDots.activePage)
+    }
+
+    @Test
+    fun `setMarkersCount should set the number of pages to the passed parameter and if the last page gets removed we want to go to the previous page`() {
+        pageIndicatorDots.setMarkersCount(3)
+
+        assertEquals(3, pageIndicatorDots.numPages)
+    }
+
+    @Test
+    fun `for setMarkersCount if the last page gets removed we want to go to the previous page`() {
+        pageIndicatorDots.setActiveMarker(2)
+
+        pageIndicatorDots.setMarkersCount(2)
+
+        assertEquals(1, pageIndicatorDots.activePage)
+        assertEquals(pageIndicatorDots.activePage.toFloat(), pageIndicatorDots.currentPosition)
+    }
+}
diff --git a/tests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt b/tests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
new file mode 100644
index 0000000..b531adb
--- /dev/null
+++ b/tests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.pm
+
+import android.content.pm.LauncherApps
+import android.content.pm.PackageInstaller
+import android.os.Build
+import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.PackageUserKey
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InstallSessionTrackerTest {
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
+    private val mockInstallSessionHelper: InstallSessionHelper = mock()
+    private val mockCallback: InstallSessionTracker.Callback = mock()
+    private val mockPackageInstaller: PackageInstaller = mock()
+
+    private val launcherModelHelper = LauncherModelHelper()
+    private val sandboxContext = launcherModelHelper.sandboxContext
+
+    lateinit var launcherApps: LauncherApps
+    lateinit var installSessionTracker: InstallSessionTracker
+
+    @Before
+    fun setup() {
+        launcherApps = sandboxContext.spyService(LauncherApps::class.java)
+        installSessionTracker =
+            InstallSessionTracker(
+                mockInstallSessionHelper,
+                mockCallback,
+                mockPackageInstaller,
+                launcherApps
+            )
+    }
+
+    @After
+    fun teardown() {
+        launcherModelHelper.destroy()
+    }
+
+    @Test
+    fun `onCreated triggers callbacks for setting up new install session`() {
+        // Given
+        val expectedSessionId = 1
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = expectedSessionId
+                appPackageName = "appPackageName"
+                userId = 0
+            }
+        val expectedPackageKey = PackageUserKey("appPackageName", UserHandle(0))
+        whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+            .thenReturn(expectedSession)
+        // When
+        installSessionTracker.onCreated(expectedSessionId)
+        // Then
+        verify(mockCallback).onInstallSessionCreated(any())
+        verify(mockCallback).onUpdateSessionDisplay(expectedPackageKey, expectedSession)
+        verify(mockInstallSessionHelper).tryQueuePromiseAppIcon(expectedSession)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING)
+    fun `onCreated for unarchival triggers onPackageStateChanged`() {
+        // Given
+        val expectedSessionId = 1
+        val expectedSession =
+            spy(PackageInstaller.SessionInfo()).apply {
+                sessionId = expectedSessionId
+                appPackageName = "appPackageName"
+                userId = 0
+                whenever(isUnarchival).thenReturn(true)
+            }
+        whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+            .thenReturn(expectedSession)
+        // When
+        installSessionTracker.onCreated(expectedSessionId)
+        // Then
+        verify(mockCallback).onPackageStateChanged(any())
+    }
+
+    @Test
+    fun `onFinished triggers onPackageStateChanged if session found in cache`() {
+        // Given
+        val expectedSessionId = 1
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = expectedSessionId
+                appPackageName = "appPackageName"
+                userId = 0
+            }
+        val expectedPackageKey = PackageUserKey("appPackageName", UserHandle(0))
+        whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+            .thenReturn(expectedSession)
+        whenever(mockInstallSessionHelper.activeSessions)
+            .thenReturn(hashMapOf(expectedPackageKey to expectedSession))
+        // When
+        installSessionTracker.onFinished(expectedSessionId, /* success */ true)
+        // Then
+        verify(mockCallback).onPackageStateChanged(any())
+    }
+
+    @Test
+    fun `onFinished failure calls onSessionFailure and promise icon removal for existing icon`() {
+        // Given
+        val expectedSessionId = 1
+        val expectedPackage = "appPackageName"
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = expectedSessionId
+                appPackageName = expectedPackage
+                userId = 0
+            }
+        val expectedPackageKey = PackageUserKey(expectedPackage, UserHandle(0))
+        whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+            .thenReturn(expectedSession)
+        whenever(mockInstallSessionHelper.activeSessions)
+            .thenReturn(hashMapOf(expectedPackageKey to expectedSession))
+        whenever(mockInstallSessionHelper.promiseIconAddedForId(expectedSessionId)).thenReturn(true)
+        // When
+        installSessionTracker.onFinished(expectedSessionId, /* success */ false)
+        // Then
+        verify(mockCallback).onSessionFailure(expectedPackage, expectedPackageKey.mUser)
+        verify(mockInstallSessionHelper).removePromiseIconId(expectedSessionId)
+    }
+
+    @Test
+    fun `onProgressChanged triggers onPackageStateChanged if verified session found`() {
+        // Given
+        val expectedSessionId = 1
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = expectedSessionId
+                appPackageName = "appPackageName"
+                userId = 0
+            }
+        whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+            .thenReturn(expectedSession)
+        // When
+        installSessionTracker.onProgressChanged(expectedSessionId, /* progress */ 50f)
+        // Then
+        verify(mockCallback).onPackageStateChanged(any())
+    }
+
+    @Test
+    fun `onBadgingChanged triggers session display update and queues promise icon if verified`() {
+        // Given
+        val expectedSessionId = 1
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = expectedSessionId
+                appPackageName = "appPackageName"
+                userId = 0
+            }
+        val expectedPackageKey = PackageUserKey("appPackageName", UserHandle(0))
+        whenever(mockInstallSessionHelper.getVerifiedSessionInfo(expectedSessionId))
+            .thenReturn(expectedSession)
+        // When
+        installSessionTracker.onBadgingChanged(expectedSessionId)
+        // Then
+        verify(mockCallback).onUpdateSessionDisplay(expectedPackageKey, expectedSession)
+        verify(mockInstallSessionHelper).tryQueuePromiseAppIcon(expectedSession)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    fun `register triggers registerPackageInstallerSessionCallback for versions from Q`() {
+        // Given
+        whenever(
+                launcherApps.registerPackageInstallerSessionCallback(
+                    MODEL_EXECUTOR,
+                    installSessionTracker
+                )
+            )
+            .then { /* no-op */ }
+        // When
+        installSessionTracker.register()
+        // Then
+        verify(launcherApps)
+            .registerPackageInstallerSessionCallback(MODEL_EXECUTOR, installSessionTracker)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    fun `unregister triggers unregisterPackageInstallerSessionCallback for versions from Q`() {
+        // Given
+        whenever(launcherApps.unregisterPackageInstallerSessionCallback(installSessionTracker))
+            .then { /* no-op */ }
+        // When
+        installSessionTracker.unregister()
+        // Then
+        verify(launcherApps).unregisterPackageInstallerSessionCallback(installSessionTracker)
+    }
+}
diff --git a/tests/src/com/android/launcher3/pm/UserCacheTest.kt b/tests/src/com/android/launcher3/pm/UserCacheTest.kt
new file mode 100644
index 0000000..b21219e
--- /dev/null
+++ b/tests/src/com/android/launcher3/pm/UserCacheTest.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.pm
+
+import android.os.Process.myUserHandle
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.TestUtil
+import com.android.launcher3.util.UserIconInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class UserCacheTest {
+    private val launcherModelHelper = LauncherModelHelper()
+    private val sandboxContext = launcherModelHelper.sandboxContext
+    private lateinit var userCache: UserCache
+
+    @Before
+    fun setup() {
+        userCache = UserCache.getInstance(sandboxContext)
+    }
+
+    @After
+    fun teardown() {
+        launcherModelHelper.destroy()
+    }
+
+    @Test
+    fun `getBadgeDrawable only returns a UserBadgeDrawable given a user in the cache`() {
+        // Given
+        val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_WORK)
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            userCache.putToCache(myUserHandle(), expectedIconInfo)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        // When
+        val actualDrawable = UserCache.getBadgeDrawable(sandboxContext, myUserHandle())
+        val unexpectedDrawable = UserCache.getBadgeDrawable(sandboxContext, UserHandle(66))
+        // Then
+        assertThat(actualDrawable).isNotNull()
+        assertThat(unexpectedDrawable).isNull()
+    }
+
+    @Test
+    fun `getPreInstallApps returns list of pre installed apps given a user`() {
+        // Given
+        val expectedApps = listOf("Google")
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            userCache.putToPreInstallCache(myUserHandle(), expectedApps)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        // When
+        val actualApps = userCache.getPreInstallApps(myUserHandle())
+        // Then
+        assertThat(actualApps).isEqualTo(expectedApps)
+    }
+
+    @Test
+    fun `getUserProfiles returns copy of UserCache profiles`() {
+        // Given
+        val expectedProfiles = listOf(myUserHandle())
+        val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_MAIN)
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            userCache.putToCache(myUserHandle(), expectedIconInfo)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        // When
+        val actualProfiles = userCache.userProfiles
+        // Then
+        assertThat(actualProfiles).isEqualTo(expectedProfiles)
+    }
+
+    @Test
+    fun `getUserForSerialNumber returns user key matching given entry serial number`() {
+        // Given
+        val expectedSerial = 42L
+        val expectedProfile = UserHandle(42)
+        val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_MAIN, expectedSerial)
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            userCache.putToCache(expectedProfile, expectedIconInfo)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        // When
+        val actualProfile = userCache.getUserForSerialNumber(expectedSerial)
+        // Then
+        assertThat(actualProfile).isEqualTo(expectedProfile)
+    }
+
+    @Test
+    fun `getUserInfo returns cached UserIconInfo given user key`() {
+        // Given
+        val expectedProfile = UserHandle(1)
+        val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_WORK)
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            userCache.putToCache(expectedProfile, expectedIconInfo)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        // When
+        val actualIconInfo = userCache.getUserInfo(expectedProfile)
+        // Then
+        assertThat(actualIconInfo).isEqualTo(expectedIconInfo)
+    }
+
+    @Test
+    fun `getSerialNumberForUser returns cached UserIconInfo serial number given user key`() {
+        // Given
+        val expectedSerial = 42L
+        val expectedProfile = UserHandle(1)
+        val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_WORK, expectedSerial)
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            userCache.putToCache(expectedProfile, expectedIconInfo)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        // When
+        val actualSerial = userCache.getSerialNumberForUser(expectedProfile)
+        // Then
+        assertThat(actualSerial).isEqualTo(expectedSerial)
+    }
+}
diff --git a/tests/src/com/android/launcher3/popup/SystemShortcutTest.java b/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
index 98b6b4b..dcfcad5 100644
--- a/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
+++ b/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
@@ -63,6 +63,8 @@
 import com.android.launcher3.util.TestSandboxModelContextWrapper;
 import com.android.launcher3.util.UserIconInfo;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
+import com.android.launcher3.widget.picker.model.data.WidgetPickerData;
 
 import org.junit.After;
 import org.junit.Assert;
@@ -73,8 +75,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.ArrayList;
-
 @SmallTest
 @RunWith(LauncherMultivalentJUnit.class)
 public class SystemShortcutTest {
@@ -86,7 +86,7 @@
     private TestSandboxModelContextWrapper mTestContext;
     private final SandboxModelContext mSandboxContext = new SandboxModelContext();
     private PrivateProfileManager mPrivateProfileManager;
-    private PopupDataProvider mPopupDataProvider;
+    private WidgetPickerDataProvider mWidgetPickerDataProvider;
     private AppInfo mAppInfo;
     @Mock UserCache mUserCache;
     @Mock ApiWrapper mApiWrapper;
@@ -119,8 +119,8 @@
         spyOn(mPrivateProfileManager);
         when(mPrivateProfileManager.getProfileUser()).thenReturn(PRIVATE_HANDLE);
 
-        mPopupDataProvider = mTestContext.getPopupDataProvider();
-        spyOn(mPopupDataProvider);
+        mWidgetPickerDataProvider = mTestContext.getWidgetPickerDataProvider();
+        spyOn(mWidgetPickerDataProvider);
     }
 
     @After
@@ -141,7 +141,7 @@
         mAppInfo = new AppInfo();
         mAppInfo.componentName = new ComponentName(mTestContext, getClass());
         assertNotNull(mAppInfo.getTargetComponent());
-        doReturn(new ArrayList<>()).when(mPopupDataProvider).getWidgetsForPackageUser(any());
+        doReturn(new WidgetPickerData()).when(mWidgetPickerDataProvider).get();
         spyOn(mAppInfo);
         SystemShortcut systemShortcut = SystemShortcut.WIDGETS
                 .getShortcut(mTestContext, mAppInfo, mView);
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 3d253b4..c926ba9 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -245,7 +245,7 @@
                 Intent.ACTION_PACKAGE_RESTARTED, Intent.ACTION_PACKAGE_DATA_CLEARED);
 
         mDevice.executeShellCommand("pm clear " + pkg);
-        assertTrue(pkg + " didn't restart", count.await(10, TimeUnit.SECONDS));
+        assertTrue(pkg + " didn't restart", count.await(20, TimeUnit.SECONDS));
         mTargetContext.unregisterReceiver(broadcastReceiver);
     }
 
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
index eb05000..20c5a25 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
@@ -20,8 +20,6 @@
 import static com.android.launcher3.util.TestConstants.AppNames.MAPS_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.MESSAGES_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.STORE_APP_NAME;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -40,7 +38,6 @@
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.rule.ScreenRecordRule;
-import com.android.launcher3.util.rule.TestStabilityRule;
 
 import org.junit.After;
 import org.junit.Before;
@@ -288,6 +285,7 @@
 
     @Test
     @PortraitLandscape
+    @ScreenRecordRule.ScreenRecord // b/330232490
     public void testEmptyPagesGetRemovedIfBothPagesAreEmpty() {
         Workspace workspace = mLauncher.getWorkspace();
 
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index f02a0c2..ad37f7b 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -98,6 +98,7 @@
 import java.util.function.BooleanSupplier;
 import java.util.function.Function;
 import java.util.function.Supplier;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -455,10 +456,6 @@
         getTestInfo(TestProtocol.REQUEST_ENABLE_ROTATION, Boolean.toString(on));
     }
 
-    public void setEnableSuggestion(boolean enableSuggestion) {
-        getTestInfo(TestProtocol.REQUEST_ENABLE_SUGGESTION, Boolean.toString(enableSuggestion));
-    }
-
     public boolean hadNontestEvents() {
         return getTestInfo(TestProtocol.REQUEST_GET_HAD_NONTEST_EVENTS)
                 .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
@@ -2402,6 +2399,16 @@
         disableSensorRotation();
         final Integer initialPid = getPid();
         final LogEventChecker eventChecker = new LogEventChecker(this);
+        eventChecker.setLogExclusionRule(event -> {
+            Matcher matcher = Pattern.compile("KeyEvent.*flags=0x([0-9a-fA-F]+)").matcher(event);
+            if (matcher.find()) {
+                int keyEventFlags = Integer.parseInt(matcher.group(1), 16);
+                // ignore KeyEvents with FLAG_CANCELED
+                return (keyEventFlags & KeyEvent.FLAG_CANCELED) != 0;
+            }
+            return false;
+        });
+
         if (eventChecker.start()) mEventChecker = eventChecker;
 
         return () -> {
@@ -2459,7 +2466,8 @@
     }
 
     float getWindowCornerRadius() {
-        // TODO(b/197326121): Check if the touch is overlapping with the corners by offsetting
+        // Return a larger corner radius to ensure gesture calculated from the radius are offset to
+        // prevent overlapping
         final float tmpBuffer = 100f;
         final Resources resources = getResources();
         if (!supportsRoundedCornersOnWindows(resources)) {
diff --git a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
index 055a357..3a49160 100644
--- a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
+++ b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
@@ -35,6 +35,8 @@
     // Map from an event sequence name to an ordered list of expected events in that sequence.
     private final ListMap<Pattern> mExpectedEvents = new ListMap<>();
 
+    private LogExclusionRule mLogExclusionRule = null;
+
     LogEventChecker(LauncherInstrumentation launcher) {
         mLauncher = launcher;
     }
@@ -48,6 +50,10 @@
         mExpectedEvents.add(sequence, pattern);
     }
 
+    void setLogExclusionRule(LogExclusionRule logExclusionRule) {
+        mLogExclusionRule = logExclusionRule;
+    }
+
     // Waits for the expected number of events and returns them.
     private ListMap<String> finishSync(long waitForExpectedCountMs) {
         final long startTime = SystemClock.uptimeMillis();
@@ -74,7 +80,9 @@
         final ListMap<String> eventSequences = new ListMap<>();
         for (String rawEvent : rawEvents) {
             final String[] split = rawEvent.split("/");
-            eventSequences.add(split[0], split[1]);
+            if (mLogExclusionRule == null || !mLogExclusionRule.shouldExclude(split[1])) {
+                eventSequences.add(split[0], split[1]);
+            }
         }
         return eventSequences;
     }
@@ -175,4 +183,8 @@
             return list;
         }
     }
+
+    interface LogExclusionRule {
+        boolean shouldExclude(String event);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 9ac6768..748d576 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -345,17 +345,34 @@
      * @return map of text -> center of the view. In case of icons with the same name, the one with
      * lower x coordinate is selected.
      */
-    public Map<String, Point> getWorkspaceIconsPositions() {
+    public Map<String, Point> getAllWorkspaceIconsPositions() {
         final UiObject2 workspace = verifyActiveContainer();
-        mLauncher.waitForLauncherInitialized(); // b/319501259
         List<UiObject2> workspaceIcons =
                 mLauncher.waitForObjectsInContainer(workspace, AppIcon.getAnyAppIconSelector());
-        return workspaceIcons.stream()
+        return getIconPositionMap(workspaceIcons);
+    }
+
+    /**
+     * @return point where icon is found for given the app name,
+     * point is visible center of the icon.
+     */
+    @NonNull
+    public Point getWorkspaceIconPosition(String appName) {
+        final UiObject2 workspace = verifyActiveContainer();
+
+        UiObject2 workspaceIcon =
+                mLauncher.waitForObjectInContainer(workspace,
+                        AppIcon.getAppIconSelector(appName, mLauncher));
+        return workspaceIcon.getVisibleCenter();
+    }
+
+    private Map<String, Point> getIconPositionMap(List<UiObject2> icons) {
+        return icons.stream()
                 .collect(
                         Collectors.toMap(
                                 /* keyMapper= */ uiObject21 -> {
-                                    Log.d(UIOBJECT_STALE_ELEMENT, "keyText: " +
-                                            uiObject21.getText());
+                                    Log.d(UIOBJECT_STALE_ELEMENT, "keyText: "
+                                            + uiObject21.getText());
                                     return uiObject21.getText();
                                 },
                                 /* valueMapper= */ uiObject2 -> {