Merge "Fix back button sometimes not showing up in the initial setup screen" into main
diff --git a/go/quickstep/res/layout/overview_actions_container.xml b/go/quickstep/res/layout/overview_actions_container.xml
index df09124..b1a6202 100644
--- a/go/quickstep/res/layout/overview_actions_container.xml
+++ b/go/quickstep/res/layout/overview_actions_container.xml
@@ -121,6 +121,17 @@
             android:layout_weight="1"
             android:visibility="gone" />
 
+    </LinearLayout>
+
+    <!-- Unused. Included only for compatibility with parent class. -->
+    <LinearLayout
+        android:id="@+id/group_action_buttons"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/overview_actions_height"
+        android:layout_gravity="top|center_horizontal"
+        android:orientation="horizontal"
+        android:visibility="gone">
+
         <Button
             android:id="@+id/action_save_app_pair"
             style="@style/GoOverviewActionButton"
@@ -128,8 +139,8 @@
             android:layout_height="wrap_content"
             android:drawableStart="@drawable/ic_save_app_pair_up_down"
             android:text="@string/action_save_app_pair"
-            android:theme="@style/ThemeControlHighlightWorkspaceColor"
-            android:visibility="gone" />
+            android:theme="@style/ThemeControlHighlightWorkspaceColor" />
+
     </LinearLayout>
 
 </com.android.quickstep.views.GoOverviewActionsView>
\ No newline at end of file
diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml
index d086da4..7aaf744 100644
--- a/quickstep/res/layout/overview_actions_container.xml
+++ b/quickstep/res/layout/overview_actions_container.xml
@@ -45,14 +45,24 @@
             android:theme="@style/ThemeControlHighlightWorkspaceColor"
             android:visibility="gone" />
 
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/group_action_buttons"
+        android:layout_width="wrap_content"
+        android:layout_height="@dimen/overview_actions_height"
+        android:layout_gravity="bottom|center_horizontal"
+        android:orientation="horizontal"
+        android:visibility="gone">
+
         <Button
             android:id="@+id/action_save_app_pair"
             style="@style/OverviewActionButton"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="@string/action_save_app_pair"
-            android:theme="@style/ThemeControlHighlightWorkspaceColor"
-            android:visibility="gone" />
+            android:theme="@style/ThemeControlHighlightWorkspaceColor" />
+
     </LinearLayout>
 
 </com.android.quickstep.views.OverviewActionsView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 9f648a7..cc3b30e 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -28,10 +28,7 @@
     launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
     launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
 
-    <com.android.quickstep.views.TaskThumbnailViewDeprecated
-        android:id="@+id/snapshot"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"/>
+    <include layout="@layout/task_thumbnail" />
 
     <!-- Filtering affects only alpha instead of the visibility since visibility can be altered
          separately through RecentsView#resetFromSplitSelectionState() -->
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index 36d7f86..89e9b3d 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -42,10 +42,7 @@
          views that do not inherint from TaskView only or create a generic TaskView that have
          N number of tasks.
      -->
-    <com.android.quickstep.views.TaskThumbnailViewDeprecated
-        android:id="@+id/snapshot"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
+    <include layout="@layout/task_thumbnail"
         android:visibility="gone" />
 
     <ViewStub
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index ec657bd..87a0f70 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -33,15 +33,10 @@
     launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
     launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
 
-    <com.android.quickstep.views.TaskThumbnailViewDeprecated
-        android:id="@+id/snapshot"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"/>
+    <include layout="@layout/task_thumbnail"/>
 
-    <com.android.quickstep.views.TaskThumbnailViewDeprecated
-        android:id="@+id/bottomright_snapshot"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"/>
+    <include layout="@layout/task_thumbnail"
+        android:id="@+id/bottomright_snapshot" />
 
     <!-- Filtering affects only alpha instead of the visibility since visibility can be altered
          separately through RecentsView#resetFromSplitSelectionState() -->
diff --git a/quickstep/res/layout/task_thumbnail.xml b/quickstep/res/layout/task_thumbnail.xml
new file mode 100644
index 0000000..f1a3d62
--- /dev/null
+++ b/quickstep/res/layout/task_thumbnail.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+<com.android.quickstep.views.TaskThumbnailViewDeprecated
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/snapshot"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" />
\ No newline at end of file
diff --git a/quickstep/res/values-de/strings.xml b/quickstep/res/values-de/strings.xml
index 5097ff9..af35fc3 100644
--- a/quickstep/res/values-de/strings.xml
+++ b/quickstep/res/values-de/strings.xml
@@ -95,7 +95,7 @@
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Einstellungen der Systemsteuerung"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Teilen"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
-    <string name="action_split" msgid="2098009717623550676">"Teilen"</string>
+    <string name="action_split" msgid="2098009717623550676">"Splitscreen"</string>
     <string name="action_save_app_pair" msgid="5974823919237645229">"App-Paar speichern"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Für Splitscreen auf weitere App tippen"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Für Splitscreen andere App auswählen"</string>
diff --git a/quickstep/res/values-es/strings.xml b/quickstep/res/values-es/strings.xml
index 159226e..0d43f1e 100644
--- a/quickstep/res/values-es/strings.xml
+++ b/quickstep/res/values-es/strings.xml
@@ -118,7 +118,7 @@
     <string name="taskbar_edu_pinning_title" msgid="210102174154211712">"Mostrar siempre la barra de tareas"</string>
     <string name="taskbar_edu_pinning_standalone" msgid="2636919474366410467">"Para mostrar siempre la barra de tareas en la parte inferior, mantén pulsada la línea divisoria"</string>
     <string name="taskbar_search_edu_title" msgid="5569194922234364530">"Mantén pulsada la tecla de acción para buscar lo que ves en pantalla"</string>
-    <string name="taskbar_edu_search_disclosure" msgid="8734536088447779686">"El producto usa la parte seleccionada de tu pantalla para hacer búsquedas. Se aplican la <xliff:g id="BEGIN_PRIVACY_LINK">&lt;a href="%1$s"&gt;</xliff:g>Política de Privacidad<xliff:g id="END_PRIVACY_LINK">&lt;/a&gt;</xliff:g> y los <xliff:g id="BEGIN_TOS_LINK">&lt;a href="%2$s"&gt;</xliff:g>Términos del Servicio<xliff:g id="END_TOS_LINK">&lt;/a&gt;</xliff:g> de Google."</string>
+    <string name="taskbar_edu_search_disclosure" msgid="8734536088447779686">"Este producto usa la parte seleccionada de tu pantalla para hacer búsquedas. Se aplican la <xliff:g id="BEGIN_PRIVACY_LINK">&lt;a href="%1$s"&gt;</xliff:g>Política de Privacidad<xliff:g id="END_PRIVACY_LINK">&lt;/a&gt;</xliff:g> y los <xliff:g id="BEGIN_TOS_LINK">&lt;a href="%2$s"&gt;</xliff:g>Términos del Servicio<xliff:g id="END_TOS_LINK">&lt;/a&gt;</xliff:g> de Google."</string>
     <string name="taskbar_edu_close" msgid="887022990168191073">"Cerrar"</string>
     <string name="taskbar_edu_done" msgid="6880178093977704569">"Hecho"</string>
     <string name="taskbar_button_home" msgid="2151398979630664652">"Inicio"</string>
diff --git a/quickstep/res/values-et/strings.xml b/quickstep/res/values-et/strings.xml
index 766dabb..c445bc8 100644
--- a/quickstep/res/values-et/strings.xml
+++ b/quickstep/res/values-et/strings.xml
@@ -95,7 +95,7 @@
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Süsteemi navigeerimisseaded"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Jaga"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Ekraanipilt"</string>
-    <string name="action_split" msgid="2098009717623550676">"Eralda"</string>
+    <string name="action_split" msgid="2098009717623550676">"Jaga pooleks"</string>
     <string name="action_save_app_pair" msgid="5974823919237645229">"Salvesta rakendusepaar"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Jagatud ekraanikuva kasutamiseks puudutage muud rakendust"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Valige jagatud ekraanikuva jaoks muu rakendus."</string>
diff --git a/quickstep/res/values-fi/strings.xml b/quickstep/res/values-fi/strings.xml
index b0694c1..040976d 100644
--- a/quickstep/res/values-fi/strings.xml
+++ b/quickstep/res/values-fi/strings.xml
@@ -95,7 +95,7 @@
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Järjestelmän navigointiasetukset"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Jaa"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Kuvakaappaus"</string>
-    <string name="action_split" msgid="2098009717623550676">"Jaa"</string>
+    <string name="action_split" msgid="2098009717623550676">"Jaettu näyttö"</string>
     <string name="action_save_app_pair" msgid="5974823919237645229">"Tallenna pari"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Avaa jaettu näyttö napauttamalla toista sovellusta"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Käytä jaettua näyttöä valitsemalla toinen sovellus"</string>
diff --git a/quickstep/res/values-my/strings.xml b/quickstep/res/values-my/strings.xml
index e696faf..ecde5a3 100644
--- a/quickstep/res/values-my/strings.xml
+++ b/quickstep/res/values-my/strings.xml
@@ -95,7 +95,7 @@
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"စနစ် လမ်းညွှန် ဆက်တင်များ"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"မျှဝေရန်"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"ဖန်သားပြင်ဓာတ်ပုံ"</string>
-    <string name="action_split" msgid="2098009717623550676">"ခွဲထုတ်ရန်"</string>
+    <string name="action_split" msgid="2098009717623550676">"ခွဲရန်"</string>
     <string name="action_save_app_pair" msgid="5974823919237645229">"အက်ပ်အတွဲ သိမ်းရန်"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"မျက်နှာပြင် ခွဲ၍ပြသရန် အက်ပ်နောက်တစ်ခုကို တို့ပါ"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"မျက်နှာပြင် ခွဲ၍ပြသခြင်းသုံးရန် နောက်အက်ပ်တစ်ခုရွေးပါ"</string>
diff --git a/quickstep/res/values-zh-rCN/strings.xml b/quickstep/res/values-zh-rCN/strings.xml
index 1086f9f..727f3e7 100644
--- a/quickstep/res/values-zh-rCN/strings.xml
+++ b/quickstep/res/values-zh-rCN/strings.xml
@@ -95,7 +95,7 @@
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"系统导航设置"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"分享"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"屏幕截图"</string>
-    <string name="action_split" msgid="2098009717623550676">"拆分"</string>
+    <string name="action_split" msgid="2098009717623550676">"分屏"</string>
     <string name="action_save_app_pair" msgid="5974823919237645229">"保存应用组合"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"点按另一个应用即可使用分屏"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"另外选择一个应用才可使用分屏模式"</string>
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 2e78489..32e4097 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -44,7 +44,7 @@
 import com.android.quickstep.util.TISBindHelper;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
-import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
+import com.android.quickstep.views.TaskView.TaskContainer;
 import com.android.systemui.shared.recents.model.Task;
 
 import java.io.PrintWriter;
@@ -259,14 +259,14 @@
                         if (foundTaskView != null) {
                             // There is already a running app of this type, use that as second app.
                             // Get index of task (0 or 1), in case it's a GroupedTaskView
-                            TaskIdAttributeContainer taskAttributes =
-                                    foundTaskView.getTaskAttributesById(foundTask.key.id);
+                            TaskContainer taskContainer =
+                                    foundTaskView.getTaskContainerById(foundTask.key.id);
                             recents.confirmSplitSelect(
                                     foundTaskView,
                                     foundTask,
-                                    taskAttributes.getIconView().getDrawable(),
-                                    taskAttributes.getThumbnailView(),
-                                    taskAttributes.getThumbnailView().getThumbnail(),
+                                    taskContainer.getIconView().getDrawable(),
+                                    taskContainer.getThumbnailView(),
+                                    taskContainer.getThumbnailView().getThumbnail(),
                                     null /* intent */,
                                     null /* user */,
                                     info);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
index 90c3ea7..a59e81b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
@@ -158,6 +158,7 @@
     override fun setAlpha(alpha: Int) {
         paint.alpha = alpha
         arrowDrawable.alpha = alpha
+        invalidateSelf()
     }
 
     override fun getAlpha(): Int {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 66e5302..8c83508 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -403,9 +403,6 @@
         }
         if (bubbleToSelect != null) {
             setSelectedBubbleInternal(bubbleToSelect);
-            if (previouslySelectedBubble == null) {
-                mBubbleStashController.animateToInitialState(update.expanded);
-            }
         }
         if (update.shouldShowEducation) {
             mBubbleBarViewController.prepareToShowEducation();
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index de93ba5..4334807 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -98,23 +98,6 @@
     private static final float FADE_IN_ANIM_POSITION_SHIFT = 1 / 60f;
 
     /**
-     * Custom property to set translationX value for the bar view while a bubble is being dragged.
-     * Skips applying translation to the dragged bubble.
-     */
-    private static final FloatProperty<BubbleBarView> BUBBLE_DRAG_TRANSLATION_X =
-            new FloatProperty<>("bubbleDragTranslationX") {
-                @Override
-                public void setValue(BubbleBarView bubbleBarView, float translationX) {
-                    bubbleBarView.setTranslationXDuringBubbleDrag(translationX);
-                }
-
-                @Override
-                public Float get(BubbleBarView bubbleBarView) {
-                    return bubbleBarView.mTranslationXDuringDrag;
-                }
-            };
-
-    /**
      * Custom property to set alpha value for the bar view while a bubble is being dragged.
      * Skips applying alpha to the dragged bubble.
      */
@@ -189,8 +172,7 @@
 
     @Nullable
     private BubbleView mDraggedBubbleView;
-    private float mTranslationXDuringDrag = 0f;
-    private float mAlphaDuringDrag = 0f;
+    private float mAlphaDuringDrag = 1f;
 
     private int mPreviousLayoutDirection = LayoutDirection.UNDEFINED;
 
@@ -264,6 +246,17 @@
         });
     }
 
+    @Override
+    public void setTranslationX(float translationX) {
+        super.setTranslationX(translationX);
+        if (mDraggedBubbleView != null) {
+            // Apply reverse of the translation as an offset to the dragged view. This ensures
+            // that the dragged bubble stays at the current location on the screen and its
+            // position is not affected by the parent translation.
+            mDraggedBubbleView.setOffsetX(-translationX);
+        }
+    }
+
     /**
      * Sets new icon size and spacing between icons and bubble bar borders.
      *
@@ -322,11 +315,9 @@
         final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
         mBubbleBarBackground.setAnchorLeft(onLeft);
         mRelativePivotX = onLeft ? 0f : 1f;
-        if (getLayoutParams() instanceof LayoutParams lp) {
-            lp.gravity = Gravity.BOTTOM | (onLeft ? Gravity.LEFT : Gravity.RIGHT);
-            setLayoutParams(lp);
-        }
-        invalidate();
+        LayoutParams lp = (LayoutParams) getLayoutParams();
+        lp.gravity = Gravity.BOTTOM | (onLeft ? Gravity.LEFT : Gravity.RIGHT);
+        setLayoutParams(lp); // triggers a relayout
     }
 
     /**
@@ -340,11 +331,6 @@
      * Update {@link BubbleBarLocation}
      */
     public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
-        if (mBubbleBarLocationAnimator != null) {
-            mBubbleBarLocationAnimator.removeAllListeners();
-            mBubbleBarLocationAnimator.cancel();
-            mBubbleBarLocationAnimator = null;
-        }
         resetDragAnimation();
         if (bubbleBarLocation != mBubbleBarLocation) {
             mBubbleBarLocation = bubbleBarLocation;
@@ -361,6 +347,10 @@
         }
         mDragging = dragging;
         setElevation(dragging ? mDragElevation : mBubbleElevation);
+        if (!mDragging) {
+            // Relayout after dragging to ensure that the dragged bubble is positioned correctly
+            requestLayout();
+        }
     }
 
     /**
@@ -459,14 +449,13 @@
     }
 
     private Animator getLocationUpdateFadeOutAnimator(BubbleBarLocation newLocation) {
-        final FloatProperty<? super BubbleBarView> txProp = getLocationAnimTranslationXProperty();
         final float shift =
                 getResources().getDisplayMetrics().widthPixels * FADE_OUT_ANIM_POSITION_SHIFT;
         final boolean onLeft = newLocation.isOnLeft(isLayoutRtl());
-        final float tx = txProp.get(this) + (onLeft ? -shift : shift);
+        final float tx = getTranslationX() + (onLeft ? -shift : shift);
 
-        ObjectAnimator positionAnim = ObjectAnimator.ofFloat(this, txProp, tx).setDuration(
-                FADE_OUT_ANIM_POSITION_DURATION_MS);
+        ObjectAnimator positionAnim = ObjectAnimator.ofFloat(this, VIEW_TRANSLATE_X, tx)
+                .setDuration(FADE_OUT_ANIM_POSITION_DURATION_MS);
         positionAnim.setInterpolator(EMPHASIZED_ACCELERATE);
 
         ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, getLocationAnimAlphaProperty(), 0f)
@@ -505,7 +494,7 @@
                 .setEndValue(finalTx)
                 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
                 .setStiffness(FADE_IN_ANIM_POSITION_SPRING_STIFFNESS)
-                .build(this, getLocationAnimTranslationXProperty());
+                .build(this, VIEW_TRANSLATE_X);
 
         ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, getLocationAnimAlphaProperty(), 1f)
                 .setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS);
@@ -516,15 +505,6 @@
     }
 
     /**
-     * Get property that can be used to animate the translation-x value for the bar.
-     * When a bubble is being dragged, uses {@link #BUBBLE_DRAG_TRANSLATION_X}.
-     * Falls back to {@link com.android.launcher3.LauncherAnimUtils#VIEW_TRANSLATE_X} otherwise.
-     */
-    private FloatProperty<? super BubbleBarView> getLocationAnimTranslationXProperty() {
-        return mDraggedBubbleView == null ? VIEW_TRANSLATE_X : BUBBLE_DRAG_TRANSLATION_X;
-    }
-
-    /**
      * Get property that can be used to animate the alpha value for the bar.
      * When a bubble is being dragged, uses {@link #BUBBLE_DRAG_ALPHA}.
      * Falls back to {@link com.android.launcher3.LauncherAnimUtils#VIEW_ALPHA} otherwise.
@@ -534,31 +514,6 @@
     }
 
     /**
-     * Set translation-x value for the bar while a bubble is being dragged.
-     * We can not update translation on the bar directly because the dragged bubble would be
-     * affected as well. As it is a child view.
-     * Instead, while a bubble is being dragged, set translation on each child view, that is not the
-     * dragged view. And set a translation on the background.
-     * This allows for the dragged bubble view to remain in position while the bar moves during
-     * animation.
-     */
-    private void setTranslationXDuringBubbleDrag(float translationX) {
-        mTranslationXDuringDrag = translationX;
-        final int childCount = getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            BubbleView view = (BubbleView) getChildAt(i);
-            if (view != mDraggedBubbleView) {
-                view.setBubbleBarTranslationX(translationX);
-            }
-        }
-        if (mBubbleBarBackground != null) {
-            mTempBackgroundBounds.set(mBubbleBarBackground.getBounds());
-            mTempBackgroundBounds.offsetTo((int) translationX, 0);
-            mBubbleBarBackground.setBounds(mTempBackgroundBounds);
-        }
-    }
-
-    /**
      * Set alpha value for the bar while a bubble is being dragged.
      * We can not update the alpha on the bar directly because the dragged bubble would be affected
      * as well. As it is a child view.
@@ -587,7 +542,6 @@
             mBubbleBarLocationAnimator.cancel();
             mBubbleBarLocationAnimator = null;
         }
-        setTranslationXDuringBubbleDrag(0f);
         setAlphaDuringBubbleDrag(1f);
         setTranslationX(0f);
         setAlpha(1f);
@@ -706,6 +660,10 @@
                 // Skip the dragged bubble. Its translation is managed by the drag controller.
                 continue;
             }
+            // Clear out drag translation and offset
+            bv.setDragTranslationX(0f);
+            bv.setOffsetX(0f);
+
             bv.setTranslationY(ty);
 
             // the position of the bubble when the bar is fully expanded
@@ -851,10 +809,6 @@
     public void setDraggedBubble(@Nullable BubbleView view) {
         if (mDraggedBubbleView != null) {
             mDraggedBubbleView.setZ(0);
-            if (view == null) {
-                // We are clearing the dragged bubble, reset drag
-                resetDragAnimation();
-            }
         }
         mDraggedBubbleView = view;
         if (view != null) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 0b74e15..ac02f1f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -406,10 +406,21 @@
                 return;
             }
 
+            if (!(b instanceof BubbleBarBubble bubble)) {
+                return;
+            }
+
             boolean isInApp = mTaskbarStashController.isInApp();
+            // if this is the first bubble, animate to the initial state. one bubble is the overflow
+            // so check for at most 2 children.
+            if (mBarView.getChildCount() <= 2) {
+                mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding);
+                return;
+            }
+
             // only animate the new bubble if we're in an app and not auto expanding
-            if (b instanceof BubbleBarBubble && isInApp && !isExpanding && !isExpanded()) {
-                mBubbleBarViewAnimator.animateBubbleInForStashed((BubbleBarBubble) b);
+            if (isInApp && !isExpanding && !isExpanded()) {
+                mBubbleBarViewAnimator.animateBubbleInForStashed(bubble);
             }
         } else {
             Log.w(TAG, "addBubble, bubble was null!");
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
index 49f114a..287e906 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
@@ -60,6 +60,7 @@
     private final float mBubbleFocusedScale;
     private final float mBubbleCapturedScale;
     private final float mDismissCapturedScale;
+    private final FloatPropertyCompat<View> mTranslationXProperty;
 
     /**
      * Should be initialised for each dragged view
@@ -81,9 +82,28 @@
         if (view instanceof BubbleBarView) {
             mBubbleFocusedScale = SCALE_BUBBLE_BAR_FOCUSED;
             mBubbleCapturedScale = mDismissCapturedScale;
+            mTranslationXProperty = DynamicAnimation.TRANSLATION_X;
         } else {
             mBubbleFocusedScale = SCALE_BUBBLE_FOCUSED;
             mBubbleCapturedScale = SCALE_BUBBLE_CAPTURED;
+            // Wrap BubbleView.DRAG_TRANSLATION_X as it can't be cast to FloatPropertyCompat<View>
+            mTranslationXProperty = new FloatPropertyCompat<>(
+                    BubbleView.DRAG_TRANSLATION_X.getName()) {
+                @Override
+                public float getValue(View object) {
+                    if (object instanceof BubbleView bubbleView) {
+                        return BubbleView.DRAG_TRANSLATION_X.get(bubbleView);
+                    }
+                    return 0;
+                }
+
+                @Override
+                public void setValue(View object, float value) {
+                    if (object instanceof BubbleView bubbleView) {
+                        BubbleView.DRAG_TRANSLATION_X.setValue(bubbleView, value);
+                    }
+                }
+            };
         }
     }
 
@@ -120,7 +140,7 @@
         mBubbleAnimator
                 .spring(DynamicAnimation.SCALE_X, 1f)
                 .spring(DynamicAnimation.SCALE_Y, 1f)
-                .spring(DynamicAnimation.TRANSLATION_X, restingPosition.x, velocity.x,
+                .spring(mTranslationXProperty, restingPosition.x, velocity.x,
                         mTranslationConfig)
                 .spring(DynamicAnimation.TRANSLATION_Y, restingPosition.y, velocity.y,
                         mTranslationConfig)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index 1764f75..15de1b8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -104,7 +104,9 @@
             }
 
             @Override
-            protected void onDragUpdate(float x, float y) {
+            protected void onDragUpdate(float x, float y, float newTx, float newTy) {
+                bubbleView.setDragTranslationX(newTx);
+                bubbleView.setTranslationY(newTy);
                 mBubblePinController.onDragUpdate(x, y);
             }
 
@@ -145,12 +147,7 @@
             private BubbleBarLocation mReleasedLocation = BubbleBarLocation.DEFAULT;
 
             private final LocationChangeListener mLocationChangeListener =
-                    new LocationChangeListener() {
-                        @Override
-                        public void onRelease(@NonNull BubbleBarLocation location) {
-                            mReleasedLocation = location;
-                        }
-                    };
+                    location -> mReleasedLocation = location;
 
             @Override
             protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
@@ -172,7 +169,9 @@
             }
 
             @Override
-            protected void onDragUpdate(float x, float y) {
+            protected void onDragUpdate(float x, float y, float newTx, float newTy) {
+                bubbleBarView.setTranslationX(newTx);
+                bubbleBarView.setTranslationY(newTy);
                 mBubbleBarPinController.onDragUpdate(x, y);
             }
 
@@ -265,8 +264,7 @@
          * Called when bubble is dragged to new coordinates.
          * Not called while bubble is stuck to the dismiss target.
          */
-        protected void onDragUpdate(float x, float y) {
-        }
+        protected abstract void onDragUpdate(float x, float y, float newTx, float newTy);
 
         /**
          * Called when the dragging interaction has ended and all the animations have completed
@@ -411,9 +409,9 @@
         private void drag(@NonNull View view, @NonNull MotionEvent event, float dx, float dy,
                 float x, float y) {
             if (mBubbleDismissController.handleTouchEvent(event)) return;
-            view.setTranslationX(mViewInitialPosition.x + dx);
-            view.setTranslationY(mViewInitialPosition.y + dy);
-            onDragUpdate(x, y);
+            final float newTx = mViewInitialPosition.x + dx;
+            final float newTy = mViewInitialPosition.y + dy;
+            onDragUpdate(x, y, newTx, newTy);
         }
 
         private void stopDragging(@NonNull View view, @NonNull MotionEvent event) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 4b3416c..d0462aa 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -123,21 +123,17 @@
     }
 
     /**
-     * Animates the bubble bar and handle to their initial state, transitioning from the state where
-     * both views are invisible. Called when the first bubble is added or when the device is
+     * Animates the handle (or bubble bar depending on state) to be visible after the device is
      * unlocked.
      *
      * <p>Normally either the bubble bar or the handle is visible,
      * and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to transition
      * between these two states. But the transition from the state where both the bar and handle
      * are invisible is slightly different.
-     *
-     * <p>The initial state will depend on the current state of the device, i.e. overview, home etc
-     * and whether bubbles are requested to be expanded.
      */
-    public void animateToInitialState(boolean expanding) {
+    private void animateAfterUnlock() {
         AnimatorSet animatorSet = new AnimatorSet();
-        if (expanding || mBubblesShowingOnHome || mBubblesShowingOnOverview) {
+        if (mBubblesShowingOnHome || mBubblesShowingOnOverview) {
             mIsStashed = false;
             animatorSet.playTogether(mIconScaleForStash.animateToValue(1),
                     mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()),
@@ -217,7 +213,7 @@
         if (isSysuiLocked != mIsSysuiLocked) {
             mIsSysuiLocked = isSysuiLocked;
             if (!mIsSysuiLocked && mBarViewController.hasBubbles()) {
-                animateToInitialState(false /* expanding */);
+                animateAfterUnlock();
             }
         }
     }
@@ -453,4 +449,9 @@
         mIsStashed = isStashed;
         onIsStashedChanged();
     }
+
+    /** Set the translation Y for the stashed handle. */
+    public void setHandleTranslationY(float ty) {
+        mHandleViewController.setTranslationYForSwipe(ty);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 3dc4ebc..61a6bce 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -22,11 +22,13 @@
 import android.graphics.Outline;
 import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.util.FloatProperty;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewOutlineProvider;
 import android.widget.ImageView;
 
+import androidx.annotation.NonNull;
 import androidx.constraintlayout.widget.ConstraintLayout;
 
 import com.android.launcher3.R;
@@ -47,6 +49,25 @@
     public static final int DEFAULT_PATH_SIZE = 100;
 
     /**
+     * Property to update drag translation value.
+     *
+     * @see BubbleView#getDragTranslationX()
+     * @see BubbleView#setDragTranslationX(float)
+     */
+    public static final FloatProperty<BubbleView> DRAG_TRANSLATION_X = new FloatProperty<>(
+            "dragTranslationX") {
+        @Override
+        public void setValue(@NonNull BubbleView bubbleView, float value) {
+            bubbleView.setDragTranslationX(value);
+        }
+
+        @Override
+        public Float get(BubbleView bubbleView) {
+            return bubbleView.getDragTranslationX();
+        }
+    };
+
+    /**
      * Flags that suppress the visibility of the 'new' dot or the app badge, for one reason or
      * another. If any of these flags are set, the dot will not be shown.
      * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown.
@@ -66,8 +87,8 @@
     private final ImageView mAppIcon;
     private final int mBubbleSize;
 
-    private float mBubbleBarTranslationX = 0f;
-    private float mTranslationX = 0f;
+    private float mDragTranslationX;
+    private float mOffsetX;
 
     private DotRenderer mDotRenderer;
     private DotRenderer.DrawParams mDrawParams;
@@ -129,33 +150,37 @@
         outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
     }
 
-    @Override
-    public void setTranslationX(float translationX) {
-        // Overriding setting translationX as it can be a combination of the parent translation
-        // and current view translation.
-        // When a BubbleView is being dragged to pin the bubble bar to other side, we animate the
-        // bar to the new location during the drag.
-        // One part of the animation is updating the translation of the bubble bar. But doing
-        // that also updates the translation for the child views, like the dragged bubble.
-        // To get around that, we instead apply translation on each child view of bubble bar. It
-        // is applied as bubble bar translation. This results in BubbleView's translation being a
-        // sum of the translation it has and the parent bubble bar translation.
-        mTranslationX = translationX;
-        applyTranslation();
+    /**
+     * Set translation-x while this bubble is being dragged.
+     * Translation applied to the view is a sum of {@code translationX} and offset defined by
+     * {@link #setOffsetX(float)}.
+     */
+    public void setDragTranslationX(float translationX) {
+        mDragTranslationX = translationX;
+        applyDragTranslation();
     }
 
     /**
-     * Translation of the bubble bar that hosts this bubble.
-     * Is applied together with translation applied on the view through
-     * {@link #setTranslationX(float)}.
+     * Get translation value applied via {@link #setDragTranslationX(float)}.
      */
-    void setBubbleBarTranslationX(float translationX) {
-        mBubbleBarTranslationX = translationX;
-        applyTranslation();
+    public float getDragTranslationX() {
+        return mDragTranslationX;
     }
 
-    private void applyTranslation() {
-        super.setTranslationX(mTranslationX + mBubbleBarTranslationX);
+    /**
+     * Set offset on x-axis while dragging.
+     * Used to counter parent translation in order to keep the dragged view at the current position
+     * on screen.
+     * Translation applied to the view is a sum of {@code offsetX} and translation defined by
+     * {@link #setDragTranslationX(float)}
+     */
+    public void setOffsetX(float offsetX) {
+        mOffsetX = offsetX;
+        applyDragTranslation();
+    }
+
+    private void applyDragTranslation() {
+        setTranslationX(mDragTranslationX + mOffsetX);
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 66521c1..be935d8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -93,15 +93,15 @@
         if (animator.isRunning()) animator.cancel()
         // the animation of a new bubble is divided into 2 parts. The first part shows the bubble
         // and the second part hides it after a delay.
-        val showAnimation = buildShowAnimation()
-        val hideAnimation = buildHideAnimation()
+        val showAnimation = buildHandleToBubbleBarAnimation()
+        val hideAnimation = buildBubbleBarToHandleAnimation()
         animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
         scheduler.post(showAnimation)
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
 
     /**
-     * Returns a [Runnable] that starts the animation that shows the new or updated bubble.
+     * Returns a [Runnable] that starts the animation that morphs the handle to the bubble bar.
      *
      * Visually, the animation is divided into 2 parts. The stash handle starts animating up and
      * fading out and then the bubble bar starts animating up and fading in.
@@ -114,7 +114,7 @@
      * 3. The third part is the overshoot of the spring animation, where we make the bubble fully
      *    visible which helps avoiding further updates when we re-enter the second part.
      */
-    private fun buildShowAnimation() = Runnable {
+    private fun buildHandleToBubbleBarAnimation() = Runnable {
         // prepare the bubble bar for the animation
         bubbleBarView.onAnimatingBubbleStarted()
         bubbleBarView.visibility = VISIBLE
@@ -197,7 +197,8 @@
     }
 
     /**
-     * Returns a [Runnable] that starts the animation that hides the bubble bar.
+     * Returns a [Runnable] that starts the animation that hides the bubble bar and morphs it into
+     * the stashed handle.
      *
      * Similarly to the show animation, this is visually divided into 2 parts. We first animate the
      * bubble bar out, and then animate the stash handle in. At the end of the animation we reset
@@ -209,13 +210,14 @@
      * 2. In the second part the bubble bar is fully hidden and the handle animates in.
      * 3. The third part is the overshoot. The handle is made fully visible.
      */
-    private fun buildHideAnimation() = Runnable {
+    private fun buildBubbleBarToHandleAnimation() = Runnable {
         if (animatingBubble == null) return@Runnable
         val offset = bubbleStashController.diffBetweenHandleAndBarCenters
         val stashedHandleTranslationY =
             bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
         // this is the total distance that both the stashed handle and the bar will be traveling
         val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
+        bubbleStashController.setHandleTranslationY(totalTranslationY)
         val animator = bubbleStashController.stashedHandlePhysicsAnimator
         animator.setDefaultSpringConfig(springConfig)
         animator.spring(DynamicAnimation.TRANSLATION_Y, 0f)
@@ -259,6 +261,50 @@
         animator.start()
     }
 
+    /** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
+    fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) {
+        val bubbleView = b.view
+        val animator = PhysicsAnimator.getInstance(bubbleView)
+        if (animator.isRunning()) animator.cancel()
+        // the animation of a new bubble is divided into 2 parts. The first part shows the bubble
+        // and the second part hides it after a delay if we are in an app.
+        val showAnimation = buildBubbleBarBounceAnimation()
+        val hideAnimation =
+            if (isInApp && !isExpanding) {
+                buildBubbleBarToHandleAnimation()
+            } else {
+                // in this case the bubble bar remains visible so not much to do. once we implement
+                // the flyout we'll update this runnable to hide it.
+                Runnable {
+                    animatingBubble = null
+                    bubbleStashController.showBubbleBarImmediate()
+                    bubbleBarView.onAnimatingBubbleCompleted()
+                }
+            }
+        animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+        scheduler.post(showAnimation)
+        scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
+    }
+
+    private fun buildBubbleBarBounceAnimation() = Runnable {
+        // prepare the bubble bar for the animation
+        bubbleBarView.onAnimatingBubbleStarted()
+        bubbleBarView.translationY = bubbleBarView.height.toFloat()
+        bubbleBarView.visibility = VISIBLE
+        bubbleBarView.alpha = 1f
+        bubbleBarView.scaleX = 1f
+        bubbleBarView.scaleY = 1f
+
+        val animator = PhysicsAnimator.getInstance(bubbleBarView)
+        animator.setDefaultSpringConfig(springConfig)
+        animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY)
+        animator.addEndListener { _, _, _, _, _, _, _ ->
+            // the bubble bar is now fully settled in. update taskbar touch region so it's touchable
+            bubbleStashController.updateTaskbarTouchRegion()
+        }
+        animator.start()
+    }
+
     /** Handles clicking on the animating bubble while the animation is still playing. */
     fun onBubbleClickedWhileAnimating() {
         val hideAnimation = animatingBubble?.hideAnimation ?: return
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 6c1d4b1..317e6f2 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
-import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
@@ -50,6 +49,7 @@
 import com.android.quickstep.util.SplitAnimationTimings;
 import com.android.quickstep.views.ClearAllButton;
 import com.android.quickstep.views.LauncherRecentsView;
+import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
 
 /**
@@ -167,8 +167,8 @@
         propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
                 clearAllButtonAlpha, LINEAR);
         float overviewButtonAlpha = state.areElementsVisible(mLauncher, OVERVIEW_ACTIONS) ? 1 : 0;
-        propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
-                MULTI_PROPERTY_VALUE, overviewButtonAlpha, config.getInterpolator(
+        propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlphaSetter(),
+                OverviewActionsView.FLOAT_SETTER, overviewButtonAlpha, config.getInterpolator(
                         ANIM_OVERVIEW_ACTIONS_FADE, LINEAR));
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 527a776..fc0df76 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -248,7 +248,7 @@
         TASK_THUMBNAIL_SPLASH_ALPHA.set(mRecentsView, fromState.showTaskThumbnailSplash() ? 1f : 0);
         mRecentsView.setContentAlpha(1);
         mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
-        mLauncher.getActionsView().getVisibilityAlpha().setValue(
+        mLauncher.getActionsView().getVisibilityAlphaSetter().accept(
                 (fromState.getVisibleElements(mLauncher) & OVERVIEW_ACTIONS) != 0 ? 1f : 0f);
         mRecentsView.setTaskIconScaledDown(true);
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index 05a55d0..16185f5 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -151,7 +151,7 @@
             int sysuiFlags = 0;
             TaskView tv = mOverviewPanel.getTaskViewAt(0);
             if (tv != null) {
-                sysuiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
+                sysuiFlags = tv.getFirstThumbnailView().getSysUiStatusNavFlags();
             }
             mLauncher.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, sysuiFlags);
         } else {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 300d697..26b528c 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -252,7 +252,7 @@
                     mTaskBeingDragged, maxDuration, currentInterpolator);
 
             // Since the thumbnail is what is filling the screen, based the end displacement on it.
-            View thumbnailView = mTaskBeingDragged.getThumbnail();
+            View thumbnailView = mTaskBeingDragged.getFirstThumbnailView();
             mTempCords[1] = orientationHandler.getSecondaryDimension(thumbnailView);
             dl.getDescendantCoordRelativeToSelf(thumbnailView, mTempCords);
             mEndDisplacement = secondaryLayerDimension - mTempCords[1];
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 2ce40b4..28ae3d2 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -135,10 +135,11 @@
 import com.android.quickstep.util.SwipePipToHomeAnimator;
 import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.util.TransformParams;
+import com.android.quickstep.views.DesktopTaskView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.TaskView;
-import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
+import com.android.quickstep.views.TaskView.TaskContainer;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -147,6 +148,7 @@
 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.startingsurface.SplashScreenExitAnimationUtils;
 
@@ -922,7 +924,7 @@
             TaskView runningTask = mRecentsView.getRunningTaskView();
             TaskView centermostTask = mRecentsView.getTaskViewNearestToCenterOfScreen();
             int centermostTaskFlags = centermostTask == null ? 0
-                    : centermostTask.getThumbnail().getSysUiStatusNavFlags();
+                    : centermostTask.getFirstThumbnailView().getSysUiStatusNavFlags();
             boolean swipeUpThresholdPassed = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD;
             boolean quickswitchThresholdPassed = centermostTask != runningTask;
 
@@ -1258,13 +1260,16 @@
                 ? mRecentsView.getNextPageTaskView() : null;
         TaskView currentPageTaskView = mRecentsView != null
                 ? mRecentsView.getCurrentPageTaskView() : null;
-        if (((nextPageTaskView != null && nextPageTaskView.isDesktopTask())
-                || (currentPageTaskView != null && currentPageTaskView.isDesktopTask()))
-                && endTarget == NEW_TASK) {
-            // TODO(b/268075592): add support for quickswitch to/from desktop
-            return LAST_TASK;
-        }
 
+        if (Flags.enableDesktopWindowingMode()
+                && !(Flags.enableDesktopWindowingWallpaperActivity()
+                && Flags.enableDesktopWindowingQuickSwitch())) {
+            if ((nextPageTaskView instanceof DesktopTaskView
+                    || currentPageTaskView instanceof DesktopTaskView)
+                    && endTarget == NEW_TASK) {
+                return LAST_TASK;
+            }
+        }
         return endTarget;
     }
 
@@ -1421,14 +1426,27 @@
             mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED);
             setClampScrollOffset(false);
         };
-        if (mRecentsView != null && (mRecentsView.getCurrentPageTaskView() != null
-                && !mRecentsView.getCurrentPageTaskView().isDesktopTask())) {
-            ActiveGestureLog.INSTANCE.trackEvent(ActiveGestureErrorDetector.GestureEvent
-                    .SET_ON_PAGE_TRANSITION_END_CALLBACK);
-            // TODO(b/268075592): add support for quickswitch to/from desktop
-            mRecentsView.setOnPageTransitionEndCallback(onPageTransitionEnd);
+
+        if (Flags.enableDesktopWindowingMode()
+                && !(Flags.enableDesktopWindowingWallpaperActivity()
+                && Flags.enableDesktopWindowingQuickSwitch())) {
+            if (mRecentsView != null && (mRecentsView.getCurrentPageTaskView() != null
+                    && !(mRecentsView.getCurrentPageTaskView() instanceof DesktopTaskView))) {
+                ActiveGestureLog.INSTANCE.trackEvent(ActiveGestureErrorDetector.GestureEvent
+                        .SET_ON_PAGE_TRANSITION_END_CALLBACK);
+                mRecentsView.setOnPageTransitionEndCallback(onPageTransitionEnd);
+            } else {
+                onPageTransitionEnd.run();
+            }
         } else {
-            onPageTransitionEnd.run();
+            if (mRecentsView != null) {
+                ActiveGestureLog.INSTANCE.trackEvent(
+                        ActiveGestureErrorDetector
+                                .GestureEvent.SET_ON_PAGE_TRANSITION_END_CALLBACK);
+                mRecentsView.setOnPageTransitionEndCallback(onPageTransitionEnd);
+            } else {
+                onPageTransitionEnd.run();
+            }
         }
 
         animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocityPxPerMs);
@@ -2250,13 +2268,14 @@
                     mRecentsAnimationController, mRecentsAnimationTargets);
         });
 
-        if ((mRecentsView.getNextPageTaskView() != null
-                && mRecentsView.getNextPageTaskView().isDesktopTask())
-                || (mRecentsView.getCurrentPageTaskView() != null
-                && mRecentsView.getCurrentPageTaskView().isDesktopTask())) {
-            // TODO(b/268075592): add support for quickswitch to/from desktop
-            mRecentsViewScrollLinked = false;
-            return;
+        if (Flags.enableDesktopWindowingMode()
+                && !(Flags.enableDesktopWindowingWallpaperActivity()
+                        && Flags.enableDesktopWindowingQuickSwitch())) {
+            if (mRecentsView.getNextPageTaskView() instanceof DesktopTaskView
+                    || mRecentsView.getCurrentPageTaskView() instanceof DesktopTaskView) {
+                mRecentsViewScrollLinked = false;
+                return;
+            }
         }
 
         // Disable scrolling in RecentsView for trackpad 3-finger swipe up gesture.
@@ -2287,15 +2306,15 @@
                 int[] taskIds = nextTask.getTaskIds();
                 ActiveGestureLog.CompoundString nextTaskLog = new ActiveGestureLog.CompoundString(
                         "Launching task: ");
-                for (TaskIdAttributeContainer c : nextTask.getTaskIdAttributeContainers()) {
-                    if (c == null) {
+                for (TaskContainer container : nextTask.getTaskContainers()) {
+                    if (container == null) {
                         continue;
                     }
                     nextTaskLog
                             .append("[id: ")
-                            .append(c.getTask().key.id)
+                            .append(container.getTask().key.id)
                             .append(", pkg: ")
-                            .append(c.getTask().key.getPackageName())
+                            .append(container.getTask().key.getPackageName())
                             .append("] | ");
                 }
                 mGestureState.updateLastStartedTaskIds(taskIds);
@@ -2407,7 +2426,7 @@
         RemoteAnimationTarget taskTarget = taskTargetOptional.get();
         TaskView taskView = mRecentsView == null
                 ? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId);
-        if (taskView == null || !taskView.getThumbnail().shouldShowSplashView()) {
+        if (taskView == null || !taskView.getFirstThumbnailView().shouldShowSplashView()) {
             ActiveGestureLog.INSTANCE.addLog("Invalid task view splash state");
             finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
             return;
diff --git a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
index aab6aa1..e33ef7f 100644
--- a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
+++ b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
@@ -23,12 +23,12 @@
 import com.android.launcher3.popup.SystemShortcut
 import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.RecentsViewContainer
-import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
+import com.android.quickstep.views.TaskView.TaskContainer
 
 /** A menu item, "Desktop", that allows the user to bring the current app into Desktop Windowing. */
 class DesktopSystemShortcut(
     container: RecentsViewContainer,
-    private val mTaskContainer: TaskIdAttributeContainer,
+    private val mTaskContainer: TaskContainer,
     abstractFloatingViewHelper: AbstractFloatingViewHelper
 ) :
     SystemShortcut<RecentsViewContainer>(
@@ -59,7 +59,7 @@
             return object : TaskShortcutFactory {
                 override fun getShortcuts(
                     container: RecentsViewContainer,
-                    taskContainer: TaskIdAttributeContainer
+                    taskContainer: TaskContainer
                 ): List<DesktopSystemShortcut>? {
                     return if (!DesktopModeStatus.canEnterDesktopMode(container.asContext())) null
                     else if (!taskContainer.task.isDockable) null
diff --git a/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt b/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
index f68f793..0f844e1 100644
--- a/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
+++ b/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
@@ -154,7 +154,7 @@
     }
 
     companion object {
-        val configHelper by lazy { DeviceConfigHelper(::DeviceConfigWrapper) }
+        @JvmStatic val configHelper by lazy { DeviceConfigHelper(::DeviceConfigWrapper) }
 
         @JvmStatic fun get() = configHelper.config
     }
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index 4b5a15d..6a9f509 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -305,8 +305,8 @@
             return null;
         }
         if (sourceTaskView == null
-                || sourceTaskView.getTask() == null
-                || sourceTaskView.getTask().key.getComponent() == null) {
+                || sourceTaskView.getFirstTask() == null
+                || sourceTaskView.getFirstTask().key.getComponent() == null) {
             // Disable if it's an invalid task
             return null;
         }
@@ -323,8 +323,8 @@
         }
 
         return mContainer.getFirstMatchForAppClose(launchCookieItemId,
-                sourceTaskView.getTask().key.getComponent().getPackageName(),
-                UserHandle.of(sourceTaskView.getTask().key.userId),
+                sourceTaskView.getFirstTask().key.getComponent().getPackageName(),
+                UserHandle.of(sourceTaskView.getFirstTask().key.userId),
                 false /* supportsAllAppsState */);
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 4b4f914..54466f3 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_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;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP;
@@ -399,7 +400,8 @@
                 && (mSystemUiStateFlags & SYSUI_STATE_MAGNIFICATION_OVERLAP) == 0
                 && ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0
                         || (mSystemUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0)
-                && (mSystemUiStateFlags & SYSUI_STATE_DEVICE_DREAMING) == 0;
+                && (mSystemUiStateFlags & SYSUI_STATE_DEVICE_DREAMING) == 0
+                && (mSystemUiStateFlags & SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION) == 0;
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index d32c7a6..1a46fb6 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -46,7 +46,7 @@
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.TaskThumbnailViewDeprecated;
 import com.android.quickstep.views.TaskView;
-import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
+import com.android.quickstep.views.TaskView.TaskContainer;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
@@ -59,7 +59,7 @@
 public class TaskOverlayFactory implements ResourceBasedOverride {
 
     public static List<SystemShortcut> getEnabledShortcuts(TaskView taskView,
-            TaskIdAttributeContainer taskContainer) {
+            TaskContainer taskContainer) {
         final ArrayList<SystemShortcut> shortcuts = new ArrayList<>();
         final RecentsViewContainer container =
                 RecentsViewContainer.containerFromContext(taskView.getContext());
@@ -292,7 +292,7 @@
 
             @Override
             public void onClick(View view) {
-                saveScreenshot(mThumbnailView.getTaskView().getTask());
+                saveScreenshot(mThumbnailView.getTaskView().getFirstTask());
                 dismissTaskMenuView();
             }
         }
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 8df4bdd..f052a9d 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -58,14 +58,13 @@
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.TaskThumbnailViewDeprecated;
 import com.android.quickstep.views.TaskView;
-import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
+import com.android.quickstep.views.TaskView.TaskContainer;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
 import com.android.systemui.shared.recents.view.RecentsTransition;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.Function;
@@ -78,7 +77,7 @@
 public interface TaskShortcutFactory {
     @Nullable
     default List<SystemShortcut> getShortcuts(RecentsViewContainer container,
-            TaskIdAttributeContainer taskContainer) {
+            TaskContainer taskContainer) {
         return null;
     }
 
@@ -108,7 +107,7 @@
     TaskShortcutFactory APP_INFO = new TaskShortcutFactory() {
         @Override
         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
-                TaskIdAttributeContainer taskContainer) {
+                TaskContainer taskContainer) {
             TaskView taskView = taskContainer.getTaskView();
             AppInfo.SplitAccessibilityInfo accessibilityInfo =
                     new AppInfo.SplitAccessibilityInfo(taskView.containsMultipleTasks(),
@@ -176,7 +175,7 @@
         private final LauncherEvent mLauncherEvent;
 
         public FreeformSystemShortcut(int iconRes, int textRes, RecentsViewContainer container,
-                TaskIdAttributeContainer taskContainer, LauncherEvent launcherEvent) {
+                TaskContainer taskContainer, LauncherEvent launcherEvent) {
             super(iconRes, textRes, container, taskContainer.getItemInfo(),
                     taskContainer.getTaskView());
             mLauncherEvent = launcherEvent;
@@ -199,7 +198,7 @@
         }
 
         private void startActivity() {
-            final Task.TaskKey taskKey = mTaskView.getTask().key;
+            final Task.TaskKey taskKey = mTaskView.getFirstTask().key;
             final int taskId = taskKey.id;
             final ActivityOptions options = makeLaunchOptions(mTarget);
             if (options != null) {
@@ -292,7 +291,7 @@
     TaskShortcutFactory SPLIT_SELECT = new TaskShortcutFactory() {
         @Override
         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
-                TaskIdAttributeContainer taskContainer) {
+                TaskContainer taskContainer) {
             DeviceProfile deviceProfile = container.getDeviceProfile();
             final Task task = taskContainer.getTask();
             final int intentFlags = task.key.baseIntent.getFlags();
@@ -327,7 +326,7 @@
         @Nullable
         @Override
         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
-                TaskIdAttributeContainer taskContainer) {
+                TaskContainer taskContainer) {
             DeviceProfile deviceProfile = container.getDeviceProfile();
             final TaskView taskView = taskContainer.getTaskView();
             final RecentsView recentsView = taskView.getRecentsView();
@@ -336,7 +335,7 @@
                     recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView));
             boolean shouldShowActionsButtonInstead =
                     isLargeTileFocusedTask && isInExpectedScrollPosition;
-            boolean hasUnpinnableApp = Arrays.stream(taskView.getTaskIdAttributeContainers())
+            boolean hasUnpinnableApp = taskView.getTaskContainers().stream()
                     .anyMatch(att -> att != null && att.getItemInfo() != null
                             && ((att.getItemInfo().runtimeStatusFlags
                                 & ItemInfoWithIcon.FLAG_NOT_PINNABLE) != 0));
@@ -375,7 +374,7 @@
     TaskShortcutFactory FREE_FORM = new TaskShortcutFactory() {
         @Override
         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
-                TaskIdAttributeContainer taskContainer) {
+                TaskContainer taskContainer) {
             final Task task = taskContainer.getTask();
             if (!task.isDockable) {
                 return null;
@@ -401,7 +400,7 @@
     TaskShortcutFactory PIN = new TaskShortcutFactory() {
         @Override
         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
-                TaskIdAttributeContainer taskContainer) {
+                TaskContainer taskContainer) {
             if (!SystemUiProxy.INSTANCE.get(container.asContext()).isActive()) {
                 return null;
             }
@@ -423,7 +422,7 @@
         private final TaskView mTaskView;
 
         public PinSystemShortcut(RecentsViewContainer target,
-                TaskIdAttributeContainer taskContainer) {
+                TaskContainer taskContainer) {
             super(R.drawable.ic_pin, R.string.recent_task_option_pin, target,
                     taskContainer.getItemInfo(), taskContainer.getTaskView());
             mTaskView = taskContainer.getTaskView();
@@ -433,7 +432,7 @@
         public void onClick(View view) {
             if (mTaskView.launchTaskAnimated() != null) {
                 SystemUiProxy.INSTANCE.get(mTarget.asContext()).startScreenPinning(
-                        mTaskView.getTask().key.id);
+                        mTaskView.getFirstTask().key.id);
             }
             dismissTaskMenuView();
             mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo())
@@ -444,7 +443,7 @@
     TaskShortcutFactory INSTALL = new TaskShortcutFactory() {
         @Override
         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
-                TaskIdAttributeContainer taskContainer) {
+                TaskContainer taskContainer) {
             Task t = taskContainer.getTask();
             return InstantAppResolver.newInstance(container.asContext()).isInstantApp(
                     t.getTopComponent().getPackageName(), t.getKey().userId)
@@ -457,7 +456,7 @@
     TaskShortcutFactory WELLBEING = new TaskShortcutFactory() {
         @Override
         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
-                TaskIdAttributeContainer taskContainer) {
+                TaskContainer taskContainer) {
             SystemShortcut<ActivityContext> wellbeingShortcut =
                     WellbeingModel.SHORTCUT_FACTORY.getShortcut(container,
                             taskContainer.getItemInfo(), taskContainer.getTaskView());
@@ -468,7 +467,7 @@
     TaskShortcutFactory SCREENSHOT = new TaskShortcutFactory() {
         @Override
         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
-                TaskIdAttributeContainer taskContainer) {
+                TaskContainer taskContainer) {
             boolean isTablet = container.getDeviceProfile().isTablet;
             boolean isGridOnlyOverview = isTablet && Flags.enableGridOnlyOverview();
             // Extra conditions if it's not grid-only overview
@@ -498,7 +497,7 @@
     TaskShortcutFactory MODAL = new TaskShortcutFactory() {
         @Override
         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
-                TaskIdAttributeContainer taskContainer) {
+                TaskContainer taskContainer) {
             boolean isTablet = container.getDeviceProfile().isTablet;
             boolean isGridOnlyOverview = isTablet && Flags.enableGridOnlyOverview();
             // Extra conditions if it's not grid-only overview
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index d89d399..d2560e6 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -121,7 +121,7 @@
                 for (int i = 0; i < recentsView.getTaskViewCount(); i++) {
                     TaskView taskView = recentsView.getTaskViewAt(i);
                     if (recentsView.isTaskViewVisible(taskView)) {
-                        Task.TaskKey key = taskView.getTask().key;
+                        Task.TaskKey key = taskView.getFirstTask().key;
                         if (componentName.equals(key.getComponent()) && userId == key.userId) {
                             return taskView;
                         }
@@ -334,7 +334,7 @@
             // During animation we apply transformation on the thumbnailView (and not the rootView)
             // to follow the TaskViewSimulator. So the final matrix applied on the thumbnailView is:
             //    Mt K(0)` K(t) Mt`
-            TaskThumbnailViewDeprecated[] thumbnails = v.getThumbnails();
+            TaskThumbnailViewDeprecated[] thumbnails = v.getThumbnailViews();
 
             // In case simulator copies and thumbnail size do no match, ensure we get the lesser.
             // This ensures we do not create arrays with empty elements or attempt to references
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index 2e76356..1bf129c 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -46,12 +46,10 @@
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.views.ClearAllButton;
 import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.RecentsViewContainer;
 
 /**
  * State controller for fallback recents activity
@@ -98,8 +96,8 @@
         setter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
                 clearAllButtonAlpha, LINEAR);
         float overviewButtonAlpha = state.hasOverviewActions() ? 1 : 0;
-        setter.setFloat(mActivity.getActionsView().getVisibilityAlpha(),
-                MultiPropertyFactory.MULTI_PROPERTY_VALUE, overviewButtonAlpha, LINEAR);
+        setter.setFloat(mActivity.getActionsView().getVisibilityAlphaSetter(),
+                OverviewActionsView.FLOAT_SETTER, overviewButtonAlpha, LINEAR);
 
         float[] scaleAndOffset = state.getOverviewScaleAndOffset(mActivity);
         setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index e3e14ae..3cae4dc 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -392,6 +392,20 @@
                 case LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END:
                     InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_ALL_APPS_SCROLL);
                     break;
+                case LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN:
+                    InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_PRIVATE_SPACE_LOCK);
+                    break;
+                case LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END:
+                    InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_PRIVATE_SPACE_LOCK);
+                    break;
+                case LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN:
+                    InteractionJankMonitorWrapper.begin(
+                            view,
+                            Cuj.CUJ_LAUNCHER_PRIVATE_SPACE_UNLOCK);
+                    break;
+                case LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END:
+                    InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_PRIVATE_SPACE_UNLOCK);
+                    break;
                 default:
                     break;
             }
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index a82031a..770a452 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -116,9 +116,9 @@
      */
     public void saveAppPair(GroupedTaskView gtv) {
         InteractionJankMonitorWrapper.begin(gtv, Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR);
-        TaskView.TaskIdAttributeContainer[] attributes = gtv.getTaskIdAttributeContainers();
-        WorkspaceItemInfo recentsInfo1 = attributes[0].getItemInfo();
-        WorkspaceItemInfo recentsInfo2 = attributes[1].getItemInfo();
+        List<TaskView.TaskContainer> containers = gtv.getTaskContainers();
+        WorkspaceItemInfo recentsInfo1 = containers.get(0).getItemInfo();
+        WorkspaceItemInfo recentsInfo2 = containers.get(1).getItemInfo();
         WorkspaceItemInfo app1 = lookupLaunchableItem(recentsInfo1.getComponentKey());
         WorkspaceItemInfo app2 = lookupLaunchableItem(recentsInfo2.getComponentKey());
 
diff --git a/quickstep/src/com/android/quickstep/util/AssistStateManager.java b/quickstep/src/com/android/quickstep/util/AssistStateManager.java
index 42db65f..7acb28d 100644
--- a/quickstep/src/com/android/quickstep/util/AssistStateManager.java
+++ b/quickstep/src/com/android/quickstep/util/AssistStateManager.java
@@ -33,8 +33,8 @@
 
     public AssistStateManager() {}
 
-    /** Whether search is available. */
-    public boolean isSearchAvailable() {
+    /** Return {@code true} if the Settings toggle is enabled. */
+    public boolean isSettingsAllEntrypointsEnabled() {
         return false;
     }
 
@@ -43,14 +43,9 @@
         return false;
     }
 
-    /** Whether CsHelper CtS invocation path is available. */
-    public Optional<Boolean> isCsHelperAvailable() {
-        return Optional.empty();
-    }
-
-    /** Whether VIS CtS invocation path is available. */
-    public Optional<Boolean> isVisAvailable() {
-        return Optional.empty();
+    /** Whether ContextualSearchService invocation path is available. */
+    public boolean isContextualSearchServiceAvailable() {
+        return false;
     }
 
     /** Get the Launcher overridden long press nav handle duration to trigger Assistant. */
@@ -90,11 +85,6 @@
         return Optional.empty();
     }
 
-    /** Return {@code true} if the Settings toggle is enabled. */
-    public boolean isSettingsAllEntrypointsEnabled() {
-        return false;
-    }
-
     /** Dump states. */
     public void dump(String prefix, PrintWriter writer) {}
 
diff --git a/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt b/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt
index 544c64d..d36dc7e 100644
--- a/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt
+++ b/quickstep/src/com/android/quickstep/util/DeviceConfigHelper.kt
@@ -48,12 +48,14 @@
                 PropReader(
                     object : PropProvider {
                         override fun <T : Any> get(key: String, fallback: T): T {
-                            if (fallback is Int)
+                            if (fallback is Int) {
+                                allKeys.add(key)
                                 return DeviceConfig.getInt(NAMESPACE_LAUNCHER, key, fallback) as T
-                            else if (fallback is Boolean)
+                            } else if (fallback is Boolean) {
+                                allKeys.add(key)
                                 return DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, key, fallback)
                                     as T
-                            else return fallback
+                            } else return fallback
                         }
                     }
                 )
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index ee2c2e1..40ea70f 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -69,7 +69,7 @@
 import com.android.quickstep.views.SplitInstructionsView
 import com.android.quickstep.views.TaskThumbnailViewDeprecated
 import com.android.quickstep.views.TaskView
-import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
+import com.android.quickstep.views.TaskView.TaskContainer
 import com.android.quickstep.views.TaskViewIcon
 import com.android.wm.shell.shared.TransitionUtil
 import java.util.Optional
@@ -114,7 +114,7 @@
         } else if (splitSelectStateController.isDismissingFromSplitPair) {
             // Initiating split from overview, but on a split pair
             val taskView = taskViewSupplier.get()
-            for (container: TaskIdAttributeContainer in taskView.taskIdAttributeContainers) {
+            for (container: TaskContainer in taskView.taskContainers) {
                 if (container.task.getKey().getId() == splitSelectStateController.initialTaskId) {
                     val drawable = getDrawable(container.iconView, splitSelectSource)
                     return SplitAnimInitProps(
@@ -134,15 +134,17 @@
         } else {
             // Initiating split from overview on fullscreen task TaskView
             val taskView = taskViewSupplier.get()
-            val drawable = getDrawable(taskView.iconView, splitSelectSource)
-            return SplitAnimInitProps(
-                taskView.thumbnail,
-                taskView.thumbnail.thumbnail,
-                drawable!!,
-                fadeWithThumbnail = true,
-                isStagedTask = true,
-                taskView.iconView.asView()
-            )
+            taskView.taskContainers.first().let {
+                val drawable = getDrawable(it.iconView, splitSelectSource)
+                return SplitAnimInitProps(
+                    it.thumbnailView,
+                    it.thumbnailView.thumbnail,
+                    drawable!!,
+                    fadeWithThumbnail = true,
+                    isStagedTask = true,
+                    iconView = it.iconView.asView()
+                )
+            }
         }
     }
 
@@ -173,7 +175,7 @@
      *   (opposite of that representing [taskIdAttributeContainer])
      */
     fun addInitialSplitFromPair(
-        taskIdAttributeContainer: TaskIdAttributeContainer,
+        taskIdAttributeContainer: TaskContainer,
         builder: PendingAnimation,
         deviceProfile: DeviceProfile,
         taskViewWidth: Int,
@@ -561,8 +563,13 @@
                 // Launch split app pair animation
                 composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
             } else {
-                composeFullscreenIconSplitLaunchAnimator(launchingIconView, info, t,
-                        finishCallback, appPairLaunchingAppIndex)
+                composeFullscreenIconSplitLaunchAnimator(
+                    launchingIconView,
+                    info,
+                    t,
+                    finishCallback,
+                    appPairLaunchingAppIndex
+                )
             }
         } else {
             // Fallback case: simple fade-in animation
@@ -629,18 +636,22 @@
 
     /**
      * @return -1 if [transitionInfo] contains both apps of the app pair to be animated, otherwise
-     *         the integer index corresponding to [launchingIconView]'s contents for the single app
-     *         to be animated
+     *   the integer index corresponding to [launchingIconView]'s contents for the single app to be
+     *   animated
      */
-    fun hasChangesForBothAppPairs(launchingIconView: AppPairIcon,
-                                          transitionInfo: TransitionInfo) : Int {
+    fun hasChangesForBothAppPairs(
+        launchingIconView: AppPairIcon,
+        transitionInfo: TransitionInfo
+    ): Int {
         val intent1 = launchingIconView.info.getFirstApp().intent.component?.packageName
         val intent2 = launchingIconView.info.getSecondApp().intent.component?.packageName
         var launchFullscreenAppIndex = -1
         for (change in transitionInfo.changes) {
             val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
-            if (TransitionUtil.isOpeningType(change.mode) &&
-                    taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN) {
+            if (
+                TransitionUtil.isOpeningType(change.mode) &&
+                    taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN
+            ) {
                 val baseIntent = taskInfo.baseIntent.component?.packageName
                 if (baseIntent == intent1) {
                     if (launchFullscreenAppIndex > -1) {
@@ -695,8 +706,12 @@
         // If launching an app pair from Taskbar inside of an app context (no access to Launcher),
         // use the scale-up animation
         if (launchingIconView.context is TaskbarActivityContext) {
-            composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback,
-                    WINDOWING_MODE_MULTI_WINDOW)
+            composeScaleUpLaunchAnimation(
+                transitionInfo,
+                t,
+                finishCallback,
+                WINDOWING_MODE_MULTI_WINDOW
+            )
             return
         }
 
@@ -767,28 +782,32 @@
         floatingView.bringToFront()
 
         launchAnimation.play(
-                getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView,
-                        rootCandidate))
+            getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView, rootCandidate)
+        )
         launchAnimation.start()
     }
 
     /**
-     * Similar to [composeIconSplitLaunchAnimator], but instructs [FloatingAppPairView] to animate
-     * a single fullscreen icon + background instead of for a pair
+     * Similar to [composeIconSplitLaunchAnimator], but instructs [FloatingAppPairView] to animate a
+     * single fullscreen icon + background instead of for a pair
      */
     @VisibleForTesting
     fun composeFullscreenIconSplitLaunchAnimator(
-            launchingIconView: AppPairIcon,
-            transitionInfo: TransitionInfo,
-            t: Transaction,
-            finishCallback: Runnable,
-            launchFullscreenIndex: Int
+        launchingIconView: AppPairIcon,
+        transitionInfo: TransitionInfo,
+        t: Transaction,
+        finishCallback: Runnable,
+        launchFullscreenIndex: Int
     ) {
         // If launching an app pair from Taskbar inside of an app context (no access to Launcher),
         // use the scale-up animation
         if (launchingIconView.context is TaskbarActivityContext) {
-            composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback,
-                    WINDOWING_MODE_FULLSCREEN)
+            composeScaleUpLaunchAnimation(
+                transitionInfo,
+                t,
+                finishCallback,
+                WINDOWING_MODE_FULLSCREEN
+            )
             return
         }
 
@@ -799,16 +818,18 @@
         // Create an AnimatorSet that will run both shell and launcher transitions together
         val launchAnimation = AnimatorSet()
 
-        val appInfo = launchingIconView.info
-                .getContents()[launchFullscreenIndex] as WorkspaceItemInfo
+        val appInfo =
+            launchingIconView.info.getContents()[launchFullscreenIndex] as WorkspaceItemInfo
         val intentToLaunch = appInfo.intent.component?.packageName
         var rootCandidate: Change? = null
         for (change in transitionInfo.changes) {
             val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
             val baseIntent = taskInfo.baseIntent.component?.packageName
-            if (TransitionUtil.isOpeningType(change.mode) &&
+            if (
+                TransitionUtil.isOpeningType(change.mode) &&
                     taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN &&
-                    baseIntent == intentToLaunch) {
+                    baseIntent == intentToLaunch
+            ) {
                 rootCandidate = change
             }
         }
@@ -832,26 +853,28 @@
         appIcon.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
 
         val floatingView =
-                FloatingAppPairView.getFloatingAppPairView(
-                        launcher,
-                        drawableArea,
-                        appIcon,
-                        null /*appIcon2*/,
-                        0 /*dividerPos*/
-                )
+            FloatingAppPairView.getFloatingAppPairView(
+                launcher,
+                drawableArea,
+                appIcon,
+                null /*appIcon2*/,
+                0 /*dividerPos*/
+            )
         floatingView.bringToFront()
         launchAnimation.play(
-                getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView,
-                        rootCandidate))
+            getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView, rootCandidate)
+        )
         launchAnimation.start()
     }
 
-    private fun getIconLaunchValueAnimator(t: Transaction,
-                                           dp: com.android.launcher3.DeviceProfile,
-                                           finishCallback: Runnable,
-                                           launcher: QuickstepLauncher,
-                                           floatingView: FloatingAppPairView,
-                                           rootCandidate: Change) : ValueAnimator {
+    private fun getIconLaunchValueAnimator(
+        t: Transaction,
+        dp: com.android.launcher3.DeviceProfile,
+        finishCallback: Runnable,
+        launcher: QuickstepLauncher,
+        floatingView: FloatingAppPairView,
+        rootCandidate: Change
+    ): ValueAnimator {
         val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
         val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet)
         progressUpdater.setDuration(timings.getDuration().toLong())
@@ -860,12 +883,12 @@
         // Shell animation: the apps are revealed toward end of the launch animation
         progressUpdater.addUpdateListener { valueAnimator: ValueAnimator ->
             val progress =
-                    Interpolators.clampToProgress(
-                            Interpolators.LINEAR,
-                            valueAnimator.animatedFraction,
-                            timings.appRevealStartOffset,
-                            timings.appRevealEndOffset
-                    )
+                Interpolators.clampToProgress(
+                    Interpolators.LINEAR,
+                    valueAnimator.animatedFraction,
+                    timings.appRevealStartOffset,
+                    timings.appRevealEndOffset
+                )
 
             // Set the alpha of the shell layer (2 apps + divider)
             t.setAlpha(rootCandidate.leash, progress)
@@ -873,65 +896,65 @@
         }
 
         progressUpdater.addUpdateListener(
-                object : MultiValueUpdateListener() {
-                    var mDx =
-                            FloatProp(
-                                    floatingView.startingPosition.left,
-                                    dp.widthPx / 2f - floatingView.startingPosition.width() / 2f,
-                                    Interpolators.clampToProgress(
-                                            timings.getStagedRectXInterpolator(),
-                                            timings.stagedRectSlideStartOffset,
-                                            timings.stagedRectSlideEndOffset
-                                    )
-                            )
-                    var mDy =
-                            FloatProp(
-                                    floatingView.startingPosition.top,
-                                    dp.heightPx / 2f - floatingView.startingPosition.height() / 2f,
-                                    Interpolators.clampToProgress(
-                                            Interpolators.EMPHASIZED,
-                                            timings.stagedRectSlideStartOffset,
-                                            timings.stagedRectSlideEndOffset
-                                    )
-                            )
-                    var mScaleX =
-                            FloatProp(
-                                    1f /* start */,
-                                    dp.widthPx / floatingView.startingPosition.width(),
-                                    Interpolators.clampToProgress(
-                                            Interpolators.EMPHASIZED,
-                                            timings.stagedRectSlideStartOffset,
-                                            timings.stagedRectSlideEndOffset
-                                    )
-                            )
-                    var mScaleY =
-                            FloatProp(
-                                    1f /* start */,
-                                    dp.heightPx / floatingView.startingPosition.height(),
-                                    Interpolators.clampToProgress(
-                                            Interpolators.EMPHASIZED,
-                                            timings.stagedRectSlideStartOffset,
-                                            timings.stagedRectSlideEndOffset
-                                    )
-                            )
+            object : MultiValueUpdateListener() {
+                var mDx =
+                    FloatProp(
+                        floatingView.startingPosition.left,
+                        dp.widthPx / 2f - floatingView.startingPosition.width() / 2f,
+                        Interpolators.clampToProgress(
+                            timings.getStagedRectXInterpolator(),
+                            timings.stagedRectSlideStartOffset,
+                            timings.stagedRectSlideEndOffset
+                        )
+                    )
+                var mDy =
+                    FloatProp(
+                        floatingView.startingPosition.top,
+                        dp.heightPx / 2f - floatingView.startingPosition.height() / 2f,
+                        Interpolators.clampToProgress(
+                            Interpolators.EMPHASIZED,
+                            timings.stagedRectSlideStartOffset,
+                            timings.stagedRectSlideEndOffset
+                        )
+                    )
+                var mScaleX =
+                    FloatProp(
+                        1f /* start */,
+                        dp.widthPx / floatingView.startingPosition.width(),
+                        Interpolators.clampToProgress(
+                            Interpolators.EMPHASIZED,
+                            timings.stagedRectSlideStartOffset,
+                            timings.stagedRectSlideEndOffset
+                        )
+                    )
+                var mScaleY =
+                    FloatProp(
+                        1f /* start */,
+                        dp.heightPx / floatingView.startingPosition.height(),
+                        Interpolators.clampToProgress(
+                            Interpolators.EMPHASIZED,
+                            timings.stagedRectSlideStartOffset,
+                            timings.stagedRectSlideEndOffset
+                        )
+                    )
 
-                    override fun onUpdate(percent: Float, initOnly: Boolean) {
-                        floatingView.progress = percent
-                        floatingView.x = mDx.value
-                        floatingView.y = mDy.value
-                        floatingView.scaleX = mScaleX.value
-                        floatingView.scaleY = mScaleY.value
-                        floatingView.invalidate()
-                    }
+                override fun onUpdate(percent: Float, initOnly: Boolean) {
+                    floatingView.progress = percent
+                    floatingView.x = mDx.value
+                    floatingView.y = mDy.value
+                    floatingView.scaleX = mScaleX.value
+                    floatingView.scaleY = mScaleY.value
+                    floatingView.invalidate()
                 }
+            }
         )
         progressUpdater.addListener(
-                object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator) {
-                        safeRemoveViewFromDragLayer(launcher, floatingView)
-                        finishCallback.run()
-                    }
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    safeRemoveViewFromDragLayer(launcher, floatingView)
+                    finishCallback.run()
                 }
+            }
         )
 
         return progressUpdater
@@ -939,9 +962,8 @@
 
     /**
      * This is a scale-up-and-fade-in animation (34% to 100%) for launching an app in Overview when
-     * there is no visible associated tile to expand from.
-     * [windowingMode] helps determine whether we are looking for a split or a single fullscreen
-     * [Change]
+     * there is no visible associated tile to expand from. [windowingMode] helps determine whether
+     * we are looking for a split or a single fullscreen [Change]
      */
     @VisibleForTesting
     fun composeScaleUpLaunchAnimation(
@@ -1095,12 +1117,12 @@
         animator.setDuration(QuickstepTransitionManager.SPLIT_LAUNCH_DURATION.toLong())
         animator.addUpdateListener { valueAnimator: ValueAnimator ->
             val progress =
-                    Interpolators.clampToProgress(
-                            Interpolators.LINEAR,
-                            valueAnimator.animatedFraction,
-                            0.8f,
-                            1f
-                    )
+                Interpolators.clampToProgress(
+                    Interpolators.LINEAR,
+                    valueAnimator.animatedFraction,
+                    0.8f,
+                    1f
+                )
             for (leash in openingTargets) {
                 animTransaction.setAlpha(leash, progress)
             }
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index 4915b62..57bc2d7 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -16,7 +16,7 @@
 
 package com.android.quickstep.views;
 
-import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import  static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
@@ -31,7 +31,6 @@
 import android.graphics.drawable.shapes.RoundRectShape;
 import android.util.AttributeSet;
 import android.util.Log;
-import android.util.SparseArray;
 import android.view.View;
 import android.widget.FrameLayout;
 
@@ -43,6 +42,7 @@
 import com.android.launcher3.desktop.DesktopRecentsTransitionController;
 import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.util.ViewPool;
 import com.android.quickstep.BaseContainerInterface;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskThumbnailCache;
@@ -70,14 +70,6 @@
 
     private static final boolean DEBUG = false;
 
-    @NonNull
-    private List<Task> mTasks = new ArrayList<>();
-
-    private final ArrayList<TaskThumbnailViewDeprecated> mSnapshotViews = new ArrayList<>();
-
-    /** Maps {@code taskIds} to corresponding {@link TaskThumbnailViewDeprecated}s */
-    private final SparseArray<TaskThumbnailViewDeprecated> mSnapshotViewMap = new SparseArray<>();
-
     private final ArrayList<CancellableTask<?>> mPendingThumbnailRequests = new ArrayList<>();
 
     private final TaskView.FullscreenDrawParams mSnapshotDrawParams;
@@ -88,6 +80,8 @@
 
     private final PointF mTempPointF = new PointF();
 
+    private final ViewPool<TaskThumbnailViewDeprecated> mTaskthumbnailViewPool;
+
     public DesktopTaskView(Context context) {
         this(context, null);
     }
@@ -110,6 +104,10 @@
                 return QuickStepContract.getWindowCornerRadius(context);
             }
         };
+        // As DesktopTaskView is inflated in background, use initialSize=0 to avoid initPool.
+        mTaskthumbnailViewPool = new ViewPool<>(context, this, R.layout.task_thumbnail,
+                /* maxSize= */10, /* initialSize= */ 0);
+        mTaskContainers = new ArrayList<>();
     }
 
     @Override
@@ -146,8 +144,7 @@
             mContainer.getDragLayer().getDescendantRectRelativeToSelf(mBackgroundView, bounds);
         } else {
             bounds.set(mBackgroundView.getLeft(), mBackgroundView.getTop(),
-                    mBackgroundView.getRight(),
-                    mBackgroundView.getBottom());
+                    mBackgroundView.getRight(), mBackgroundView.getBottom());
         }
         return Unit.INSTANCE;
     }
@@ -171,83 +168,36 @@
         }
         cancelPendingLoadTasks();
 
-        mTasks = new ArrayList<>(tasks);
-        mSnapshotViewMap.clear();
-
-        // Ensure there are equal number of snapshot views and tasks.
-        // More tasks than views, add views. More views than tasks, remove views.
-        // TODO(b/251586230): use a ViewPool for creating TaskThumbnailViews
-        if (mSnapshotViews.size() > mTasks.size()) {
-            int diff = mSnapshotViews.size() - mTasks.size();
-            for (int i = 0; i < diff; i++) {
-                TaskThumbnailViewDeprecated snapshotView = mSnapshotViews.remove(0);
-                removeView(snapshotView);
-            }
-        } else if (mSnapshotViews.size() < mTasks.size()) {
-            int diff = mTasks.size() - mSnapshotViews.size();
-            for (int i = 0; i < diff; i++) {
-                TaskThumbnailViewDeprecated snapshotView =
-                        new TaskThumbnailViewDeprecated(getContext());
-                mSnapshotViews.add(snapshotView);
-                // Add snapshots from to position after the initial child views.
-                addView(snapshotView, mChildCountAtInflation,
+        ((ArrayList<TaskContainer>) mTaskContainers).ensureCapacity(tasks.size());
+        for (int i = 0; i < tasks.size(); i++) {
+            Task task = tasks.get(i);
+            TaskThumbnailViewDeprecated thumbnailView;
+            if (i >= mTaskContainers.size()) {
+                thumbnailView = mTaskthumbnailViewPool.getView();
+                // Add thumbnailView from to position after the initial child views.
+                addView(thumbnailView, mChildCountAtInflation,
                         new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
+            } else {
+                thumbnailView = mTaskContainers.get(i).getThumbnailView();
+            }
+            thumbnailView.bind(task);
+            TaskContainer taskContainer = new TaskContainer(task, thumbnailView, mIconView,
+                    STAGE_POSITION_UNDEFINED, /*digitalWellBeingToast=*/ null);
+            if (i >= mTaskContainers.size()) {
+                mTaskContainers.add(taskContainer);
+            } else {
+                mTaskContainers.set(i, taskContainer);
             }
         }
-
-        for (int i = 0; i < mTasks.size(); i++) {
-            Task task = mTasks.get(i);
-            TaskThumbnailViewDeprecated snapshotView = mSnapshotViews.get(i);
-            snapshotView.bind(task);
-            mSnapshotViewMap.put(task.key.id, snapshotView);
+        while (mTaskContainers.size() > tasks.size()) {
+            TaskContainer taskContainer = mTaskContainers.remove(mTaskContainers.size() - 1);
+            removeView(taskContainer.getThumbnailView());
+            mTaskthumbnailViewPool.recycle(taskContainer.getThumbnailView());
         }
 
-        updateTaskIdContainer();
-        updateTaskIdAttributeContainer();
-
         setOrientationState(orientedState);
     }
 
-    private void updateTaskIdContainer() {
-        mTaskIdContainer = new int[mTasks.size()];
-        for (int i = 0; i < mTasks.size(); i++) {
-            mTaskIdContainer[i] = mTasks.get(i).key.id;
-        }
-    }
-
-    private void updateTaskIdAttributeContainer() {
-        mTaskIdAttributeContainer = new TaskIdAttributeContainer[mTasks.size()];
-        for (int i = 0; i < mTasks.size(); i++) {
-            Task task = mTasks.get(i);
-            TaskThumbnailViewDeprecated thumbnailView = mSnapshotViewMap.get(task.key.id);
-            mTaskIdAttributeContainer[i] = createAttributeContainer(task, thumbnailView);
-        }
-    }
-
-    private TaskIdAttributeContainer createAttributeContainer(Task task,
-            TaskThumbnailViewDeprecated thumbnailView) {
-        return new TaskIdAttributeContainer(task, thumbnailView, mIconView,
-                STAGE_POSITION_UNDEFINED);
-    }
-
-    @Nullable
-    @Override
-    public Task getTask() {
-        // TODO(b/249371338): returning first task. This won't work well with multiple tasks.
-        return mTasks.size() > 0 ? mTasks.get(0) : null;
-    }
-
-    @Override
-    public TaskThumbnailViewDeprecated getThumbnail() {
-        // TODO(b/249371338): returning single thumbnail. This won't work well with multiple tasks.
-        Task task = getTask();
-        if (task != null) {
-            return mSnapshotViewMap.get(task.key.id);
-        }
-        // Return the place holder snapshot views. Callers expect this to be non-null
-        return mTaskThumbnailViewDeprecated;
-    }
-
     @Override
     public void onTaskListVisibilityChanged(boolean visible, int changes) {
         cancelPendingLoadTasks();
@@ -256,15 +206,12 @@
             TaskThumbnailCache thumbnailCache = model.getThumbnailCache();
 
             if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
-                for (Task task : mTasks) {
+                for (TaskContainer container : mTaskContainers) {
                     CancellableTask<?> thumbLoadRequest =
-                            thumbnailCache.updateThumbnailInBackground(task, thumbnailData -> {
-                                TaskThumbnailViewDeprecated thumbnailView =
-                                        mSnapshotViewMap.get(task.key.id);
-                                if (thumbnailView != null) {
-                                    thumbnailView.setThumbnail(task, thumbnailData);
-                                }
-                            });
+                            thumbnailCache.updateThumbnailInBackground(container.getTask(),
+                                    thumbnailData -> container.getThumbnailView().setThumbnail(
+                                            container.getTask(),
+                                            thumbnailData));
                     if (thumbLoadRequest != null) {
                         mPendingThumbnailRequests.add(thumbLoadRequest);
                     }
@@ -272,13 +219,10 @@
             }
         } else {
             if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
-                for (Task task : mTasks) {
-                    TaskThumbnailViewDeprecated thumbnailView = mSnapshotViewMap.get(task.key.id);
-                    if (thumbnailView != null) {
-                        thumbnailView.setThumbnail(null, null);
-                    }
+                for (TaskContainer container : mTaskContainers) {
+                    container.getThumbnailView().setThumbnail(null, null);
                     // Reset the task thumbnail ref
-                    task.thumbnail = null;
+                    container.getTask().thumbnail = null;
                 }
             }
         }
@@ -327,42 +271,19 @@
     }
 
     @Override
-    public boolean isDesktopTask() {
-        return true;
-    }
-
-    @Override
     void refreshThumbnails(@Nullable HashMap<Integer, ThumbnailData> thumbnailDatas) {
         // Sets new thumbnails based on the incoming data and refreshes the rest.
-        // Create a copy of the thumbnail map, so we can track thumbnails that need refreshing.
-        SparseArray<TaskThumbnailViewDeprecated> thumbnailsToRefresh = mSnapshotViewMap.clone();
         if (thumbnailDatas != null) {
-            for (Task task : mTasks) {
-                int key = task.key.id;
-                TaskThumbnailViewDeprecated thumbnailView = thumbnailsToRefresh.get(key);
-                ThumbnailData thumbnailData = thumbnailDatas.get(key);
-                if (thumbnailView != null && thumbnailData != null) {
-                    thumbnailView.setThumbnail(task, thumbnailData);
-                    // Remove this thumbnail from the list that should be refreshed.
-                    thumbnailsToRefresh.remove(key);
+            for (TaskContainer container : mTaskContainers) {
+                ThumbnailData thumbnailData = thumbnailDatas.get(container.getTask().key.id);
+                if (thumbnailData != null) {
+                    container.getThumbnailView().setThumbnail(container.getTask(), thumbnailData);
+                } else {
+                    // Refresh the rest that were not updated.
+                    container.getThumbnailView().refresh();
                 }
             }
         }
-
-        // Refresh the rest that were not updated.
-        for (int i = 0; i < thumbnailsToRefresh.size(); i++) {
-            thumbnailsToRefresh.valueAt(i).refresh();
-        }
-    }
-
-    @Override
-    public TaskThumbnailViewDeprecated[] getThumbnails() {
-        TaskThumbnailViewDeprecated[] thumbnails =
-                new TaskThumbnailViewDeprecated[mSnapshotViewMap.size()];
-        for (int i = 0; i < thumbnails.length; i++) {
-            thumbnails[i] = mSnapshotViewMap.valueAt(i);
-        }
-        return thumbnails;
     }
 
     @Override
@@ -370,11 +291,8 @@
         resetPersistentViewTransforms();
         // Clear any references to the thumbnail (it will be re-read either from the cache or the
         // system on next bind)
-        for (Task task : mTasks) {
-            TaskThumbnailViewDeprecated thumbnailView = mSnapshotViewMap.get(task.key.id);
-            if (thumbnailView != null) {
-                thumbnailView.setThumbnail(task, null);
-            }
+        for (TaskContainer container : mTaskContainers) {
+            container.getThumbnailView().setThumbnail(container.getTask(), null);
         }
         setOverlayEnabled(false);
         onTaskListVisibilityChanged(false);
@@ -392,8 +310,7 @@
         int thumbnailTopMarginPx = mContainer.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
         containerHeight -= thumbnailTopMarginPx;
 
-        int thumbnails = mSnapshotViewMap.size();
-        if (thumbnails == 0) {
+        if (mTaskContainers.isEmpty()) {
             return;
         }
 
@@ -413,8 +330,8 @@
         }
 
         // Desktop tile is a shrunk down version of launcher and freeform task thumbnails.
-        for (int i = 0; i < mTasks.size(); i++) {
-            Task task = mTasks.get(i);
+        for (TaskContainer container : mTaskContainers) {
+            Task task = container.getTask();
             Rect taskSize = task.appBounds;
             if (taskSize == null) {
                 // Default to quarter of the desktop if we did not get app bounds.
@@ -424,7 +341,7 @@
             int thumbWidth = (int) (taskSize.width() * scaleWidth);
             int thumbHeight = (int) (taskSize.height() * scaleHeight);
 
-            TaskThumbnailViewDeprecated thumbnailView = mSnapshotViewMap.get(task.key.id);
+            TaskThumbnailViewDeprecated thumbnailView = container.getThumbnailView();
             if (thumbnailView != null) {
                 thumbnailView.measure(MeasureSpec.makeMeasureSpec(thumbWidth, MeasureSpec.EXACTLY),
                         MeasureSpec.makeMeasureSpec(thumbHeight, MeasureSpec.EXACTLY));
@@ -466,9 +383,8 @@
         } else {
             mBackgroundView.setVisibility(VISIBLE);
         }
-        for (int i = 0; i < mSnapshotViewMap.size(); i++) {
-            TaskThumbnailViewDeprecated thumbnailView = mSnapshotViewMap.valueAt(i);
-            thumbnailView.getTaskOverlay().setFullscreenProgress(progress);
+        for (TaskContainer container : mTaskContainers) {
+            container.getThumbnailView().getTaskOverlay().setFullscreenProgress(progress);
         }
         // Animate icons and DWB banners in/out, except in QuickSwitch state, when tiles are
         // oversized and banner would look disproportionately large.
@@ -482,33 +398,30 @@
     @Override
     protected void updateSnapshotRadius() {
         super.updateSnapshotRadius();
-        for (int i = 0; i < mSnapshotViewMap.size(); i++) {
-            if (i == 0) {
-                // All snapshots share the same params. Only update it with the first snapshot.
-                updateFullscreenParams(mSnapshotDrawParams);
-            }
-            mSnapshotViewMap.valueAt(i).setFullscreenParams(mSnapshotDrawParams);
+        updateFullscreenParams(mSnapshotDrawParams);
+        for (TaskContainer container : mTaskContainers) {
+            container.getThumbnailView().setFullscreenParams(mSnapshotDrawParams);
         }
     }
 
     @Override
     public void setColorTint(float amount, int tintColor) {
-        for (int i = 0; i < mSnapshotViewMap.size(); i++) {
-            mSnapshotViewMap.valueAt(i).setDimAlpha(amount);
+        for (TaskContainer container : mTaskContainers) {
+            container.getThumbnailView().setDimAlpha(amount);
         }
     }
 
     @Override
     protected void applyThumbnailSplashAlpha() {
-        for (int i = 0; i < mSnapshotViewMap.size(); i++) {
-            mSnapshotViewMap.valueAt(i).setSplashAlpha(mTaskThumbnailSplashAlpha);
+        for (TaskContainer container : mTaskContainers) {
+            container.getThumbnailView().setSplashAlpha(mTaskThumbnailSplashAlpha);
         }
     }
 
     @Override
     void setThumbnailVisibility(int visibility, int taskId) {
-        for (int i = 0; i < mSnapshotViewMap.size(); i++) {
-            mSnapshotViewMap.valueAt(i).setVisibility(visibility);
+        for (TaskContainer container : mTaskContainers) {
+            container.getThumbnailView().setVisibility(visibility);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
index 82ba30b..f92b9dd 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -320,12 +320,12 @@
                 (FrameLayout.LayoutParams) mBanner.getLayoutParams();
         DeviceProfile deviceProfile = mContainer.getDeviceProfile();
         layoutParams.bottomMargin = ((ViewGroup.MarginLayoutParams)
-                mTaskView.getThumbnail().getLayoutParams()).bottomMargin;
+                mTaskView.getFirstThumbnailView().getLayoutParams()).bottomMargin;
         RecentsPagedOrientationHandler orientationHandler = mTaskView.getPagedOrientationHandler();
         Pair<Float, Float> translations = orientationHandler
                 .getDwbLayoutTranslations(mTaskView.getMeasuredWidth(),
                         mTaskView.getMeasuredHeight(), mSplitBounds, deviceProfile,
-                        mTaskView.getThumbnails(), mTask.key.id, mBanner);
+                        mTaskView.getThumbnailViews(), mTask.key.id, mBanner);
         mSplitOffsetTranslationX = translations.first;
         mSplitOffsetTranslationY = translations.second;
         updateTranslationY();
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index b2a8503..1b4d22f 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -64,8 +64,6 @@
 public class GroupedTaskView extends TaskView {
 
     private static final String TAG = GroupedTaskView.class.getSimpleName();
-    @Nullable
-    private Task mSecondaryTask;
     // TODO(b/336612373): Support new TTV for GroupedTaskView
     private TaskThumbnailViewDeprecated mSnapshotView2;
     private TaskViewIcon mIconView2;
@@ -92,6 +90,17 @@
         mDigitalWellBeingToast2 = new DigitalWellBeingToast(mContainer, this);
     }
 
+    /**
+     * Returns the second task bound to this TaskView.
+     *
+     * @deprecated Use {@link #mTaskContainers} instead.
+     */
+    @Deprecated
+    @Nullable
+    private Task getSecondTask() {
+        return mTaskContainers.size() > 1 ? mTaskContainers.get(1).getTask() : null;
+    }
+
     @Override
     public Unit getThumbnailBounds(@NonNull Rect bounds, boolean relativeToDragLayer) {
         if (mSplitBoundsConfig == null) {
@@ -141,13 +150,11 @@
     public void bind(Task primary, Task secondary, RecentsOrientedState orientedState,
             @Nullable SplitBounds splitBoundsConfig) {
         super.bind(primary, orientedState);
-        mSecondaryTask = secondary;
-        mTaskIdContainer = new int[]{mTaskIdContainer[0], secondary.key.id};
-        mTaskIdAttributeContainer = new TaskIdAttributeContainer[]{
-                mTaskIdAttributeContainer[0],
-                new TaskIdAttributeContainer(secondary, mSnapshotView2,
-                        mIconView2, STAGE_POSITION_BOTTOM_OR_RIGHT)};
-        mTaskIdAttributeContainer[0].setStagePosition(
+        mTaskContainers = Arrays.asList(
+                mTaskContainers.get(0),
+                new TaskContainer(secondary, findViewById(R.id.bottomright_snapshot),
+                        mIconView2, STAGE_POSITION_BOTTOM_OR_RIGHT, mDigitalWellBeingToast2));
+        mTaskContainers.get(0).setStagePosition(
                 SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT);
         mSnapshotView2.bind(secondary);
         mSplitBoundsConfig = splitBoundsConfig;
@@ -169,12 +176,12 @@
     public void setUpShowAllInstancesListener() {
         // sets up the listener for the left/top task
         super.setUpShowAllInstancesListener();
-        if (mTaskIdAttributeContainer.length < 2) {
+        if (mTaskContainers.size() < 2) {
             return;
         }
 
         // right/bottom task's base package name
-        String taskPackageName = mTaskIdAttributeContainer[1].getTask().key.getPackageName();
+        String taskPackageName = mTaskContainers.get(1).getTask().key.getPackageName();
 
         // icon of the right/bottom task
         View showWindowsView = findViewById(R.id.show_windows_right);
@@ -190,20 +197,21 @@
             TaskIconCache iconCache = model.getIconCache();
 
             if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
-                mThumbnailLoadRequest2 = thumbnailCache.updateThumbnailInBackground(mSecondaryTask,
-                        thumbnailData -> mSnapshotView2.setThumbnail(
-                                mSecondaryTask, thumbnailData
+                mThumbnailLoadRequest2 = thumbnailCache.updateThumbnailInBackground(
+                        getSecondTask(),
+                        thumbnailData -> mSnapshotView2.setThumbnail(getSecondTask(),
+                                thumbnailData
                         ));
             }
 
             if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
-                mIconLoadRequest2 = iconCache.updateIconInBackground(mSecondaryTask,
+                mIconLoadRequest2 = iconCache.updateIconInBackground(getSecondTask(),
                         (task) -> {
                             setIcon(mIconView2, task.icon);
                             if (enableOverviewIconMenu()) {
                                 setText(mIconView2, task.title);
                             }
-                            mDigitalWellBeingToast2.initialize(mSecondaryTask);
+                            mDigitalWellBeingToast2.initialize(getSecondTask());
                             mDigitalWellBeingToast2.setSplitConfiguration(mSplitBoundsConfig);
                             mDigitalWellBeingToast.setSplitConfiguration(mSplitBoundsConfig);
                         });
@@ -213,7 +221,7 @@
                 mSnapshotView2.setThumbnail(null, null);
                 // Reset the task thumbnail reference as well (it will be fetched from the cache or
                 // reloaded next time we need it)
-                mSecondaryTask.thumbnail = null;
+                getSecondTask().thumbnail = null;
             }
             if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
                 setIcon(mIconView2, null);
@@ -271,7 +279,7 @@
     @Nullable
     @Override
     public RunnableList launchTaskAnimated() {
-        if (mTask == null || mSecondaryTask == null) {
+        if (mTaskContainers.isEmpty()) {
             return null;
         }
 
@@ -304,8 +312,8 @@
     private void launchTaskInternal(@NonNull Consumer<Boolean> callback, boolean isQuickswitch,
             boolean launchingExistingTaskView) {
         getRecentsView().getSplitSelectController().launchExistingSplitPair(
-                launchingExistingTaskView ? this : null, mTask.key.id,
-                mSecondaryTask.key.id, SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT,
+                launchingExistingTaskView ? this : null, getFirstTask().key.id,
+                getSecondTask().key.id, SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT,
                 callback, isQuickswitch, getSnapPosition());
         Log.d(TAG, "launchTaskInternal - launchExistingSplitPair: " + Arrays.toString(
                 getTaskIds()));
@@ -314,10 +322,10 @@
     @Override
     void refreshThumbnails(@Nullable HashMap<Integer, ThumbnailData> thumbnailDatas) {
         super.refreshThumbnails(thumbnailDatas);
-        if (mSecondaryTask != null && thumbnailDatas != null) {
-            final ThumbnailData thumbnailData = thumbnailDatas.get(mSecondaryTask.key.id);
+        if (getSecondTask() != null && thumbnailDatas != null) {
+            final ThumbnailData thumbnailData = thumbnailDatas.get(getSecondTask().key.id);
             if (thumbnailData != null) {
-                mSnapshotView2.setThumbnail(mSecondaryTask, thumbnailData);
+                mSnapshotView2.setThumbnail(getSecondTask(), thumbnailData);
                 return;
             }
         }
@@ -325,11 +333,6 @@
         mSnapshotView2.refresh();
     }
 
-    @Override
-    public TaskThumbnailViewDeprecated[] getThumbnails() {
-        return new TaskThumbnailViewDeprecated[]{mTaskThumbnailViewDeprecated, mSnapshotView2};
-    }
-
     /**
      * Returns taskId that split selection was initiated with,
      * {@link ActivityTaskManager#INVALID_TASK_ID} if no tasks in this TaskView are part of
@@ -350,7 +353,7 @@
             // below aren't reliable since both of those views may be gone/transformed
             int initSplitTaskId = getThisTaskCurrentlyInSplitSelection();
             if (initSplitTaskId != INVALID_TASK_ID) {
-                return initSplitTaskId == mTask.key.id ? 1 : 0;
+                return initSplitTaskId == getFirstTask().key.id ? 1 : 0;
             }
         }
 
@@ -371,7 +374,7 @@
     @Override
     public void onRecycle() {
         super.onRecycle();
-        mSnapshotView2.setThumbnail(mSecondaryTask, null);
+        mSnapshotView2.setThumbnail(getSecondTask(), null);
         mSplitBoundsConfig = null;
     }
 
@@ -404,9 +407,8 @@
         } else {
             // Currently being split with this taskView, let the non-split selected thumbnail
             // take up full thumbnail area
-            Optional<TaskIdAttributeContainer> nonSplitContainer = Arrays.stream(
-                    mTaskIdAttributeContainer).filter(
-                            container -> container.getTask().key.id != initSplitTaskId).findAny();
+            Optional<TaskContainer> nonSplitContainer = mTaskContainers.stream().filter(
+                    container -> container.getTask().key.id != initSplitTaskId).findAny();
             nonSplitContainer.ifPresent(
                     taskIdAttributeContainer -> taskIdAttributeContainer.getThumbnailView().measure(
                             widthMeasureSpec, MeasureSpec.makeMeasureSpec(
@@ -492,10 +494,10 @@
     }
 
     private void updateSecondaryDwbPlacement() {
-        if (mSecondaryTask == null) {
+        if (getSecondTask() == null) {
             return;
         }
-        mDigitalWellBeingToast2.initialize(mSecondaryTask);
+        mDigitalWellBeingToast2.initialize(getSecondTask());
     }
 
     @Override
@@ -548,17 +550,11 @@
      */
     @Override
     void setThumbnailVisibility(int visibility, int taskId) {
-        if (visibility == VISIBLE) {
-            mTaskThumbnailViewDeprecated.setVisibility(visibility);
-            mDigitalWellBeingToast.setBannerVisibility(visibility);
-            mSnapshotView2.setVisibility(visibility);
-            mDigitalWellBeingToast2.setBannerVisibility(visibility);
-        } else if (mTaskIdContainer.length > 0 && mTaskIdContainer[0] == taskId) {
-            mTaskThumbnailViewDeprecated.setVisibility(visibility);
-            mDigitalWellBeingToast.setBannerVisibility(visibility);
-        } else {
-            mSnapshotView2.setVisibility(visibility);
-            mDigitalWellBeingToast2.setBannerVisibility(visibility);
+        for (TaskContainer container : mTaskContainers) {
+            if (visibility == VISIBLE || container.getTask().key.id == taskId) {
+                container.getThumbnailView().setVisibility(visibility);
+                container.getDigitalWellBeingToast().setBannerVisibility(visibility);
+            }
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index 5188d4a..8a917d6 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -20,6 +20,7 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.util.FloatProperty;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.Button;
@@ -33,9 +34,7 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.R;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.DisplayController;
-import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.NavigationMode;
 import com.android.quickstep.TaskOverlayFactory.OverlayUICallbacks;
@@ -43,12 +42,26 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.function.Consumer;
 
 /**
  * View for showing action buttons in Overview
  */
 public class OverviewActionsView<T extends OverlayUICallbacks> extends FrameLayout
         implements OnClickListener, Insettable {
+    public static final FloatProperty<Consumer<Float>> FLOAT_SETTER =
+            new FloatProperty<>("floatSetter") {
+                @Override
+                public void setValue(Consumer<Float> consumer, float v) {
+                    consumer.accept(v);
+                }
+
+                @Override
+                public Float get(Consumer<Float> consumer) {
+                    return -1f;
+                }
+            };
 
     private final Rect mInsets = new Rect();
 
@@ -89,30 +102,24 @@
     private static final int INDEX_HIDDEN_FLAGS_ALPHA = 3;
     private static final int INDEX_SHARE_TARGET_ALPHA = 4;
     private static final int INDEX_SCROLL_ALPHA = 5;
-    private static final int NUM_ALPHAS = 6;
-
-    public @interface ScreenshotButtonHiddenFlags { }
-    public static final int FLAG_MULTIPLE_TASKS_HIDE_SCREENSHOT = 1 << 0;
+    private static final int INDEX_GROUPED_ALPHA = 6;
+    private static final int INDEX_3P_LAUNCHER = 7;
+    private static final int NUM_ALPHAS = 8;
 
     public @interface SplitButtonHiddenFlags { }
     public static final int FLAG_SMALL_SCREEN_HIDE_SPLIT = 1 << 0;
-    public static final int FLAG_MULTIPLE_TASKS_HIDE_SPLIT = 1 << 1;
 
-    public @interface SplitButtonDisabledFlags { }
-    public static final int FLAG_SINGLE_TASK_DISABLE_SPLIT = 1 << 0;
+    /** Holds MultiValueAlpha values for all actions buttons */
+    private final MultiValueAlpha[] mMultiValueAlphas = new MultiValueAlpha[2];
+    /** Index used for single-task actions in the mMultiValueAlphas array */
+    private static final int ACTIONS_ALPHAS = 0;
+    /** Index used for grouped-task actions in the mMultiValueAlphas array */
+    private static final int GROUP_ACTIONS_ALPHAS = 1;
 
-    public @interface AppPairButtonHiddenFlags { }
-    public static final int FLAG_SINGLE_TASK_HIDE_APP_PAIR = 1 << 0;
-    public static final int FLAG_SMALL_SCREEN_HIDE_APP_PAIR = 1 << 1;
-    public static final int FLAG_3P_LAUNCHER_HIDE_APP_PAIR = 1 << 2;
-
-    private MultiValueAlpha mMultiValueAlpha;
-
+    /** Container for the action buttons below a focused, non-split Overview tile. */
     protected LinearLayout mActionButtons;
-    // The screenshot button is implemented as a Button in launcher3 and NexusLauncher, but is an
-    // ImageButton in go launcher (does not share a common class with Button). Take care when
-    // casting this.
-    private View mScreenshotButton;
+    /** Container for the action buttons below a focused, split Overview tile. */
+    protected LinearLayout mGroupActionButtons;
     private Button mSplitButton;
     private Button mSaveAppPairButton;
 
@@ -122,21 +129,16 @@
     @ActionsDisabledFlags
     protected int mDisabledFlags;
 
-    @ScreenshotButtonHiddenFlags
-    private int mScreenshotButtonHiddenFlags;
-
     @SplitButtonHiddenFlags
     private int mSplitButtonHiddenFlags;
 
-    @AppPairButtonHiddenFlags
-    private int mAppPairButtonHiddenFlags;
-
     @Nullable
     protected T mCallbacks;
 
     @Nullable
     protected DeviceProfile mDp;
     private final Rect mTaskSize = new Rect();
+    private boolean mIsGroupedTask = false;
 
     public OverviewActionsView(Context context) {
         this(context, null);
@@ -153,12 +155,21 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
+        // Initialize 2 view containers: one for single tasks, one for grouped tasks.
+        // These will take up the same space on the screen and alternate visibility as needed.
         mActionButtons = findViewById(R.id.action_buttons);
-        mMultiValueAlpha = new MultiValueAlpha(mActionButtons, NUM_ALPHAS);
-        mMultiValueAlpha.setUpdateVisibility(true);
+        mGroupActionButtons = findViewById(R.id.group_action_buttons);
+        // Initialize a list to set alpha on mActionButtons and mGroupActionButtons simultaneously.
+        mMultiValueAlphas[ACTIONS_ALPHAS] = new MultiValueAlpha(mActionButtons, NUM_ALPHAS);
+        mMultiValueAlphas[GROUP_ACTIONS_ALPHAS] =
+                new MultiValueAlpha(mGroupActionButtons, NUM_ALPHAS);
+        Arrays.stream(mMultiValueAlphas).forEach(a -> a.setUpdateVisibility(true));
 
-        mScreenshotButton = findViewById(R.id.action_screenshot);
-        mScreenshotButton.setOnClickListener(this);
+        // The screenshot button is implemented as a Button in launcher3 and NexusLauncher, but is
+        // an ImageButton in go launcher (does not share a common class with Button). Take care when
+        // casting this.
+        View screenshotButton = findViewById(R.id.action_screenshot);
+        screenshotButton.setOnClickListener(this);
         mSplitButton = findViewById(R.id.action_split);
         mSplitButton.setOnClickListener(this);
         mSaveAppPairButton = findViewById(R.id.action_save_app_pair);
@@ -209,7 +220,7 @@
             mHiddenFlags &= ~visibilityFlags;
         }
         boolean isHidden = mHiddenFlags != 0;
-        mMultiValueAlpha.get(INDEX_HIDDEN_FLAGS_ALPHA).setValue(isHidden ? 0 : 1);
+        setActionsAlpha(INDEX_HIDDEN_FLAGS_ALPHA, isHidden ? 0 : 1);
     }
 
     /**
@@ -236,12 +247,8 @@
      * @param isGroupedTask True if the focused task is a grouped task.
      */
     public void updateForGroupedTask(boolean isGroupedTask) {
-        // Update flags to see if split button should be hidden.
-        updateSplitButtonHiddenFlags(FLAG_MULTIPLE_TASKS_HIDE_SPLIT, isGroupedTask);
-        // Update flags to see if screenshot button should be hidden.
-        updateScreenshotButtonHiddenFlags(FLAG_MULTIPLE_TASKS_HIDE_SCREENSHOT, isGroupedTask);
-        // Update flags to see if save app pair button should be hidden.
-        updateAppPairButtonHiddenFlags(FLAG_SINGLE_TASK_HIDE_APP_PAIR, !isGroupedTask);
+        mIsGroupedTask = isGroupedTask;
+        updateActionButtonsVisibility();
     }
 
     /**
@@ -251,36 +258,30 @@
         assert mDp != null;
         // Update flags to see if split button should be hidden.
         updateSplitButtonHiddenFlags(FLAG_SMALL_SCREEN_HIDE_SPLIT, !mDp.isTablet);
-        // Update flags to see if save app pair button should be hidden.
-        updateAppPairButtonHiddenFlags(FLAG_SMALL_SCREEN_HIDE_APP_PAIR, !mDp.isTablet);
+        updateActionButtonsVisibility();
+    }
+
+    private void updateActionButtonsVisibility() {
+        assert mDp != null;
+        boolean showSingleTaskActions = !mIsGroupedTask;
+        boolean showGroupActions = mIsGroupedTask && mDp.isTablet;
+        getActionsAlphas().get(INDEX_GROUPED_ALPHA).setValue(showSingleTaskActions ? 1 : 0);
+        getGroupActionsAlphas().get(INDEX_GROUPED_ALPHA).setValue(showGroupActions ? 1 : 0);
     }
 
     /**
      * Updates flags to hide and show actions buttons for 1p/3p launchers.
      */
     public void updateFor3pLauncher(boolean is3pLauncher) {
-        updateAppPairButtonHiddenFlags(FLAG_3P_LAUNCHER_HIDE_APP_PAIR, is3pLauncher);
+        getGroupActionsAlphas().get(INDEX_3P_LAUNCHER).setValue(is3pLauncher ? 0 : 1);
     }
 
-    /**
-     * Updates the proper flags to indicate whether the "Screenshot" button should be hidden.
-     *
-     * @param flag   The flag to update.
-     * @param enable Whether to enable the hidden flag: True will cause view to be hidden.
-     */
-    private void updateScreenshotButtonHiddenFlags(@ScreenshotButtonHiddenFlags int flag,
-            boolean enable) {
-        if (mScreenshotButton == null) return;
-        if (enable) {
-            mScreenshotButtonHiddenFlags |= flag;
-        } else {
-            mScreenshotButtonHiddenFlags &= ~flag;
-        }
-        int desiredVisibility = mScreenshotButtonHiddenFlags == 0 ? VISIBLE : GONE;
-        if (mScreenshotButton.getVisibility() != desiredVisibility) {
-            mScreenshotButton.setVisibility(desiredVisibility);
-            mActionButtons.requestLayout();
-        }
+    private MultiValueAlpha getActionsAlphas() {
+        return mMultiValueAlphas[ACTIONS_ALPHAS];
+    }
+
+    private MultiValueAlpha getGroupActionsAlphas() {
+        return mMultiValueAlphas[GROUP_ACTIONS_ALPHAS];
     }
 
     /**
@@ -304,56 +305,36 @@
         }
     }
 
-    /**
-     * Updates the proper flags to indicate whether the "Save app pair" button should be disabled.
-     *
-     * @param flag   The flag to update.
-     * @param enable Whether to enable the hidden flag: True will cause view to be hidden.
-     */
-    private void updateAppPairButtonHiddenFlags(
-            @AppPairButtonHiddenFlags int flag, boolean enable) {
-        if (!FeatureFlags.enableAppPairs()) {
-            return;
-        }
-
-        if (mSaveAppPairButton == null) return;
-        if (enable) {
-            mAppPairButtonHiddenFlags |= flag;
-        } else {
-            mAppPairButtonHiddenFlags &= ~flag;
-        }
-        int desiredVisibility = mAppPairButtonHiddenFlags == 0 ? VISIBLE : GONE;
-        if (mSaveAppPairButton.getVisibility() != desiredVisibility) {
-            mSaveAppPairButton.setVisibility(desiredVisibility);
-            mActionButtons.requestLayout();
-        }
+    private void setActionsAlpha(int index, float value) {
+        Arrays.stream(mMultiValueAlphas).forEach(a -> a.get(index).setValue(value));
     }
 
-    public MultiProperty getContentAlpha() {
-        return mMultiValueAlpha.get(INDEX_CONTENT_ALPHA);
+    public Consumer<Float> getContentAlphaSetter() {
+        return v -> setActionsAlpha(INDEX_CONTENT_ALPHA, v);
     }
 
-    public MultiProperty getVisibilityAlpha() {
-        return mMultiValueAlpha.get(INDEX_VISIBILITY_ALPHA);
+    public Consumer<Float> getVisibilityAlphaSetter() {
+        return v -> setActionsAlpha(INDEX_VISIBILITY_ALPHA, v);
     }
 
-    public MultiProperty getFullscreenAlpha() {
-        return mMultiValueAlpha.get(INDEX_FULLSCREEN_ALPHA);
+    public Consumer<Float> getFullscreenAlphaSetter() {
+        return v -> setActionsAlpha(INDEX_FULLSCREEN_ALPHA, v);
     }
 
-    public MultiProperty getShareTargetAlpha() {
-        return mMultiValueAlpha.get(INDEX_SHARE_TARGET_ALPHA);
+    public Consumer<Float> getShareTargetAlphaSetter() {
+        return v -> setActionsAlpha(INDEX_SHARE_TARGET_ALPHA, v);
     }
 
-    public MultiProperty getIndexScrollAlpha() {
-        return mMultiValueAlpha.get(INDEX_SCROLL_ALPHA);
+    public Consumer<Float> getIndexScrollAlphaSetter() {
+        return v -> setActionsAlpha(INDEX_SCROLL_ALPHA, v);
     }
 
     /**
      * Returns the visibility of the overview actions buttons.
      */
-    public @Visibility int getActionsButtonVisibility() {
-        return mActionButtons.getVisibility();
+    public boolean areActionsButtonsVisible() {
+        return mActionButtons.getVisibility() == View.VISIBLE
+                || mGroupActionButtons.getVisibility() == View.VISIBLE;
     }
 
     /**
@@ -366,10 +347,17 @@
 
     /** Updates vertical margins for different navigation mode or configuration changes. */
     public void updateVerticalMargin(NavigationMode mode) {
+        updateActionBarPosition(mActionButtons);
+        updateActionBarPosition(mGroupActionButtons);
+    }
+
+    /** Positions actions buttons according to device settings and insets. */
+    private void updateActionBarPosition(LinearLayout actionBar) {
         if (mDp == null) {
             return;
         }
-        LayoutParams actionParams = (LayoutParams) mActionButtons.getLayoutParams();
+
+        LayoutParams actionParams = (LayoutParams) actionBar.getLayoutParams();
         actionParams.setMargins(
                 actionParams.leftMargin, mDp.overviewActionsTopMarginPx,
                 actionParams.rightMargin, getBottomMargin());
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 077cd1b..b5ce7f7 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -204,7 +204,7 @@
 import com.android.quickstep.util.TaskVisualsChangeListener;
 import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.util.VibrationConstants;
-import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
+import com.android.quickstep.views.TaskView.TaskContainer;
 import com.android.systemui.plugins.ResourceProvider;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -591,7 +591,7 @@
             if (taskView == null) {
                 return;
             }
-            Task.TaskKey taskKey = taskView.getTask().key;
+            Task.TaskKey taskKey = taskView.getFirstTask().key;
             UI_HELPER_EXECUTOR.execute(new CancellableTask<>(
                     () -> PackageManagerWrapper.getInstance()
                             .getActivityInfo(taskKey.getComponent(), taskKey.userId) == null,
@@ -991,8 +991,7 @@
         if (mHandleTaskStackChanges) {
             TaskView taskView = getTaskViewByTaskId(taskId);
             if (taskView != null) {
-                for (TaskIdAttributeContainer container :
-                        taskView.getTaskIdAttributeContainers()) {
+                for (TaskContainer container : taskView.getTaskContainers()) {
                     if (container == null || taskId != container.getTask().key.id) {
                         continue;
                     }
@@ -1007,11 +1006,12 @@
     public void onTaskIconChanged(String pkg, UserHandle user) {
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView tv = requireTaskViewAt(i);
-            Task task = tv.getTask();
+            Task task = tv.getFirstTask();
             if (task != null && task.key != null && pkg.equals(task.key.getPackageName())
                     && task.key.userId == user.getIdentifier()) {
                 task.icon = null;
-                if (tv.getIconView().getDrawable() != null) {
+                TaskViewIcon firstIconView = tv.getFirstIconView();
+                if (firstIconView != null && firstIconView.getDrawable() != null) {
                     tv.onTaskListVisibilityChanged(true /* visible */);
                 }
             }
@@ -1042,7 +1042,7 @@
                 continue;
             }
             // taskView could be a GroupedTaskView, so select the relevant task by ID
-            TaskIdAttributeContainer taskAttributes = taskView.getTaskAttributesById(id);
+            TaskContainer taskAttributes = taskView.getTaskContainerById(id);
             if (taskAttributes == null) {
                 continue;
             }
@@ -1731,7 +1731,7 @@
 
         int[] currentTaskIds;
         TaskView currentTaskView = getTaskViewAt(mCurrentPage);
-        if (currentTaskView != null && currentTaskView.getTask() != null) {
+        if (currentTaskView != null && currentTaskView.getFirstTask() != null) {
             currentTaskIds = currentTaskView.getTaskIds();
         } else {
             currentTaskIds = new int[0];
@@ -2019,7 +2019,7 @@
         mClearAllButton.setFullscreenProgress(fullscreenProgress);
 
         // Fade out the actions view quickly (0.1 range)
-        mActionsView.getFullscreenAlpha().setValue(
+        mActionsView.getFullscreenAlphaSetter().accept(
                 mapToRange(fullscreenProgress, 0, 0.1f, 1f, 0f, LINEAR));
     }
 
@@ -2270,8 +2270,8 @@
     }
 
     private void animateActionsViewAlpha(float alphaValue, long duration) {
-        mActionsViewAlphaAnimator = ObjectAnimator.ofFloat(
-                mActionsView.getVisibilityAlpha(), MULTI_PROPERTY_VALUE, alphaValue);
+        mActionsViewAlphaAnimator = ObjectAnimator.ofFloat(mActionsView.getVisibilityAlphaSetter(),
+                OverviewActionsView.FLOAT_SETTER, alphaValue);
         mActionsViewAlphaAnimatorFinalValue = alphaValue;
         mActionsViewAlphaAnimator.setDuration(duration);
         // Set autocancel to prevent race-conditiony setting of alpha from other animations
@@ -2290,7 +2290,7 @@
         mClearAllButton.onRecentsViewScroll(scroll, mOverviewGridEnabled);
 
         // Clear all button alpha was set by the previous line.
-        mActionsView.getIndexScrollAlpha().setValue(1 - mClearAllButton.getScrollAlpha());
+        mActionsView.getIndexScrollAlphaSetter().accept(1 - mClearAllButton.getScrollAlpha());
     }
 
     @Override
@@ -2354,8 +2354,8 @@
         // Update the task data for the in/visible children
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView taskView = requireTaskViewAt(i);
-            TaskIdAttributeContainer[] containers = taskView.getTaskIdAttributeContainers();
-            if (containers.length == 0) {
+            List<TaskContainer> containers = taskView.getTaskContainers();
+            if (containers.isEmpty()) {
                 continue;
             }
             int index = indexOfChild(taskView);
@@ -2367,8 +2367,8 @@
             }
             if (visible) {
                 // Default update all non-null tasks, then remove running ones
-                List<Task> tasksToUpdate = Arrays.stream(containers).filter(Objects::nonNull)
-                        .map(TaskIdAttributeContainer::getTask)
+                List<Task> tasksToUpdate = containers.stream()
+                        .map(TaskContainer::getTask)
                         .collect(Collectors.toCollection(ArrayList::new));
                 if (mTmpRunningTasks != null) {
                     for (Task t : mTmpRunningTasks) {
@@ -2393,7 +2393,7 @@
                     mHasVisibleTaskData.put(task.key.id, visible);
                 }
             } else {
-                for (TaskIdAttributeContainer container : containers) {
+                for (TaskContainer container : containers) {
                     if (container == null) {
                         continue;
                     }
@@ -3816,7 +3816,7 @@
 
                 if (success) {
                     if (shouldRemoveTask) {
-                        if (dismissedTaskView.getTask() != null) {
+                        if (dismissedTaskView.getFirstTask() != null) {
                             if (dismissedTaskView.isRunningTask()) {
                                 finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
                                         () -> removeTaskInternal(dismissedTaskViewId));
@@ -4295,7 +4295,7 @@
         int alphaInt = Math.round(alpha * 255);
         mEmptyMessagePaint.setAlpha(alphaInt);
         mEmptyIcon.setAlpha(alphaInt);
-        mActionsView.getContentAlpha().setValue(mContentAlpha);
+        mActionsView.getContentAlphaSetter().accept(mContentAlpha);
 
         if (alpha > 0) {
             setVisibility(VISIBLE);
@@ -4730,7 +4730,7 @@
      */
     public void resetModalVisuals() {
         if (mSelectedTask != null) {
-            mSelectedTask.getThumbnail().getTaskOverlay().resetModalVisuals();
+            mSelectedTask.getFirstThumbnailView().getTaskOverlay().resetModalVisuals();
         }
     }
 
@@ -4749,7 +4749,7 @@
             StatsLogManager.EventEnum splitEvent) {
         mSplitHiddenTaskView = taskView;
         mSplitSelectStateController.setInitialTaskSelect(null /*intent*/,
-                stagePosition, taskView.getItemInfo(), splitEvent, taskView.mTask.key.id);
+                stagePosition, taskView.getItemInfo(), splitEvent, taskView.getFirstTask().key.id);
         mSplitSelectStateController.setAnimateCurrentTaskDismissal(
                 true /*animateCurrentTaskDismissal*/);
         mSplitHiddenTaskViewIndex = indexOfChild(taskView);
@@ -4800,11 +4800,11 @@
             // Animate pair thumbnail into full thumbnail
             boolean primaryTaskSelected = mSplitHiddenTaskView.getTaskIds()[0]
                     == mSplitSelectStateController.getInitialTaskId();
-            TaskIdAttributeContainer taskIdAttributeContainer = mSplitHiddenTaskView
-                    .getTaskIdAttributeContainers()[primaryTaskSelected ? 1 : 0];
-            TaskThumbnailViewDeprecated thumbnail = taskIdAttributeContainer.getThumbnailView();
+            TaskContainer taskContainer = mSplitHiddenTaskView
+                    .getTaskContainers().get(primaryTaskSelected ? 1 : 0);
+            TaskThumbnailViewDeprecated thumbnail = taskContainer.getThumbnailView();
             mSplitSelectStateController.getSplitAnimationController()
-                    .addInitialSplitFromPair(taskIdAttributeContainer, builder,
+                    .addInitialSplitFromPair(taskContainer, builder,
                             mContainer.getDeviceProfile(),
                             mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(),
                             primaryTaskSelected);
@@ -5202,7 +5202,7 @@
         updateGridProperties();
         updateScrollSynchronously();
 
-        int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
+        int targetSysUiFlags = tv.getFirstThumbnailView().getSysUiStatusNavFlags();
         final boolean[] passedOverviewThreshold = new boolean[] {false};
         ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
         progressAnim.addUpdateListener(animator -> {
@@ -5266,7 +5266,7 @@
                 } else {
                     tv.launchTask(this::onTaskLaunchAnimationEnd);
                 }
-                Task task = tv.getTask();
+                Task task = tv.getFirstTask();
                 if (task != null) {
                     mContainer.getStatsLogManager().logger().withItemInfo(tv.getItemInfo())
                             .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
@@ -5899,7 +5899,7 @@
         }
 
         taskView.setShowScreenshot(true);
-        for (TaskIdAttributeContainer container : taskView.getTaskIdAttributeContainers()) {
+        for (TaskContainer container : taskView.getTaskContainers()) {
             if (container == null) {
                 continue;
             }
@@ -6269,7 +6269,7 @@
     /**
      * Moves the provided task into desktop mode, and invoke {@code successCallback} if succeeded.
      */
-    public void moveTaskToDesktop(TaskIdAttributeContainer taskContainer,
+    public void moveTaskToDesktop(TaskContainer taskContainer,
             Runnable successCallback) {
         if (!DesktopModeStatus.canEnterDesktopMode(mContext)) {
             return;
@@ -6278,7 +6278,7 @@
                 () -> moveTaskToDesktopInternal(taskContainer, successCallback)));
     }
 
-    private void moveTaskToDesktopInternal(TaskIdAttributeContainer taskContainer,
+    private void moveTaskToDesktopInternal(TaskContainer taskContainer,
             Runnable successCallback) {
         if (mDesktopRecentsTransitionController == null) {
             return;
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index 443f83c..d0bf2c2 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -56,7 +56,7 @@
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.TaskCornerRadius;
-import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
+import com.android.quickstep.views.TaskView.TaskContainer;
 
 /**
  * Contains options for a recent task when long-pressing its icon.
@@ -76,7 +76,7 @@
     private ValueAnimator mRevealAnimator;
     @Nullable private Runnable mOnClosingStartCallback;
     private TaskView mTaskView;
-    private TaskIdAttributeContainer mTaskContainer;
+    private TaskContainer mTaskContainer;
     private LinearLayout mOptionLayout;
     private float mMenuTranslationYBeforeOpen;
     private float mMenuTranslationXBeforeOpen;
@@ -163,7 +163,10 @@
         }
     }
 
-    public static boolean showForTask(TaskIdAttributeContainer taskContainer,
+    /**
+     * Show a task menu for the given taskContainer.
+     */
+    public static boolean showForTask(TaskContainer taskContainer,
             @Nullable Runnable onClosingStartCallback) {
         RecentsViewContainer container = RecentsViewContainer.containerFromContext(
                 taskContainer.getTaskView().getContext());
@@ -173,11 +176,14 @@
         return taskMenuView.populateAndShowForTask(taskContainer);
     }
 
-    public static boolean showForTask(TaskIdAttributeContainer taskContainer) {
+    /**
+     * Show a task menu for the given taskContainer.
+     */
+    public static boolean showForTask(TaskContainer taskContainer) {
         return showForTask(taskContainer, null);
     }
 
-    private boolean populateAndShowForTask(TaskIdAttributeContainer taskContainer) {
+    private boolean populateAndShowForTask(TaskContainer taskContainer) {
         if (isAttachedToWindow()) {
             return false;
         }
@@ -198,7 +204,7 @@
         return true;
     }
 
-    private void addMenuOptions(TaskIdAttributeContainer taskContainer) {
+    private void addMenuOptions(TaskContainer taskContainer) {
         if (enableOverviewIconMenu()) {
             removeView(mTaskName);
         } else {
@@ -226,7 +232,7 @@
         mOptionLayout.addView(menuOptionView);
     }
 
-    private void orientAroundTaskView(TaskIdAttributeContainer taskContainer) {
+    private void orientAroundTaskView(TaskContainer taskContainer) {
         RecentsView recentsView = mContainer.getOverviewPanel();
         RecentsPagedOrientationHandler orientationHandler =
                 recentsView.getPagedOrientationHandler();
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
index a138db0..7adc32e 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
@@ -37,14 +37,14 @@
 import com.android.launcher3.popup.SystemShortcut
 import com.android.launcher3.util.Themes
 import com.android.quickstep.TaskOverlayFactory
-import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
+import com.android.quickstep.views.TaskView.TaskContainer
 
 class TaskMenuViewWithArrow<T> : ArrowPopup<T> where T : RecentsViewContainer, T : Context {
     companion object {
         const val TAG = "TaskMenuViewWithArrow"
 
         fun <T> showForTask(
-            taskContainer: TaskIdAttributeContainer,
+            taskContainer: TaskContainer,
             alignedOptionIndex: Int = 0
         ): Boolean where T : RecentsViewContainer, T : Context {
             val container: RecentsViewContainer =
@@ -87,7 +87,7 @@
 
     private lateinit var taskView: TaskView
     private lateinit var optionLayout: LinearLayout
-    private lateinit var taskContainer: TaskIdAttributeContainer
+    private lateinit var taskContainer: TaskContainer
 
     private var optionMeasuredHeight = 0
     private val arrowHorizontalPadding: Int
@@ -141,7 +141,7 @@
     }
 
     private fun populateAndShowForTask(
-        taskContainer: TaskIdAttributeContainer,
+        taskContainer: TaskContainer,
         alignedOptionIndex: Int
     ): Boolean {
         if (isAttachedToWindow) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
index 9802beb..21c6ca8 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
@@ -53,6 +53,7 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.SystemUiController.SystemUiControllerFlags;
+import com.android.launcher3.util.ViewPool;
 import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.views.TaskView.FullscreenDrawParams;
@@ -66,7 +67,7 @@
  * @deprecated This class will be replaced by the new [TaskThumbnailView].
  */
 @Deprecated
-public class TaskThumbnailViewDeprecated extends View {
+public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusable {
     private static final MainThreadInitializedObject<FullscreenDrawParams> TEMP_PARAMS =
             new MainThreadInitializedObject<>(FullscreenDrawParams::new);
 
@@ -606,4 +607,9 @@
         }
         return mThumbnailData.isRealSnapshot && !mTask.isLocked;
     }
+
+    @Override
+    public void onRecycle() {
+        // Do nothing
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index f789686..26f313a 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -326,9 +326,6 @@
                 }
             };
 
-    @Nullable
-    protected Task mTask;
-    @Nullable // can be null when enableRefactorTaskThumbnail() == true
     protected TaskThumbnailViewDeprecated mTaskThumbnailViewDeprecated;
     protected TaskThumbnailView mTaskThumbnailView;
     protected TaskViewIcon mIconView;
@@ -371,9 +368,7 @@
     private float mStableAlpha = 1;
 
     private int mTaskViewId = -1;
-    protected int[] mTaskIdContainer = new int[0];
-    protected TaskIdAttributeContainer[] mTaskIdAttributeContainer =
-            new TaskIdAttributeContainer[0];
+    protected List<TaskContainer> mTaskContainers = Collections.emptyList();
 
     private boolean mShowScreenshot;
     private boolean mBorderEnabled;
@@ -501,16 +496,19 @@
     public void 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
-        if (mTask != null) {
+        if (!mTaskContainers.isEmpty()) {
             bindTaskThumbnailView();
         }
     }
 
     /**
-     * Builds proto for logging
+     * Builds proto for logging.
+     *
+     * @deprecated Use {@link #getItemInfo(Task)} instead.
      */
+    @Deprecated
     public WorkspaceItemInfo getItemInfo() {
-        return getItemInfo(mTask);
+        return getItemInfo(getFirstTask());
     }
 
     /**
@@ -687,11 +685,9 @@
      */
     public void bind(Task task, RecentsOrientedState orientedState) {
         cancelPendingLoadTasks();
-        mTask = task;
-        mTaskIdContainer = new int[]{mTask.key.id};
-        mTaskIdAttributeContainer = new TaskIdAttributeContainer[]{
-                new TaskIdAttributeContainer(task, mTaskThumbnailViewDeprecated, mIconView,
-                        STAGE_POSITION_UNDEFINED)};
+        mTaskContainers = Collections.singletonList(
+                new TaskContainer(task, mTaskThumbnailViewDeprecated, mIconView,
+                        STAGE_POSITION_UNDEFINED, mDigitalWellBeingToast));
         if (enableRefactorTaskThumbnail()) {
             bindTaskThumbnailView();
         } else {
@@ -703,17 +699,17 @@
     private void bindTaskThumbnailView() {
         // 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
-        mTaskThumbnailView.getViewModel().bind(new TaskThumbnail(mTask, isRunningTask()));
+        mTaskThumbnailView.getViewModel().bind(new TaskThumbnail(getFirstTask(), isRunningTask()));
     }
 
     /**
      * Sets up an on-click listener and the visibility for show_windows icon on top of the task.
      */
     public void setUpShowAllInstancesListener() {
-        if (mTaskIdAttributeContainer.length == 0) {
+        if (mTaskContainers.isEmpty()) {
             return;
         }
-        String taskPackageName = mTaskIdAttributeContainer[0].mTask.key.getPackageName();
+        String taskPackageName = mTaskContainers.get(0).getTask().key.getPackageName();
 
         // icon of the top/left task
         View showWindowsView = findViewById(R.id.show_windows);
@@ -755,50 +751,62 @@
         filterView.setOnClickListener(callback);
     }
 
-    public TaskIdAttributeContainer[] getTaskIdAttributeContainers() {
-        return mTaskIdAttributeContainer;
+    /**
+     * Returns a list of all TaskContainers in the TaskView.
+     */
+    public List<TaskContainer> getTaskContainers() {
+        return mTaskContainers;
     }
 
+    /**
+     * Returns the first task bound to this TaskView.
+     *
+     * @deprecated Use {@link #mTaskContainers} instead.
+     */
+    @Deprecated
     @Nullable
-    public Task getTask() {
-        return mTask;
+    public Task getFirstTask() {
+        return !mTaskContainers.isEmpty() ? mTaskContainers.get(0).getTask() : null;
     }
 
     /**
      * Check if given {@code taskId} is tracked in this view
      */
     public boolean containsTaskId(int taskId) {
-        return Arrays.stream(mTaskIdContainer).anyMatch(myTaskId -> myTaskId == taskId);
+        return getTaskContainerById(taskId) != null;
     }
 
     /**
      * Returns a copy of integer array containing taskIds of all tasks in the TaskView.
      */
     public int[] getTaskIds() {
-        return Arrays.copyOf(mTaskIdContainer, mTaskIdContainer.length);
+        return mTaskContainers.stream().mapToInt(
+                container -> container.getTask().key.id).toArray();
     }
 
     public boolean containsMultipleTasks() {
-        return mTaskIdContainer.length > 1;
+        return mTaskContainers.size() > 1;
     }
 
     /**
-     * Returns the TaskIdAttributeContainer corresponding to a given taskId, or null if the TaskView
-     * does not contain a Task with that ID.
+     * Returns the TaskContainer corresponding to a given taskId, or null if the TaskView does
+     * not contain a Task with that ID.
      */
     @Nullable
-    public TaskIdAttributeContainer getTaskAttributesById(int taskId) {
-        for (TaskIdAttributeContainer attributes : mTaskIdAttributeContainer) {
-            if (attributes.getTask().key.id == taskId) {
-                return attributes;
-            }
-        }
-
-        return null;
+    public TaskContainer getTaskContainerById(int taskId) {
+        return mTaskContainers.stream().filter(
+                container -> container.getTask().key.id == taskId).findFirst().orElse(null);
     }
 
-    public TaskThumbnailViewDeprecated getThumbnail() {
-        return mTaskThumbnailViewDeprecated;
+    /**
+     * Returns the first thumbnailView of the TaskView.
+     *
+     * @deprecated Use {@link #mTaskContainers} instead.
+     */
+    @Deprecated
+    public TaskThumbnailViewDeprecated getFirstThumbnailView() {
+        return !mTaskContainers.isEmpty() ? mTaskContainers.get(0).getThumbnailView()
+                : mTaskThumbnailViewDeprecated;
     }
 
     void refreshThumbnails(@Nullable HashMap<Integer, ThumbnailData> thumbnailDatas) {
@@ -806,10 +814,10 @@
             // TODO(b/334825222) add thumbnail logic
             return;
         }
-        if (mTask != null && thumbnailDatas != null) {
-            final ThumbnailData thumbnailData = thumbnailDatas.get(mTask.key.id);
+        if (getFirstTask() != null && thumbnailDatas != null) {
+            final ThumbnailData thumbnailData = thumbnailDatas.get(getFirstTask().key.id);
             if (thumbnailData != null) {
-                mTaskThumbnailViewDeprecated.setThumbnail(mTask, thumbnailData);
+                mTaskThumbnailViewDeprecated.setThumbnail(getFirstTask(), thumbnailData);
                 return;
             }
         }
@@ -817,19 +825,27 @@
         mTaskThumbnailViewDeprecated.refresh();
     }
 
-    /** TODO(b/197033698) Remove all usages of above method and migrate to this one */
-    public TaskThumbnailViewDeprecated[] getThumbnails() {
-        return new TaskThumbnailViewDeprecated[]{mTaskThumbnailViewDeprecated};
+    public TaskThumbnailViewDeprecated[] getThumbnailViews() {
+        return mTaskContainers.stream().map(
+                TaskContainer::getThumbnailView).toArray(
+                TaskThumbnailViewDeprecated[]::new);
     }
 
-    public TaskViewIcon getIconView() {
-        return mIconView;
+    /**
+     * Returns the first iconView of the TaskView.
+     *
+     * @deprecated Use {@link #mTaskContainers} instead.
+     */
+    @Deprecated
+    @Nullable
+    public TaskViewIcon getFirstIconView() {
+        return !mTaskContainers.isEmpty() ? mTaskContainers.get(0).getIconView() : null;
     }
 
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         RecentsView recentsView = getRecentsView();
-        if (recentsView == null || getTask() == null) {
+        if (recentsView == null || getFirstTask() == null) {
             return false;
         }
         SplitSelectStateController splitSelectStateController =
@@ -837,7 +853,7 @@
         // Disable taps for split selection animation unless we have multiple tasks
         boolean disableTapsForSplitSelect =
                 splitSelectStateController.isSplitSelectActive()
-                        && splitSelectStateController.getInitialTaskId() == getTask().key.id
+                        && splitSelectStateController.getInitialTaskId() == getFirstTask().key.id
                         && !containsMultipleTasks();
         if (disableTapsForSplitSelect) {
             return false;
@@ -850,19 +866,21 @@
     }
 
     private void onClick(View view) {
-        if (getTask() == null) {
+        if (getFirstTask() == null) {
             Log.d("b/310064698", "onClick - task is null");
             return;
         }
         if (confirmSecondSplitSelectApp()) {
-            Log.d("b/310064698", mTask + " - onClick - split select is active");
+            Log.d("b/310064698",
+                    Arrays.toString(getTaskIds()) + " - onClick - split select is active");
             return;
         }
         RunnableList callbackList = launchTasks();
-        Log.d("b/310064698", mTask + " - onClick - callbackList: " + callbackList);
+        Log.d("b/310064698",
+                Arrays.toString(getTaskIds()) + " - onClick - callbackList: " + callbackList);
         if (callbackList != null) {
-            callbackList.add(() -> Log.d("b/310064698", Arrays.toString(
-                    getTaskIds()) + " - onClick - launchCompleted"));
+            callbackList.add(() -> Log.d("b/310064698",
+                    Arrays.toString(getTaskIds()) + " - onClick - launchCompleted"));
         }
         mContainer.getStatsLogManager().logger().withItemInfo(getItemInfo())
                 .log(LAUNCHER_TASK_LAUNCH_TAP);
@@ -874,10 +892,10 @@
      */
     protected boolean confirmSecondSplitSelectApp() {
         int index = getLastSelectedChildTaskIndex();
-        if (index >= mTaskIdAttributeContainer.length) {
+        if (index >= mTaskContainers.size()) {
             return false;
         }
-        TaskIdAttributeContainer container = mTaskIdAttributeContainer[index];
+        TaskContainer container = mTaskContainers.get(index);
         if (container != null) {
             return getRecentsView().confirmSplitSelect(this, container.getTask(),
                     container.getIconView().getDrawable(), container.getThumbnailView(),
@@ -903,14 +921,15 @@
      */
     @Nullable
     public RunnableList launchTaskAnimated() {
-        if (mTask != null) {
+        if (getFirstTask() != null) {
             TestLogging.recordEvent(
-                    TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask);
+                    TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", Arrays.toString(
+                            getTaskIds()));
             ActivityOptionsWrapper opts = mContainer.getActivityLaunchOptions(this, null);
             opts.options.setLaunchDisplayId(
                     getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId());
             if (ActivityManagerWrapper.getInstance()
-                    .startActivityFromRecents(mTask.key, opts.options)) {
+                    .startActivityFromRecents(getFirstTask().key, opts.options)) {
                 Log.d(TAG, "launchTaskAnimated - startActivityFromRecents: " + Arrays.toString(
                         getTaskIds()));
                 ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED);
@@ -938,7 +957,7 @@
                 return null;
             }
         } else {
-            Log.d(TAG, "launchTaskAnimated - mTask is null" + Arrays.toString(getTaskIds()));
+            Log.d(TAG, "launchTaskAnimated - getTask() is null" + Arrays.toString(getTaskIds()));
             return null;
         }
     }
@@ -954,9 +973,10 @@
      * Starts the task associated with this view without any animation
      */
     public void launchTask(@NonNull Consumer<Boolean> callback, boolean isQuickswitch) {
-        if (mTask != null) {
+        if (getFirstTask() != null) {
             TestLogging.recordEvent(
-                    TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask);
+                    TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", Arrays.toString(
+                            getTaskIds()));
 
             TaskRemovedDuringLaunchListener failureListener = new TaskRemovedDuringLaunchListener(
                     getContext().getApplicationContext());
@@ -964,7 +984,7 @@
                 // We only listen for failures to launch in quickswitch because the during this
                 // gesture launcher is in the background state, vs other launches which are in
                 // the actual overview state
-                failureListener.register(mContainer, mTask.key.id, () -> {
+                failureListener.register(mContainer, getFirstTask().key.id, () -> {
                     notifyTaskLaunchFailed(TAG);
                     RecentsView rv = getRecentsView();
                     if (rv != null) {
@@ -997,7 +1017,7 @@
             if (!enableRefactorTaskThumbnail()) {
                 opts.setDisableStartingWindow(mTaskThumbnailViewDeprecated.shouldShowSplashView());
             }
-            Task.TaskKey key = mTask.key;
+            Task.TaskKey key = getFirstTask().key;
             UI_HELPER_EXECUTOR.execute(() -> {
                 if (!ActivityManagerWrapper.getInstance().startActivityFromRecents(key, opts)) {
                     // If the call to start activity failed, then post the result immediately,
@@ -1013,7 +1033,7 @@
             });
         } else {
             callback.accept(false);
-            Log.d(TAG, "launchTask - mTask is null" + Arrays.toString(getTaskIds()));
+            Log.d(TAG, "launchTask - getTask() is null" + Arrays.toString(getTaskIds()));
         }
     }
 
@@ -1069,7 +1089,8 @@
             anim.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animator) {
-                    if (mTask != null && mTask.key.displayId != getRootViewDisplayId()) {
+                    if (getFirstTask() != null
+                            && getFirstTask().key.displayId != getRootViewDisplayId()) {
                         launchTaskAnimated();
                     }
                     mIsClickableAsLiveTile = true;
@@ -1110,7 +1131,7 @@
      * @param visible If this task view will be visible to the user in overview or hidden
      */
     public void onTaskListVisibilityChanged(boolean visible, @TaskDataChanges int changes) {
-        if (mTask == null) {
+        if (getFirstTask() == null) {
             return;
         }
         cancelPendingLoadTasks();
@@ -1123,15 +1144,16 @@
 
             if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
                 mThumbnailLoadRequest = thumbnailCache.updateThumbnailInBackground(
-                        mTask, thumbnail -> {
+                        getFirstTask(), thumbnail -> {
                             if (!enableRefactorTaskThumbnail()) {
                                 // TODO(b/334825222) add thumbnail state
-                                mTaskThumbnailViewDeprecated.setThumbnail(mTask, thumbnail);
+                                mTaskThumbnailViewDeprecated.setThumbnail(getFirstTask(),
+                                        thumbnail);
                             }
                         });
             }
             if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
-                mIconLoadRequest = iconCache.updateIconInBackground(mTask,
+                mIconLoadRequest = iconCache.updateIconInBackground(getFirstTask(),
                         (task) -> {
                             setIcon(mIconView, task.icon);
                             if (enableOverviewIconMenu()) {
@@ -1151,7 +1173,7 @@
                 }
                 // Reset the task thumbnail reference as well (it will be fetched from the cache or
                 // reloaded next time we need it)
-                mTask.thumbnail = null;
+                getFirstTask().thumbnail = null;
             }
             if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
                 setIcon(mIconView, null);
@@ -1195,9 +1217,8 @@
     }
 
     protected boolean showTaskMenuWithContainer(TaskViewIcon iconView) {
-        Optional<TaskIdAttributeContainer> menuContainer = Arrays.stream(
-                mTaskIdAttributeContainer).filter(
-                        container -> container.getIconView() == iconView).findAny();
+        Optional<TaskContainer> menuContainer = mTaskContainers.stream().filter(
+                container -> container.getIconView() == iconView).findAny();
         if (menuContainer.isEmpty()) {
             return false;
         }
@@ -1270,7 +1291,7 @@
         if (!enableRefactorTaskThumbnail()) {
             mTaskThumbnailViewDeprecated.getTaskOverlay().updateOrientationState(orientationState);
         }
-        mDigitalWellBeingToast.initialize(mTask);
+        mDigitalWellBeingToast.initialize(getFirstTask());
     }
 
     /**
@@ -1281,11 +1302,6 @@
         return deviceProfile.isTablet && !isFocusedTask();
     }
 
-    /** Whether this task view represents the desktop */
-    public boolean isDesktopTask() {
-        return false;
-    }
-
     /**
      * Called to animate a smooth transition when going directly from an app into Overview (and
      * vice versa). Icons fade in, and DWB banners slide in with a "shift up" animation.
@@ -1378,7 +1394,7 @@
         if (enableRefactorTaskThumbnail()) {
             notifyIsRunningTaskUpdated();
         } else {
-            mTaskThumbnailViewDeprecated.setThumbnail(mTask, null);
+            mTaskThumbnailViewDeprecated.setThumbnail(getFirstTask(), null);
         }
         setOverlayEnabled(false);
         onTaskListVisibilityChanged(false);
@@ -1675,7 +1691,7 @@
                         getContext().getText(R.string.accessibility_close)));
 
         final Context context = getContext();
-        for (TaskIdAttributeContainer taskContainer : mTaskIdAttributeContainer) {
+        for (TaskContainer taskContainer : mTaskContainers) {
             for (SystemShortcut s : TraceHelper.allowIpcs(
                     "TV.a11yInfo", () -> getEnabledShortcuts(this, taskContainer))) {
                 info.addAction(s.createAccessibilityAction(context));
@@ -1710,7 +1726,7 @@
             return true;
         }
 
-        for (TaskIdAttributeContainer taskContainer : mTaskIdAttributeContainer) {
+        for (TaskContainer taskContainer : mTaskContainers) {
             for (SystemShortcut s : getEnabledShortcuts(this,
                     taskContainer)) {
                 if (s.hasHandlerForAction(action)) {
@@ -1724,8 +1740,8 @@
     }
 
     @Nullable
-    public RecentsView getRecentsView() {
-        return (RecentsView) getParent();
+    public RecentsView<?, ?> getRecentsView() {
+        return (RecentsView<?, ?>) getParent();
     }
 
     RecentsPagedOrientationHandler getPagedOrientationHandler() {
@@ -1734,8 +1750,9 @@
 
     private void notifyTaskLaunchFailed(String tag) {
         String msg = "Failed to launch task";
-        if (mTask != null) {
-            msg += " (task=" + mTask.key.baseIntent + " userId=" + mTask.key.userId + ")";
+        if (getFirstTask() != null) {
+            msg += " (task=" + getFirstTask().key.baseIntent + " userId="
+                    + getFirstTask().key.userId + ")";
         }
         Log.w(tag, msg);
         Toast.makeText(getContext(), R.string.activity_not_available, LENGTH_SHORT).show();
@@ -1976,7 +1993,10 @@
         }
     }
 
-    public class TaskIdAttributeContainer {
+    /**
+     * Holder for all Task dependent information.
+     */
+    public class TaskContainer {
         private final TaskThumbnailViewDeprecated mThumbnailView;
         private final Task mTask;
         private final TaskViewIcon mIconView;
@@ -1984,15 +2004,18 @@
         private @SplitConfigurationOptions.StagePosition int mStagePosition;
         @IdRes
         private final int mA11yNodeId;
+        private final DigitalWellBeingToast mDigitalWellBeingToast;
 
-        public TaskIdAttributeContainer(Task task, TaskThumbnailViewDeprecated thumbnailView,
-                TaskViewIcon iconView, int stagePosition) {
+        public TaskContainer(Task task, TaskThumbnailViewDeprecated thumbnailView,
+                TaskViewIcon iconView, int stagePosition,
+                DigitalWellBeingToast digitalWellBeingToast) {
             this.mTask = task;
             this.mThumbnailView = thumbnailView;
             this.mIconView = iconView;
             this.mStagePosition = stagePosition;
             this.mA11yNodeId = (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) ?
                     R.id.split_bottomRight_appInfo : R.id.split_topLeft_appInfo;
+            this.mDigitalWellBeingToast = digitalWellBeingToast;
         }
 
         public TaskThumbnailViewDeprecated getThumbnailView() {
@@ -2026,5 +2049,9 @@
         public int getA11yNodeId() {
             return mA11yNodeId;
         }
+
+        public DigitalWellBeingToast getDigitalWellBeingToast() {
+            return mDigitalWellBeingToast;
+        }
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index 7065075..2bcfa3f 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -264,6 +264,120 @@
         assertThat(animatorScheduler.delayedBlock).isNull()
     }
 
+    @Test
+    fun animateToInitialState_inApp() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateToInitialState(bubble, isInApp = true, isExpanding = false)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(barAnimator.isRunning()).isFalse()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(bubbleBarView.alpha).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(handle.translationY).isEqualTo(0)
+        assertThat(handle.alpha).isEqualTo(1)
+
+        verify(bubbleStashController).stashBubbleBarImmediate()
+    }
+
+    @Test
+    fun animateToInitialState_inApp_autoExpanding() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateToInitialState(bubble, isInApp = true, isExpanding = true)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(barAnimator.isRunning()).isFalse()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(bubbleBarView.alpha).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(bubbleBarView.alpha).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        verify(bubbleStashController).showBubbleBarImmediate()
+    }
+
+    @Test
+    fun animateToInitialState_inHome() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(barAnimator.isRunning()).isFalse()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(bubbleBarView.alpha).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(bubbleBarView.alpha).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+        verify(bubbleStashController).showBubbleBarImmediate()
+    }
+
     private fun setUpBubbleBar() {
         bubbleBarView = BubbleBarView(context)
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -322,3 +436,4 @@
 private const val DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS = -20f
 private const val HANDLE_TRANSLATION = -30f
 private const val BAR_TRANSLATION_Y_FOR_TASKBAR = -50f
+private const val BAR_TRANSLATION_Y_FOR_HOTSEAT = -40f
diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
index 0f9d96c..d59aafb 100644
--- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
@@ -16,12 +16,12 @@
 
 package com.android.quickstep
 
-import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
-import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
-import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import android.content.ComponentName
 import android.content.Intent
 import android.platform.test.flag.junit.SetFlagsRule
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.launcher3.AbstractFloatingView
 import com.android.launcher3.AbstractFloatingViewHelper
 import com.android.launcher3.logging.StatsLogManager
@@ -39,12 +39,12 @@
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
-import org.mockito.quality.Strictness
 import org.mockito.kotlin.any
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
 
 /** Test for DesktopSystemShortcut */
 class DesktopSystemShortcutTest {
@@ -64,15 +64,18 @@
     private lateinit var mockitoSession: StaticMockitoSession
 
     @Before
-    fun setUp(){
-        mockitoSession = mockitoSession().strictness(Strictness.LENIENT)
-                .spyStatic(DesktopModeStatus::class.java).startMocking()
+    fun setUp() {
+        mockitoSession =
+            mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .spyStatic(DesktopModeStatus::class.java)
+                .startMocking()
         doReturn(true).`when` { DesktopModeStatus.enforceDeviceRestrictions() }
         doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
     }
 
     @After
-    fun tearDown(){
+    fun tearDown() {
         mockitoSession.finishMocking()
     }
 
@@ -85,11 +88,12 @@
                 isDockable = true
             }
         val taskContainer =
-            taskView.TaskIdAttributeContainer(
+            taskView.TaskContainer(
                 task,
                 null,
                 null,
-                SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
+                SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+                null
             )
 
         val shortcuts = factory.getShortcuts(launcher, taskContainer)
@@ -106,11 +110,12 @@
                 isDockable = true
             }
         val taskContainer =
-            taskView.TaskIdAttributeContainer(
+            taskView.TaskContainer(
                 task,
                 null,
                 null,
-                SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
+                SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+                null
             )
 
         val shortcuts = factory.getShortcuts(launcher, taskContainer)
@@ -128,11 +133,12 @@
                 isDockable = true
             }
         val taskContainer =
-            taskView.TaskIdAttributeContainer(
+            taskView.TaskContainer(
                 task,
                 null,
                 null,
-                SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
+                SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+                null
             )
 
         val shortcuts = factory.getShortcuts(launcher, taskContainer)
@@ -148,11 +154,12 @@
                 isDockable = false
             }
         val taskContainer =
-            taskView.TaskIdAttributeContainer(
+            taskView.TaskContainer(
                 task,
                 null,
                 null,
-                SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
+                SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+                null
             )
 
         val shortcuts = factory.getShortcuts(launcher, taskContainer)
@@ -168,11 +175,12 @@
                 isDockable = true
             }
         val taskContainer =
-            taskView.TaskIdAttributeContainer(
+            taskView.TaskContainer(
                 task,
                 null,
                 null,
-                SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
+                SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+                null
             )
 
         whenever(launcher.getOverviewPanel<LauncherRecentsView>()).thenReturn(recentsView)
diff --git a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
index 4aa7cb0..37ab131 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
@@ -86,8 +86,8 @@
         final TaskView task = getOnceNotNull("No latest task", launcher -> getLatestTask(launcher));
 
         return getFromLauncher(launcher -> {
-            assertTrue("Latest task is not Calculator",
-                    CALCULATOR_PACKAGE.equals(task.getTask().getTopComponent().getPackageName()));
+            assertTrue("Latest task is not Calculator", CALCULATOR_PACKAGE.equals(
+                    task.getFirstTask().getTopComponent().getPackageName()));
             return task.getDigitalWellBeingToast();
         });
     }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
index efe773b..50f74c2 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
@@ -27,6 +27,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 
+import com.android.launcher3.allapps.PrivateProfileManager;
 import com.android.launcher3.tapl.HomeAllApps;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.tapl.PrivateSpaceContainer;
@@ -34,6 +35,7 @@
 import com.android.launcher3.util.rule.ScreenRecordRule;
 
 import org.junit.After;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -164,6 +166,60 @@
         }
     }
 
+    @Test
+    @ScreenRecordRule.ScreenRecord // b/334946529
+    @Ignore("b/339179262")
+    public void testPrivateSpaceLockingBehaviour() throws IOException {
+        // Scroll to the bottom of All Apps
+        executeOnLauncher(launcher -> launcher.getAppsView().resetAndScrollToPrivateSpaceHeader());
+        HomeAllApps homeAllApps = mLauncher.getAllApps();
+
+        // Disable Private Space
+        togglePrivateSpace(PrivateProfileManager.STATE_DISABLED, homeAllApps);
+
+        homeAllApps.freeze();
+        try {
+            // Verify Locked View elements are present.
+            homeAllApps.getPrivateSpaceLockedView();
+        } finally {
+            // UnFreeze
+            homeAllApps.unfreeze();
+        }
+
+        // Enable Private Space
+        togglePrivateSpace(PrivateProfileManager.STATE_ENABLED, homeAllApps);
+
+        homeAllApps.freeze();
+        try {
+            // Verify UnLocked View elements are present.
+            homeAllApps.getPrivateSpaceUnlockedView();
+        } finally {
+            // UnFreeze
+            homeAllApps.unfreeze();
+        }
+    }
+
+    private void togglePrivateSpace(int state, HomeAllApps homeAllApps) {
+        homeAllApps.freeze();
+        try {
+            // Try Toggling Private Space
+            homeAllApps.togglePrivateSpace();
+        }  finally {
+            // UnFreeze
+            homeAllApps.unfreeze();
+        }
+        PrivateProfileManager manager = getFromLauncher(l -> l.getAppsView()
+                .getPrivateProfileManager());
+        waitForLauncherCondition("Private profile toggle to state: " + state + " failed",
+                launcher -> {
+                    manager.reset();
+                    return manager.getCurrentState() == state;
+                },
+                LauncherInstrumentation.WAIT_TIME_MS);
+        // Wait for Launcher UI to be updated with Private Space Items.
+        waitForLauncherUIUpdate();
+    }
+
     private void waitForPrivateSpaceSetup() {
         waitForLauncherCondition("Private Profile not setup",
                 launcher -> launcher.getAppsView().hasPrivateProfile(),
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
index 2a54057..bfd7bdb 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
@@ -130,7 +130,7 @@
                             .tapMenu()
                             .hasMenuItem("Save app pair"));
         } else {
-            overview.getOverviewActions().assertHasAction("Save app pair");
+            overview.getOverviewGroupActions().assertHasAction("Save app pair");
         }
     }
 
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index de98703..f29df61 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -35,7 +35,7 @@
 import com.android.quickstep.views.IconView
 import com.android.quickstep.views.TaskThumbnailViewDeprecated
 import com.android.quickstep.views.TaskView
-import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
+import com.android.quickstep.views.TaskView.TaskContainer
 import com.android.systemui.shared.recents.model.Task
 import org.junit.Assert.assertEquals
 import org.junit.Before
@@ -67,7 +67,7 @@
     private val mockGroupedTaskView: GroupedTaskView = mock()
     private val mockTask: Task = mock()
     private val mockTaskKey: Task.TaskKey = mock()
-    private val mockTaskIdAttributeContainer: TaskIdAttributeContainer = mock()
+    private val mockTaskContainer: TaskContainer = mock()
     // AppPairIcon
     private val mockAppPairIcon: AppPairIcon = mock()
     private val mockContextThemeWrapper: ContextThemeWrapper = mock()
@@ -83,14 +83,15 @@
     private val transitionInfo: TransitionInfo = mock()
     private val transaction: Transaction = mock()
 
-    lateinit var splitAnimationController: SplitAnimationController
+    private lateinit var splitAnimationController: SplitAnimationController
 
     @Before
     fun setup() {
-        whenever(mockTaskView.thumbnail).thenReturn(mockThumbnailView)
+        whenever(mockTaskContainer.thumbnailView).thenReturn(mockThumbnailView)
         whenever(mockThumbnailView.thumbnail).thenReturn(mockBitmap)
-        whenever(mockTaskView.iconView).thenReturn(mockIconView)
+        whenever(mockTaskContainer.iconView).thenReturn(mockIconView)
         whenever(mockIconView.drawable).thenReturn(mockTaskViewDrawable)
+        whenever(mockTaskView.taskContainers).thenReturn(List(1) { mockTaskContainer })
 
         whenever(splitSelectSource.drawable).thenReturn(mockSplitSourceDrawable)
         whenever(splitSelectSource.view).thenReturn(mockSplitSourceView)
@@ -177,14 +178,13 @@
         // Remove icon view from GroupedTaskView
         whenever(mockIconView.drawable).thenReturn(null)
 
-        whenever(mockTaskIdAttributeContainer.task).thenReturn(mockTask)
-        whenever(mockTaskIdAttributeContainer.iconView).thenReturn(mockIconView)
-        whenever(mockTaskIdAttributeContainer.thumbnailView).thenReturn(mockThumbnailView)
+        whenever(mockTaskContainer.task).thenReturn(mockTask)
+        whenever(mockTaskContainer.iconView).thenReturn(mockIconView)
+        whenever(mockTaskContainer.thumbnailView).thenReturn(mockThumbnailView)
         whenever(mockTask.getKey()).thenReturn(mockTaskKey)
         whenever(mockTaskKey.getId()).thenReturn(taskId)
         whenever(mockSplitSelectStateController.initialTaskId).thenReturn(taskId)
-        whenever(mockGroupedTaskView.taskIdAttributeContainers)
-            .thenReturn(Array(1) { mockTaskIdAttributeContainer })
+        whenever(mockGroupedTaskView.taskContainers).thenReturn(List(1) { mockTaskContainer })
         val splitAnimInitProps: SplitAnimationController.Companion.SplitAnimInitProps =
             splitAnimationController.getFirstAnimInitViews(
                 { mockGroupedTaskView },
@@ -277,9 +277,7 @@
         doNothing()
             .whenever(spySplitAnimationController)
             .composeIconSplitLaunchAnimator(any(), any(), any(), any())
-        doReturn(-1)
-                .whenever(spySplitAnimationController)
-                .hasChangesForBothAppPairs(any(), any())
+        doReturn(-1).whenever(spySplitAnimationController).hasChangesForBothAppPairs(any(), any())
 
         spySplitAnimationController.playSplitLaunchAnimation(
             null /* launchingTaskView */,
@@ -305,41 +303,10 @@
         val spySplitAnimationController = spy(splitAnimationController)
         whenever(mockAppPairIcon.context).thenReturn(mockContextThemeWrapper)
         doNothing()
-                .whenever(spySplitAnimationController)
-                .composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), any())
-        doReturn(0)
-                .whenever(spySplitAnimationController)
-                .hasChangesForBothAppPairs(any(), any())
-
-        spySplitAnimationController.playSplitLaunchAnimation(
-                null /* launchingTaskView */,
-                mockAppPairIcon,
-                taskId,
-                taskId2,
-                null /* apps */,
-                null /* wallpapers */,
-                null /* nonApps */,
-                stateManager,
-                depthController,
-                transitionInfo,
-                transaction,
-                {} /* finishCallback */
-        )
-
-        verify(spySplitAnimationController)
-                .composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), eq(0))
-    }
-
-    @Test
-    fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarCMultiWindow() {
-        val spySplitAnimationController = spy(splitAnimationController)
-        whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
-        doNothing()
             .whenever(spySplitAnimationController)
-            .composeScaleUpLaunchAnimation(any(), any(), any(), any())
-        doReturn(-1)
-                .whenever(spySplitAnimationController)
-                .hasChangesForBothAppPairs(any(), any())
+            .composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), any())
+        doReturn(0).whenever(spySplitAnimationController).hasChangesForBothAppPairs(any(), any())
+
         spySplitAnimationController.playSplitLaunchAnimation(
             null /* launchingTaskView */,
             mockAppPairIcon,
@@ -355,8 +322,35 @@
             {} /* finishCallback */
         )
 
-        verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any(),
-                eq(WINDOWING_MODE_MULTI_WINDOW))
+        verify(spySplitAnimationController)
+            .composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), eq(0))
+    }
+
+    @Test
+    fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarCMultiWindow() {
+        val spySplitAnimationController = spy(splitAnimationController)
+        whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
+        doNothing()
+            .whenever(spySplitAnimationController)
+            .composeScaleUpLaunchAnimation(any(), any(), any(), any())
+        doReturn(-1).whenever(spySplitAnimationController).hasChangesForBothAppPairs(any(), any())
+        spySplitAnimationController.playSplitLaunchAnimation(
+            null /* launchingTaskView */,
+            mockAppPairIcon,
+            taskId,
+            taskId2,
+            null /* apps */,
+            null /* wallpapers */,
+            null /* nonApps */,
+            stateManager,
+            depthController,
+            transitionInfo,
+            transaction,
+            {} /* finishCallback */
+        )
+
+        verify(spySplitAnimationController)
+            .composeScaleUpLaunchAnimation(any(), any(), any(), eq(WINDOWING_MODE_MULTI_WINDOW))
     }
 
     @Test
@@ -364,28 +358,26 @@
         val spySplitAnimationController = spy(splitAnimationController)
         whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
         doNothing()
-                .whenever(spySplitAnimationController)
-                .composeScaleUpLaunchAnimation(any(), any(), any(), any())
-        doReturn(0)
-                .whenever(spySplitAnimationController)
-                .hasChangesForBothAppPairs(any(), any())
+            .whenever(spySplitAnimationController)
+            .composeScaleUpLaunchAnimation(any(), any(), any(), any())
+        doReturn(0).whenever(spySplitAnimationController).hasChangesForBothAppPairs(any(), any())
         spySplitAnimationController.playSplitLaunchAnimation(
-                null /* launchingTaskView */,
-                mockAppPairIcon,
-                taskId,
-                taskId2,
-                null /* apps */,
-                null /* wallpapers */,
-                null /* nonApps */,
-                stateManager,
-                depthController,
-                transitionInfo,
-                transaction,
-                {} /* finishCallback */
+            null /* launchingTaskView */,
+            mockAppPairIcon,
+            taskId,
+            taskId2,
+            null /* apps */,
+            null /* wallpapers */,
+            null /* nonApps */,
+            stateManager,
+            depthController,
+            transitionInfo,
+            transaction,
+            {} /* finishCallback */
         )
 
-        verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any(),
-                eq(WINDOWING_MODE_FULLSCREEN))
+        verify(spySplitAnimationController)
+            .composeScaleUpLaunchAnimation(any(), any(), any(), eq(WINDOWING_MODE_FULLSCREEN))
     }
 
     @Test
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 28fb119..8b69318 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -44,7 +44,7 @@
     <string name="add_to_home_screen" msgid="9168649446635919791">"Dodaj na početni ekran"</string>
     <string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"Dodali ste vidžet <xliff:g id="WIDGET_NAME">%1$s</xliff:g> na početni ekran"</string>
     <string name="suggested_widgets_header_title" msgid="1844314680798145222">"Predlozi"</string>
-    <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"Neophodne aplikacije"</string>
+    <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"Osnovno"</string>
     <string name="news_widget_recommendation_category_label" msgid="6756167867113741310">"Novosti i časopisi"</string>
     <string name="social_and_entertainment_widget_recommendation_category_label" msgid="2923840997302308191">"Zona za opuštanje"</string>
     <string name="entertainment_widget_recommendation_category_label" msgid="3973107268630717874">"Zabava"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index b05002d..736eaab 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -44,7 +44,7 @@
     <string name="add_to_home_screen" msgid="9168649446635919791">"Přidat na plochu"</string>
     <string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g> byl přidán na plochu"</string>
     <string name="suggested_widgets_header_title" msgid="1844314680798145222">"Návrhy"</string>
-    <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"Základní"</string>
+    <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"Nejdůležitější aplikace"</string>
     <string name="news_widget_recommendation_category_label" msgid="6756167867113741310">"Zprávy a časopisy"</string>
     <string name="social_and_entertainment_widget_recommendation_category_label" msgid="2923840997302308191">"Vaše klidová zóna"</string>
     <string name="entertainment_widget_recommendation_category_label" msgid="3973107268630717874">"Zábava"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 4aaa44b..623b183 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -48,7 +48,7 @@
     <string name="news_widget_recommendation_category_label" msgid="6756167867113741310">"Noticias y revistas"</string>
     <string name="social_and_entertainment_widget_recommendation_category_label" msgid="2923840997302308191">"Zona de descanso"</string>
     <string name="entertainment_widget_recommendation_category_label" msgid="3973107268630717874">"Entretenimiento"</string>
-    <string name="social_widget_recommendation_category_label" msgid="689147679536384717">"Social"</string>
+    <string name="social_widget_recommendation_category_label" msgid="689147679536384717">"Redes sociales"</string>
     <string name="fitness_widget_recommendation_category_label" msgid="2756483898236585324">"Salud y bienestar"</string>
     <string name="weather_widget_recommendation_category_label" msgid="3059715991930798039">"Clima"</string>
     <string name="others_widget_recommendation_category_label" msgid="5555987036267226245">"Sugerencias para ti"</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index c91e510..4df953a 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -48,7 +48,7 @@
     <string name="news_widget_recommendation_category_label" msgid="6756167867113741310">"Noticias y revistas"</string>
     <string name="social_and_entertainment_widget_recommendation_category_label" msgid="2923840997302308191">"Tu zona de descanso"</string>
     <string name="entertainment_widget_recommendation_category_label" msgid="3973107268630717874">"Entretenimiento"</string>
-    <string name="social_widget_recommendation_category_label" msgid="689147679536384717">"Social"</string>
+    <string name="social_widget_recommendation_category_label" msgid="689147679536384717">"Redes sociales"</string>
     <string name="fitness_widget_recommendation_category_label" msgid="2756483898236585324">"Salud y actividad física"</string>
     <string name="weather_widget_recommendation_category_label" msgid="3059715991930798039">"El tiempo"</string>
     <string name="others_widget_recommendation_category_label" msgid="5555987036267226245">"Sugerencias para ti"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 4f6f121..4c72f00 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -44,7 +44,7 @@
     <string name="add_to_home_screen" msgid="9168649446635919791">"Ajouter à l\'écran d\'accueil"</string>
     <string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g> ajouté à l\'écran d\'accueil"</string>
     <string name="suggested_widgets_header_title" msgid="1844314680798145222">"Suggestions"</string>
-    <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"Les bases"</string>
+    <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"Indispensables"</string>
     <string name="news_widget_recommendation_category_label" msgid="6756167867113741310">"Actualités et magazines"</string>
     <string name="social_and_entertainment_widget_recommendation_category_label" msgid="2923840997302308191">"Votre espace détente"</string>
     <string name="entertainment_widget_recommendation_category_label" msgid="3973107268630717874">"Divertissement"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index b6af6b3..da6a711 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -44,7 +44,7 @@
     <string name="add_to_home_screen" msgid="9168649446635919791">"Dodaj do ekranu głównego"</string>
     <string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"Widżet <xliff:g id="WIDGET_NAME">%1$s</xliff:g> został dodany do ekranu głównego"</string>
     <string name="suggested_widgets_header_title" msgid="1844314680798145222">"Sugestie"</string>
-    <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"Niezbędne"</string>
+    <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"Najbardziej przydatne"</string>
     <string name="news_widget_recommendation_category_label" msgid="6756167867113741310">"Wiadomości i czasopisma"</string>
     <string name="social_and_entertainment_widget_recommendation_category_label" msgid="2923840997302308191">"Strefa relaksu"</string>
     <string name="entertainment_widget_recommendation_category_label" msgid="3973107268630717874">"Rozrywka"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index bb82b67..9eaae37 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -44,7 +44,7 @@
     <string name="add_to_home_screen" msgid="9168649446635919791">"Додај на почетни екран"</string>
     <string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"Додали сте виџет <xliff:g id="WIDGET_NAME">%1$s</xliff:g> на почетни екран"</string>
     <string name="suggested_widgets_header_title" msgid="1844314680798145222">"Предлози"</string>
-    <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"Неопходне апликације"</string>
+    <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"Основно"</string>
     <string name="news_widget_recommendation_category_label" msgid="6756167867113741310">"Новости и часописи"</string>
     <string name="social_and_entertainment_widget_recommendation_category_label" msgid="2923840997302308191">"Зона за опуштање"</string>
     <string name="entertainment_widget_recommendation_category_label" msgid="3973107268630717874">"Забава"</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 2975e97..a18aab6 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -44,7 +44,7 @@
     <string name="add_to_home_screen" msgid="9168649446635919791">"添加到主屏幕"</string>
     <string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"已将“<xliff:g id="WIDGET_NAME">%1$s</xliff:g>”微件添加到主屏幕"</string>
     <string name="suggested_widgets_header_title" msgid="1844314680798145222">"建议"</string>
-    <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"必备"</string>
+    <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"必备之选"</string>
     <string name="news_widget_recommendation_category_label" msgid="6756167867113741310">"新闻与杂志"</string>
     <string name="social_and_entertainment_widget_recommendation_category_label" msgid="2923840997302308191">"您的休闲区"</string>
     <string name="entertainment_widget_recommendation_category_label" msgid="3973107268630717874">"娱乐"</string>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c971223..c3d4273 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -485,7 +485,7 @@
     <!-- Description for Private Space Transition button -->
     <string name="ps_container_transition">Private Space Transitioning</string>
     <!-- Title for Private Space install app icon -->
-    <string name="ps_add_button_label">Install apps</string>
+    <string name="ps_add_button_label">Install</string>
     <!-- Content description for install app icon -->
     <string name="ps_add_button_content_description">Install apps to Private Space</string>
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 009d709..b89d05e 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -96,7 +96,9 @@
 import static com.android.launcher3.popup.SystemShortcut.WIDGETS;
 import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
 import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
+import static com.android.launcher3.testing.shared.TestProtocol.CLOCK_ICON_DRAWABLE_LEAKING;
 import static com.android.launcher3.testing.shared.TestProtocol.LAUNCHER_ACTIVITY_STOPPED_MESSAGE;
+import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.ItemInfoMatcher.forFolderMatch;
 import static com.android.launcher3.util.SettingsCache.TOUCHPAD_NATURAL_SCROLLING;
@@ -421,6 +423,7 @@
     @Override
     @TargetApi(Build.VERSION_CODES.S)
     protected void onCreate(Bundle savedInstanceState) {
+        testLogD(CLOCK_ICON_DRAWABLE_LEAKING, "onCreate: instance=" + this);
         mStartupLatencyLogger = createStartupLatencyLogger(
                 sIsNewProcess
                         ? LockedUserState.get(this).isUserUnlockedAtLauncherStartup()
@@ -1079,6 +1082,7 @@
 
     @Override
     protected void onStart() {
+        testLogD(CLOCK_ICON_DRAWABLE_LEAKING, "onStart: instance=" + this);
         TraceHelper.INSTANCE.beginSection(ON_START_EVT);
         super.onStart();
         if (!mDeferOverlayCallbacks) {
@@ -1092,6 +1096,7 @@
     @Override
     @CallSuper
     protected void onDeferredResumed() {
+        testLogD(CLOCK_ICON_DRAWABLE_LEAKING, "onDeferredResumed: instance=" + this);
         logStopAndResume(true /* isResume */);
 
         // Process any items that were added while Launcher was away.
@@ -1279,6 +1284,7 @@
 
     @Override
     protected void onResume() {
+        testLogD(CLOCK_ICON_DRAWABLE_LEAKING, "onResume: instance=" + this);
         TraceHelper.INSTANCE.beginSection(ON_RESUME_EVT);
         super.onResume();
 
@@ -1294,6 +1300,7 @@
 
     @Override
     protected void onPause() {
+        testLogD(CLOCK_ICON_DRAWABLE_LEAKING, "onPause: instance=" + this);
         // Ensure that items added to Launcher are queued until Launcher returns
         ItemInstallQueue.INSTANCE.get(this).pauseModelPush(FLAG_ACTIVITY_PAUSED);
 
@@ -1776,6 +1783,7 @@
 
     @Override
     public void onDestroy() {
+        testLogD(CLOCK_ICON_DRAWABLE_LEAKING, "onDestroy: instance=" + this);
         super.onDestroy();
         ACTIVITY_TRACKER.onActivityDestroyed(this);
 
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 03de334..869b995 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -206,7 +206,7 @@
     }
 
     private void refreshAndReloadLauncher() {
-        LauncherIcons.clearPool();
+        LauncherIcons.clearPool(mContext);
         mIconCache.updateIconParams(
                 mInvariantDeviceProfile.fillResIconDpi, mInvariantDeviceProfile.iconBitmapSize);
         mModel.forceReload();
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index ae0e80c..53f9cfc 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -20,7 +20,6 @@
 import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
 
-import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PRIVATESPACE;
 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN;
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_ICON;
@@ -28,8 +27,12 @@
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER;
 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE;
@@ -635,8 +638,6 @@
             return;
         }
         ViewGroup settingsAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup);
-        ViewGroup lockButton = mPSHeader.findViewById(R.id.ps_lock_unlock_button);
-        TextView lockText = lockButton.findViewById(R.id.lock_text);
         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.
@@ -646,21 +647,30 @@
                 LayoutTransition.CHANGING,
                 expand ? SETTINGS_AND_LOCK_GROUP_TRANSITION_DELAY : NO_DELAY);
         PropertySetter headerSetter = new AnimatedPropertySetter();
-        ImageButton settingsButton = mPSHeader.findViewById(R.id.ps_settings_button);
-        updateSettingsGearAlpha(settingsButton, expand, headerSetter);
-        updateLockTextAlpha(lockText, expand, headerSetter);
+        headerSetter.add(updateSettingsGearAlpha(expand));
+        headerSetter.add(updateLockTextAlpha(expand));
         AnimatorSet animatorSet = headerSetter.buildAnim();
         animatorSet.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
+                mStatsLogManager.logger().sendToInteractionJankMonitor(
+                        expand
+                                ? 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);
+                mPSHeader.findViewById(R.id.lock_text).setVisibility(expand ? VISIBLE : GONE);
                 setAnimationRunning(true);
             }
         });
         animatorSet.addListener(forEndCallback(() -> {
             setAnimationRunning(false);
             getMainRecyclerView().setChildAttachedConsumer(child -> child.setAlpha(1));
+            mStatsLogManager.logger().sendToInteractionJankMonitor(
+                    expand
+                            ? LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END
+                            : LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END,
+                    mAllApps.getActiveRecyclerView());
             if (!expand) {
                 // Call onAppsUpdated() because it may be canceled when this animation occurs.
                 mAllApps.getPersonalAppList().onAppsUpdated();
@@ -728,19 +738,44 @@
     }
 
     /** Change the settings gear alpha when expanded or collapsed. */
-    private void updateSettingsGearAlpha(ImageButton settingsButton, boolean expand,
-            PropertySetter setter) {
-        float toAlpha = expand ? 1 : 0;
-        setter.setFloat(settingsButton, VIEW_ALPHA, toAlpha, Interpolators.LINEAR)
-                .setDuration(SETTINGS_OPACITY_DURATION).setStartDelay(expand ?
-                        SETTINGS_OPACITY_DELAY : NO_DELAY);
+    private ValueAnimator updateSettingsGearAlpha(boolean expand) {
+        if (mPSHeader == null) {
+            return new ValueAnimator();
+        }
+        float from = expand ? 0 : 1;
+        float to = expand ? 1 : 0;
+        ValueAnimator settingsAlphaAnim = ObjectAnimator.ofFloat(from, to);
+        settingsAlphaAnim.setDuration(SETTINGS_OPACITY_DURATION);
+        settingsAlphaAnim.setStartDelay(expand ? SETTINGS_OPACITY_DELAY : NO_DELAY);
+        settingsAlphaAnim.setInterpolator(Interpolators.LINEAR);
+        settingsAlphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                mPSHeader.findViewById(R.id.ps_settings_button)
+                        .setAlpha((float) valueAnimator.getAnimatedValue());
+            }
+        });
+        return settingsAlphaAnim;
     }
 
-    private void updateLockTextAlpha(TextView textView, boolean expand, PropertySetter setter) {
-        float toAlpha = expand ? 1 : 0;
-        setter.setFloat(textView, VIEW_ALPHA, toAlpha, Interpolators.LINEAR)
-                .setDuration(expand ? TEXT_UNLOCK_OPACITY_DURATION : TEXT_LOCK_OPACITY_DURATION)
-                .setStartDelay(expand ? LOCK_TEXT_OPACITY_DELAY : NO_DELAY);
+    private ValueAnimator updateLockTextAlpha(boolean expand) {
+        if (mPSHeader == null) {
+            return new ValueAnimator();
+        }
+        float from = expand ? 0 : 1;
+        float to = expand ? 1 : 0;
+        ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to);
+        alphaAnim.setDuration(expand ? TEXT_UNLOCK_OPACITY_DURATION : TEXT_LOCK_OPACITY_DURATION);
+        alphaAnim.setStartDelay(expand ? LOCK_TEXT_OPACITY_DELAY : NO_DELAY);
+        alphaAnim.setInterpolator(Interpolators.LINEAR);
+        alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                mPSHeader.findViewById(R.id.lock_text).setAlpha(
+                        (float) valueAnimator.getAnimatedValue());
+            }
+        });
+        return alphaAnim;
     }
 
     void expandPrivateSpace() {
diff --git a/src/com/android/launcher3/allapps/UserProfileManager.java b/src/com/android/launcher3/allapps/UserProfileManager.java
index 6a1f37a..3351ee3 100644
--- a/src/com/android/launcher3/allapps/UserProfileManager.java
+++ b/src/com/android/launcher3/allapps/UserProfileManager.java
@@ -52,11 +52,11 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface UserProfileState { }
 
+    protected final StatsLogManager mStatsLogManager;
     @UserProfileState
     private int mCurrentState;
 
     private final UserManager mUserManager;
-    private final StatsLogManager mStatsLogManager;
     private final UserCache mUserCache;
 
     protected UserProfileManager(UserManager userManager,
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 1f388c2..ae8f1d5 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -76,7 +76,6 @@
 import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.WidgetItem;
@@ -107,7 +106,6 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.ConcurrentLinkedQueue;
 
 /**
  * Utility class for generating the preview of Launcher for a given InvariantDeviceProfile.
@@ -126,44 +124,12 @@
      */
     public static class PreviewContext extends SandboxContext {
 
-        private final InvariantDeviceProfile mIdp;
-        private final ConcurrentLinkedQueue<LauncherIconsForPreview> mIconPool =
-                new ConcurrentLinkedQueue<>();
-
         public PreviewContext(Context base, InvariantDeviceProfile idp) {
             super(base);
-            mIdp = idp;
             putObject(InvariantDeviceProfile.INSTANCE, idp);
             putObject(LauncherAppState.INSTANCE,
                     new LauncherAppState(this, null /* iconCacheFileName */));
         }
-
-        /**
-         * Creates a new LauncherIcons for the preview, skipping the global pool
-         */
-        public LauncherIcons newLauncherIcons(Context context) {
-            LauncherIconsForPreview launcherIconsForPreview = mIconPool.poll();
-            if (launcherIconsForPreview != null) {
-                return launcherIconsForPreview;
-            }
-            return new LauncherIconsForPreview(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize,
-                    -1 /* poolId */);
-        }
-
-        private final class LauncherIconsForPreview extends LauncherIcons {
-
-            private LauncherIconsForPreview(Context context, int fillResIconDpi, int iconBitmapSize,
-                    int poolId) {
-                super(context, fillResIconDpi, iconBitmapSize, poolId);
-            }
-
-            @Override
-            public void recycle() {
-                // Clear any temporary state variables
-                clear();
-                mIconPool.offer(this);
-            }
-        }
     }
 
     private final List<OnDeviceProfileChangeListener> mDpChangeListeners = new ArrayList<>();
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index 7331c6f..e90a1e0 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -25,79 +25,53 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.graphics.IconShape;
-import com.android.launcher3.graphics.LauncherPreviewRenderer;
 import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.UserIconInfo;
 
+import java.util.concurrent.ConcurrentLinkedQueue;
+
 /**
  * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class
  * that are threadsafe.
  */
 public class LauncherIcons extends BaseIconFactory implements AutoCloseable {
 
-    private static final Object sPoolSync = new Object();
-    private static LauncherIcons sPool;
-    private static int sPoolId = 0;
+    private static final MainThreadInitializedObject<Pool> POOL =
+            new MainThreadInitializedObject<>(Pool::new);
 
     /**
      * Return a new Message instance from the global pool. Allows us to
      * avoid allocating new objects in many cases.
      */
     public static LauncherIcons obtain(Context context) {
-        if (context instanceof LauncherPreviewRenderer.PreviewContext) {
-            return ((LauncherPreviewRenderer.PreviewContext) context).newLauncherIcons(context);
-        }
-
-        int poolId;
-        synchronized (sPoolSync) {
-            if (sPool != null) {
-                LauncherIcons m = sPool;
-                sPool = m.next;
-                m.next = null;
-                return m;
-            }
-            poolId = sPoolId;
-        }
-
-        InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
-        return new LauncherIcons(context, idp.fillResIconDpi, idp.iconBitmapSize, poolId);
+        return POOL.get(context).obtain();
     }
 
-    public static void clearPool() {
-        synchronized (sPoolSync) {
-            sPool = null;
-            sPoolId++;
-        }
+    public static void clearPool(Context context) {
+        POOL.get(context).close();
     }
 
-    private final int mPoolId;
-
-    private LauncherIcons next;
+    private final ConcurrentLinkedQueue<LauncherIcons> mPool;
 
     private MonochromeIconFactory mMonochromeIconFactory;
 
-    protected LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize, int poolId) {
+    protected LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize,
+            ConcurrentLinkedQueue<LauncherIcons> pool) {
         super(context, fillResIconDpi, iconBitmapSize,
                 IconShape.INSTANCE.get(context).getShape().enableShapeDetection());
         mMonoIconEnabled = Themes.isThemedIconEnabled(context);
-        mPoolId = poolId;
+        mPool = pool;
     }
 
     /**
      * Recycles a LauncherIcons that may be in-use.
      */
     public void recycle() {
-        synchronized (sPoolSync) {
-            if (sPoolId != mPoolId) {
-                return;
-            }
-            // Clear any temporary state variables
-            clear();
-
-            next = sPool;
-            sPool = this;
-        }
+        clear();
+        mPool.add(this);
     }
 
     @Override
@@ -122,4 +96,33 @@
     public void close() {
         recycle();
     }
+
+    private static class Pool implements SafeCloseable {
+
+        private final Context mContext;
+
+        @NonNull
+        private ConcurrentLinkedQueue<LauncherIcons> mPool = new ConcurrentLinkedQueue<>();
+
+        private Pool(Context context) {
+            mContext = context;
+        }
+
+        public LauncherIcons obtain() {
+            ConcurrentLinkedQueue<LauncherIcons> pool = mPool;
+            LauncherIcons m = pool.poll();
+
+            if (m == null) {
+                InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mContext);
+                return new LauncherIcons(mContext, idp.fillResIconDpi, idp.iconBitmapSize, pool);
+            } else {
+                return m;
+            }
+        }
+
+        @Override
+        public void close() {
+            mPool = new ConcurrentLinkedQueue<>();
+        }
+    }
 }
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 2f3c2b6..25eeacb 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -769,7 +769,19 @@
         LAUNCHER_PRIVATE_SPACE_USER_INSTALLED_APPS_COUNT(1672),
 
         @UiEvent(doc = "Number of preinstalled Private profile apps, shown under separator line")
-        LAUNCHER_PRIVATE_SPACE_PREINSTALLED_APPS_COUNT(1673)
+        LAUNCHER_PRIVATE_SPACE_PREINSTALLED_APPS_COUNT(1673),
+
+        @UiEvent(doc = "Private space lock animation started")
+        LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN(1725),
+
+        @UiEvent(doc = "Private space lock animation finished")
+        LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END(1726),
+
+        @UiEvent(doc = "Private space unlock animation started")
+        LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN(1727),
+
+        @UiEvent(doc = "Private space unlock animation finished")
+        LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END(1728)
 
         // ADD MORE
         ;
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 8892a18..eabacbf 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -166,6 +166,13 @@
         }
     }
 
+    @Override
+    protected float getShiftRange() {
+        // We add the extra height added during predictive back / swipe up to the shift range, so
+        // that the idle interpolator knows to animate the view off fully.
+        return mContent.getHeight() + getBottomOffsetPx();
+    }
+
     /**
      * Click handler for tap to add button.
      */
diff --git a/tests/Android.bp b/tests/Android.bp
index 5ec2263..11177de 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -105,6 +105,7 @@
 android_library {
     name: "Launcher3TestResources",
     resource_dirs: ["res"],
+    asset_dirs: ["assets"],
     // TODO(b/319712088): re-enable use_resource_processor
     use_resource_processor: false,
 }
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 8c47332..74b3ccc 100644
--- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -179,6 +179,8 @@
     public static final String WIDGET_CONFIG_NULL_EXTRA_INTENT = "b/324419890";
     public static final String ACTIVITY_NOT_RESUMED_AFTER_BACK = "b/322823209";
     public static final String OVERVIEW_SELECT_TOOLTIP_MISALIGNED = "b/332485341";
+    public static final String CLOCK_ICON_DRAWABLE_LEAKING = "b/319168409";
+
     public static final String REQUEST_EMULATE_DISPLAY = "emulate-display";
     public static final String REQUEST_STOP_EMULATE_DISPLAY = "stop-emulate-display";
     public static final String REQUEST_IS_EMULATE_DISPLAY_RUNNING = "is-emulate-display-running";
diff --git a/tests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
similarity index 100%
rename from tests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
rename to tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
diff --git a/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
similarity index 100%
rename from tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
rename to tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
diff --git a/tests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
similarity index 97%
rename from tests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
index 13dfd5e..c32461e 100644
--- a/tests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
@@ -22,6 +22,8 @@
 import android.view.View
 import androidx.core.view.get
 import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
 import com.android.launcher3.CellLayout
 import com.android.launcher3.celllayout.board.CellLayoutBoard
 import com.android.launcher3.celllayout.board.IconPoint
@@ -34,6 +36,7 @@
 import org.junit.Assert
 import org.junit.Rule
 import org.junit.Test
+import org.junit.runner.RunWith
 
 private class HotseatReorderTestCase(
     val startBoard: CellLayoutBoard,
@@ -44,6 +47,8 @@
     }
 }
 
+@SmallTest
+@RunWith(AndroidJUnit4::class)
 class HotseatReorderUnitTest {
 
     private val applicationContext: Context =
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
similarity index 100%
rename from tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
rename to tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTestCase.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTestCase.java
similarity index 100%
rename from tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTestCase.java
rename to tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTestCase.java
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderPreviewAnimationTest.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderPreviewAnimationTest.kt
similarity index 95%
rename from tests/src/com/android/launcher3/celllayout/ReorderPreviewAnimationTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderPreviewAnimationTest.kt
index 0bec1b2..a9355ec 100644
--- a/tests/src/com/android/launcher3/celllayout/ReorderPreviewAnimationTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderPreviewAnimationTest.kt
@@ -141,11 +141,14 @@
             ReorderPreviewAnimation.MODE_PREVIEW,
             AnimationValues(dx = 0, dy = 0, scale = 100)
         )
-        testAnimationAtGivenProgress(
-            PREVIEW_DURATION * 99,
-            ReorderPreviewAnimation.MODE_PREVIEW,
-            AnimationValues(dx = 5, dy = -10, scale = 96)
-        )
+        // (b/339313407) Temporarily disable this test as the behavior is
+        // inconsistent between Soong & Gradle builds.
+        //
+        // testAnimationAtGivenProgress(
+        //     PREVIEW_DURATION * 99,
+        //     ReorderPreviewAnimation.MODE_PREVIEW,
+        //     AnimationValues(dx = 5, dy = -10, scale = 96)
+        // )
         testAnimationAtGivenProgress(
             PREVIEW_DURATION * 98,
             ReorderPreviewAnimation.MODE_PREVIEW,
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderTestCase.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderTestCase.java
similarity index 100%
rename from tests/src/com/android/launcher3/celllayout/ReorderTestCase.java
rename to tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderTestCase.java
diff --git a/tests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt
similarity index 100%
rename from tests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt
rename to tests/multivalentTests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt
diff --git a/tests/src/com/android/launcher3/celllayout/testgenerator/DeterministicRandomGenerator.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/testgenerator/DeterministicRandomGenerator.kt
similarity index 100%
rename from tests/src/com/android/launcher3/celllayout/testgenerator/DeterministicRandomGenerator.kt
rename to tests/multivalentTests/src/com/android/launcher3/celllayout/testgenerator/DeterministicRandomGenerator.kt
diff --git a/tests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
similarity index 100%
rename from tests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
rename to tests/multivalentTests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
diff --git a/tests/src/com/android/launcher3/celllayout/testgenerator/RandomMultiBoardGenerator.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/testgenerator/RandomMultiBoardGenerator.kt
similarity index 100%
rename from tests/src/com/android/launcher3/celllayout/testgenerator/RandomMultiBoardGenerator.kt
rename to tests/multivalentTests/src/com/android/launcher3/celllayout/testgenerator/RandomMultiBoardGenerator.kt
diff --git a/tests/src/com/android/launcher3/popup/SystemShortcutTest.java b/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
index e459956..89a4544 100644
--- a/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
+++ b/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
@@ -244,7 +244,6 @@
         SystemShortcut systemShortcut = SystemShortcut.PRIVATE_PROFILE_INSTALL
                 .getShortcut(mTestContext, mAppInfo, mView);
 
-        verify(mPrivateProfileManager, times(2)).getProfileUser();
         assertNull(systemShortcut);
     }
 
@@ -266,7 +265,6 @@
         SystemShortcut systemShortcut = SystemShortcut.PRIVATE_PROFILE_INSTALL
                 .getShortcut(mTestContext, mAppInfo, mView);
 
-        verify(mPrivateProfileManager, times(3)).getProfileUser();
         verify(mPrivateProfileManager).isEnabled();
         assertNotNull(systemShortcut);
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index f3ec852..71cd19d 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -58,6 +58,7 @@
     private static final String FAST_SCROLLER_RES_ID = "fast_scroller";
     private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile(
             "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
+    private static final String UNLOCK_BUTTON_VIEW_RES_ID = "ps_lock_unlock_button";
 
     private final int mHeight;
     private final int mIconHeight;
@@ -433,7 +434,28 @@
     public PrivateSpaceContainer getPrivateSpaceUnlockedView() {
         final UiObject2 allAppsContainer = verifyActiveContainer();
         final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer);
-        return new PrivateSpaceContainer(mLauncher, appListRecycler, this);
+        return new PrivateSpaceContainer(mLauncher, appListRecycler, this, true);
+    }
+
+    /** Returns PrivateSpaceContainer in locked state, if present in view. */
+    @NonNull
+    public PrivateSpaceContainer getPrivateSpaceLockedView() {
+        final UiObject2 allAppsContainer = verifyActiveContainer();
+        final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer);
+        return new PrivateSpaceContainer(mLauncher, appListRecycler, this, false);
+    }
+
+    /**
+     * Toggles Lock/Unlock of Private Space, changing the All Apps Ui.
+     */
+    public void togglePrivateSpace() {
+        final UiObject2 allAppsContainer = verifyActiveContainer();
+        final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer);
+        UiObject2 unLockButtonView = mLauncher.waitForObjectInContainer(appListRecycler,
+                UNLOCK_BUTTON_VIEW_RES_ID);
+        mLauncher.waitForObjectEnabled(unLockButtonView, "Private Space Unlock Button");
+        mLauncher.assertTrue("PS Unlock Button is non-clickable", unLockButtonView.isClickable());
+        unLockButtonView.click();
     }
 
     protected abstract void verifyVisibleContainerOnDismiss();
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 68829e0..2e3944d 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -359,6 +359,21 @@
     }
 
     /**
+     * Gets Overview Actions specific to grouped tasks.
+     *
+     * @return The Overview group actions bar
+     */
+    @NonNull
+    public OverviewActions getOverviewGroupActions() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to get overview group actions")) {
+            verifyActiveContainer();
+            UiObject2 groupActions = mLauncher.waitForOverviewObject("group_action_buttons");
+            return new OverviewActions(groupActions, mLauncher);
+        }
+    }
+
+    /**
      * Returns if clear all button is visible.
      */
     public boolean isClearAllVisible() {
@@ -449,10 +464,18 @@
     private void verifyActionsViewVisibility() {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to assert overview actions view visibility")) {
+            boolean isTablet = mLauncher.isTablet();
+            OverviewTask task = isTablet ? getFocusedTaskForTablet() : getCurrentTask();
+
             if (isActionsViewVisible()) {
-                mLauncher.waitForOverviewObject("action_buttons");
+                if (task.isTaskSplit()) {
+                    mLauncher.waitForOverviewObject("group_action_buttons");
+                } else {
+                    mLauncher.waitForOverviewObject("action_buttons");
+                }
             } else {
                 mLauncher.waitUntilOverviewObjectGone("action_buttons");
+                mLauncher.waitUntilOverviewObjectGone("group_action_buttons");
             }
         }
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index aa8d339..68b0a36 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -108,7 +108,7 @@
 public final class LauncherInstrumentation {
 
     private static final String TAG = "Tapl";
-    private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 15;
+    private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 5;
     private static final int GESTURE_STEP_MS = 16;
 
     static final Pattern EVENT_PILFER_POINTERS = Pattern.compile("pilferPointers");
diff --git a/tests/tapl/com/android/launcher3/tapl/PrivateSpaceContainer.java b/tests/tapl/com/android/launcher3/tapl/PrivateSpaceContainer.java
index 0e65ffb..a2814f0 100644
--- a/tests/tapl/com/android/launcher3/tapl/PrivateSpaceContainer.java
+++ b/tests/tapl/com/android/launcher3/tapl/PrivateSpaceContainer.java
@@ -25,22 +25,30 @@
  */
 public class PrivateSpaceContainer {
     private static final String PS_HEADER_RES_ID = "ps_header_layout";
-    private static final String INSTALL_APP_TITLE = "Install apps";
+    private static final String INSTALL_APP_TITLE = "Install";
     private static final String DIVIDER_RES_ID = "private_space_divider";
 
     private final LauncherInstrumentation mLauncher;
     private final UiObject2 mAppListRecycler;
     private final AllApps mAppList;
+    private final boolean mPrivateSpaceEnabled;
 
     PrivateSpaceContainer(LauncherInstrumentation launcherInstrumentation,
-            UiObject2 appListRecycler, AllApps appList) {
+            UiObject2 appListRecycler, AllApps appList, boolean privateSpaceEnabled) {
         mLauncher = launcherInstrumentation;
         mAppListRecycler = appListRecycler;
         mAppList = appList;
+        mPrivateSpaceEnabled = privateSpaceEnabled;
 
-        verifyHeaderIsPresent();
-        verifyInstallAppButtonIsPresent();
-        verifyDividerIsPresent();
+        if (mPrivateSpaceEnabled) {
+            verifyHeaderIsPresent();
+            verifyInstallAppButtonIsPresent();
+            verifyDividerIsPresent();
+        } else {
+            verifyHeaderIsPresent();
+            verifyInstallAppButtonIsNotPresent();
+            verifyDividerIsNotPresent();
+        }
     }
 
     // Assert PS Header is in view.
@@ -48,7 +56,7 @@
     private void verifyHeaderIsPresent() {
         final UiObject2 psHeader = mLauncher.waitForObjectInContainer(mAppListRecycler,
                 PS_HEADER_RES_ID);
-        new PrivateSpaceHeader(mLauncher, psHeader, true);
+        new PrivateSpaceHeader(mLauncher, psHeader, mPrivateSpaceEnabled);
     }
 
 
@@ -57,11 +65,21 @@
         mAppList.getAppIcon(INSTALL_APP_TITLE);
     }
 
+    // Assert Install App Item is not present in view.
+    private void verifyInstallAppButtonIsNotPresent() {
+        mLauncher.waitUntilLauncherObjectGone(DIVIDER_RES_ID);
+    }
+
     // Assert Sys App Divider is present in view.
     private void verifyDividerIsPresent() {
         mLauncher.waitForObjectInContainer(mAppListRecycler, DIVIDER_RES_ID);
     }
 
+    // Assert Sys App Divider is not present in view.
+    private void verifyDividerIsNotPresent() {
+        mLauncher.waitUntilLauncherObjectGone(DIVIDER_RES_ID);
+    }
+
     /**
      * Verifies that a user installed app is present above the divider.
      */
@@ -73,4 +91,4 @@
         mLauncher.assertTrue("Installed App: " + appName + " is not above the divider",
                 iconCenter.y < dividerCenter.y);
     }
-}
+}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/PrivateSpaceHeader.java b/tests/tapl/com/android/launcher3/tapl/PrivateSpaceHeader.java
index 3a7f038..cc64aae 100644
--- a/tests/tapl/com/android/launcher3/tapl/PrivateSpaceHeader.java
+++ b/tests/tapl/com/android/launcher3/tapl/PrivateSpaceHeader.java
@@ -45,7 +45,7 @@
         if (mPrivateSpaceEnabled) {
             verifyUnlockedState();
         } else {
-            mLauncher.fail("Private Space found in non enabled state");
+            verifyLockedState();
         }
     }
 
@@ -74,4 +74,23 @@
         mLauncher.assertEquals("PS lock text is incorrect", "Lock", lockText.getText());
 
     }
+
+    /** Verify Locked State elements in Private Space Header */
+    private void verifyLockedState() {
+        UiObject2 headerText = mLauncher.waitForObjectInContainer(mPrivateSpaceHeader,
+                PS_HEADER_TEXT_RES_ID);
+        mLauncher.assertEquals("PS Header Text is incorrect ",
+                "Private", headerText.getText());
+
+        UiObject2 unLockButtonView = mLauncher.waitForObjectInContainer(mPrivateSpaceHeader,
+                UNLOCK_BUTTON_VIEW_RES_ID);
+        mLauncher.waitForObjectEnabled(unLockButtonView, "Private Space Unlock Button");
+        mLauncher.assertTrue("PS Unlock Button is non-clickable", unLockButtonView.isClickable());
+
+        mLauncher.waitForObjectInContainer(mPrivateSpaceHeader,
+                LOCK_ICON_RES_ID);
+
+        mLauncher.waitUntilLauncherObjectGone(SETTINGS_BUTTON_RES_ID);
+        mLauncher.waitUntilLauncherObjectGone(LOCK_TEXT_RES_ID);
+    }
 }