Merge "Add additional debug logging for when Launcher cannot find a valid activity to launch an app with." into main
diff --git a/OWNERS b/OWNERS
index 4409b33..31e4610 100644
--- a/OWNERS
+++ b/OWNERS
@@ -10,8 +10,6 @@
 vadimt@google.com
 winsonc@google.com
 jonmiranda@google.com
-alexchau@google.com
-patmanning@google.com
 awickham@google.com
 
 # Launcher workspace eng team
@@ -30,5 +28,15 @@
 peanutbutter@google.com
 jeremysim@google.com
 
+# Overview eng team
+alexchau@google.com
+samcackett@google.com
+silvajordan@google.com
+uwaisashraf@google.com
+
+# Physical Keyboard & Trackpad eng team
+patmanning@google.com
+helencheuk@google.com
+
 per-file FeatureFlags.java, globs = set noparent
 per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com, captaincole@google.com
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index d379132..147cac6 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -16,6 +16,13 @@
 }
 
 flag {
+    name: "enable_twoline_toggle"
+    namespace: "launcher"
+    description: "Enables visibility in home settings to see the toggle to turn on/off two lines in all apps."
+    bug: "316027081"
+}
+
+flag {
     name: "enable_grid_only_overview"
     namespace: "launcher"
     description: "Enable a grid-only overview without a focused task."
@@ -83,6 +90,9 @@
     namespace: "launcher"
     description: "Enables full width two pane widget picker for tablets in landscape and portrait"
     bug: "315055849"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
 }
 
 flag {
@@ -164,6 +174,13 @@
 }
 
 flag {
+  name: "enable_add_app_widget_via_config_activity_v2"
+  namespace: "launcher"
+  description: "When adding app widget through config activity, directly add it to workspace to reduce flicker"
+  bug: "284236964"
+}
+
+flag {
     name: "use_activity_overlay"
     namespace: "launcher"
     description: "Use an activity for home screen overlay"
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 2953c8e..e75812a 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -79,7 +79,7 @@
              android:clearTaskOnLaunch="true"
              android:stateNotNeeded="true"
              android:theme="@style/LauncherTheme"
-             android:screenOrientation="unspecified"
+             android:screenOrientation="behind"
              android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
              android:resizeableActivity="true"
              android:resumeWhilePausing="true"
diff --git a/quickstep/res/drawable/button_taskbar_edu_colored.xml b/quickstep/res/drawable/button_taskbar_edu_colored.xml
index a94a996..104c4b2 100644
--- a/quickstep/res/drawable/button_taskbar_edu_colored.xml
+++ b/quickstep/res/drawable/button_taskbar_edu_colored.xml
@@ -20,7 +20,7 @@
         android:color="?android:attr/colorControlHighlight">
         <item>
             <shape android:shape="rectangle">
-                <corners android:radius="16dp"/>
+                <corners android:radius="28dp"/>
                 <solid android:color="@color/taskbar_edu_button_color"/>
             </shape>
         </item>
diff --git a/quickstep/res/drawable/ic_chevron_down.xml b/quickstep/res/drawable/ic_chevron_down.xml
index 77a8295..f246cbc 100644
--- a/quickstep/res/drawable/ic_chevron_down.xml
+++ b/quickstep/res/drawable/ic_chevron_down.xml
@@ -13,35 +13,22 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:aapt="http://schemas.android.com/aapt"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <target android:name="scaleGroup">
-        <aapt:attr name="android:animation">
-            <objectAnimator
-                android:duration="150"
-                android:propertyName="scaleX"
-                android:valueFrom="1"
-                android:valueTo="-1" />
-        </aapt:attr>
-    </target>
-    <aapt:attr name="android:drawable">
-        <vector
-            android:width="48dp"
-            android:height="48dp"
-            android:autoMirrored="true"
-            android:tint="?androidprv:attr/materialColorOnSurface"
-            android:viewportHeight="48"
-            android:viewportWidth="48">
-            <group
-                android:name="scaleGroup"
-                android:pivotX="24"
-                android:pivotY="24"
-                android:rotation="90">
-                <path
-                    android:fillColor="@android:color/white"
-                    android:pathData="M18.75,36 L16.6,33.85 26.5,23.95 16.6,14.05 18.75,11.9 30.8,23.95Z" />
-            </group>
-        </vector>
-    </aapt:attr>
-</animated-vector>
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:autoMirrored="true"
+    android:tint="?androidprv:attr/materialColorOnSurface"
+    android:viewportHeight="48"
+    android:viewportWidth="48">
+    <group
+        android:name="scaleGroup"
+        android:pivotX="24"
+        android:pivotY="24"
+        android:rotation="90">
+        <path
+            android:fillColor="@android:color/white"
+            android:pathData="M18.75,36 L16.6,33.85 26.5,23.95 16.6,14.05 18.75,11.9 30.8,23.95Z" />
+    </group>
+</vector>
diff --git a/quickstep/res/layout/taskbar_edu_features.xml b/quickstep/res/layout/taskbar_edu_features.xml
index 15de74c..a7bd184 100644
--- a/quickstep/res/layout/taskbar_edu_features.xml
+++ b/quickstep/res/layout/taskbar_edu_features.xml
@@ -122,7 +122,8 @@
         style="@style/TaskbarEdu.Button.Done"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginTop="32dp"
+        android:layout_marginTop="20dp"
+        android:paddingVertical="12dp"
         android:text="@string/taskbar_edu_done"
         android:textColor="?androidprv:attr/textColorOnAccent"
 
diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
index caf8a0b..e1443d0 100644
--- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -31,6 +31,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.Flags;
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.FloatingHeaderRow;
@@ -137,7 +138,9 @@
         int totalHeight = iconHeight + iconPadding + textHeight + mVerticalPadding * 2;
         // Prediction row height will be 4dp bigger than the regular apps in A-Z list when two line
         // is not enabled. Otherwise, the extra height will increase by just the textHeight.
-        int extraHeight = FeatureFlags.enableTwolineAllapps() ? textHeight : mTopRowExtraHeight;
+        int extraHeight = (FeatureFlags.enableTwolineAllapps() && (!Flags.enableTwolineToggle()
+                || (Flags.enableTwolineToggle() && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(
+                        getContext())))) ? textHeight : mTopRowExtraHeight;
         totalHeight += extraHeight;
         return getVisibility() == GONE ? 0 : totalHeight + getPaddingTop() + getPaddingBottom();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 37e5309..50e8d0e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -176,6 +176,7 @@
     /**
      * Should be called from onResume() and onPause(), and animates the Taskbar accordingly.
      */
+    @Override
     public void onLauncherVisibilityChanged(boolean isVisible) {
         onLauncherVisibilityChanged(isVisible, false /* fromInit */);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index b69f657..9006df8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -332,7 +332,7 @@
 
             // Update icon size
             deviceProfile.iconSizePx = deviceProfile.taskbarIconSize;
-            deviceProfile.updateIconSize(1f, getResources());
+            deviceProfile.updateIconSize(1f, this);
         };
         mDeviceProfile = originDeviceProfile.toBuilder(this)
                 .withDimensionsOverride(overrideProvider).build();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index 491938d..f6478df 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -147,6 +147,15 @@
     }
 
     @Override
+    protected boolean isEventWithinSystemGestureRegion(MotionEvent ev) {
+        final float x = ev.getX();
+        final float y = ev.getY();
+
+        return x >= mSystemGestureRegion.left && x < getWidth() - mSystemGestureRegion.right
+                && y >= mSystemGestureRegion.top;
+    }
+
+    @Override
     protected boolean canFindActiveController() {
         // Unlike super class, we want to be able to find controllers when touches occur in the
         // gesture area. For example, this allows Folder to close itself when touching the Taskbar.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
index 916b1e6..144c0c2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
@@ -53,6 +53,7 @@
 
     private boolean mHasSprungOnceThisGesture;
     private @Nullable ValueAnimator mSpringBounce;
+    private boolean mGestureInProgress;
     private boolean mGestureEnded;
     private boolean mAnimationToHomeRunning;
 
@@ -155,7 +156,12 @@
     /**
      * Returns an animation to reset the taskbar translation to {@code 0}.
      */
-    public ObjectAnimator createAnimToResetTranslation(long duration) {
+    public ValueAnimator createAnimToResetTranslation(long duration) {
+        if (mGestureInProgress) {
+            // Return an empty animator as the translation will reset itself after gesture ends.
+            return ValueAnimator.ofFloat(0).setDuration(duration);
+        }
+
         ObjectAnimator animator = mTranslationYForSwipe.animateToValue(0);
         animator.setInterpolator(Interpolators.LINEAR);
         animator.setDuration(duration);
@@ -192,6 +198,7 @@
             mAnimationToHomeRunning = false;
             cancelSpringIfExists();
             reset();
+            mGestureInProgress = true;
         }
         /**
          * Called when there is movement to move the taskbar.
@@ -215,6 +222,7 @@
                 mGestureEnded = true;
                 startSpring();
             }
+            mGestureInProgress = false;
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index bb2ac73..af1bd2a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -260,7 +260,8 @@
                                     taskAttributes.getThumbnailView(),
                                     taskAttributes.getThumbnailView().getThumbnail(),
                                     null /* intent */,
-                                    null /* user */);
+                                    null /* user */,
+                                    info);
                             return;
                         }
                     }
@@ -273,7 +274,8 @@
                             startingView,
                             null /* thumbnail */,
                             intent,
-                            info.user);
+                            info.user,
+                            info);
                 }
         );
     }
@@ -358,4 +360,13 @@
 
     /** Adjusts the hotseat for the bubble bar. */
     public void adjustHotseatForBubbleBar(boolean isBubbleBarVisible) {}
+
+    /**
+     * Adjusts the taskbar based on the visibility of the launcher.
+     * @param isVisible True if launcher is visible, false otherwise.
+     */
+    public void onLauncherVisibilityChanged(boolean isVisible) {
+        mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP, !isVisible);
+        mControllers.taskbarStashController.applyState();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index 9329e16..dbaeafb 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -22,6 +22,7 @@
 import static com.android.app.animation.Interpolators.INSTANT;
 import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
@@ -111,7 +112,7 @@
         boolean exitingOverview = !FeatureFlags.enableSplitContextually() && !toState.overviewUi;
         if (mRecentsView.isSplitSelectionActive() && exitingOverview) {
             setter.add(mRecentsView.getSplitSelectController().getSplitAnimationController()
-                    .createPlaceholderDismissAnim(mLauncher));
+                    .createPlaceholderDismissAnim(mLauncher, LAUNCHER_SPLIT_SELECTION_EXIT_HOME));
             setter.setViewAlpha(
                     mRecentsView.getSplitInstructionsView(),
                     0,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index f3f36c5..722676a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -39,6 +39,8 @@
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
 import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
 import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
 import static com.android.launcher3.popup.QuickstepSystemShortcut.getSplitSelectShortcutByPosition;
 import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
@@ -596,7 +598,7 @@
         list.add(getDragController());
         Consumer<AnimatorSet> splitAnimator = animatorSet ->
                 animatorSet.play(mSplitSelectStateController.getSplitAnimationController()
-                        .createPlaceholderDismissAnim(this));
+                        .createPlaceholderDismissAnim(this, LAUNCHER_SPLIT_SELECTION_EXIT_HOME));
         switch (mode) {
             case NO_BUTTON:
                 list.add(new NoButtonQuickSwitchTouchController(this));
@@ -767,8 +769,10 @@
             // If Launcher pauses before both split apps are selected, exit split screen.
             if (!mSplitSelectStateController.isBothSplitAppsConfirmed() &&
                     !mSplitSelectStateController.isLaunchingFirstAppFullscreen()) {
+                mSplitSelectStateController
+                        .logExitReason(LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
                 mSplitSelectStateController.getSplitAnimationController()
-                        .playPlaceholderDismissAnim(this);
+                        .playPlaceholderDismissAnim(this, LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
             }
         }
     }
@@ -1042,17 +1046,17 @@
     }
 
     @Override
-    protected void handleSplitAnimationGoingToHome() {
-        super.handleSplitAnimationGoingToHome();
+    protected void handleSplitAnimationGoingToHome(StatsLogManager.EventEnum splitDismissReason) {
+        super.handleSplitAnimationGoingToHome(splitDismissReason);
         mSplitSelectStateController.getSplitAnimationController()
-                .playPlaceholderDismissAnim(this);
+                .playPlaceholderDismissAnim(this, splitDismissReason);
     }
 
     @Override
-    public void dismissSplitSelection() {
-        super.dismissSplitSelection();
+    public void dismissSplitSelection(StatsLogManager.LauncherEvent splitDismissEvent) {
+        super.dismissSplitSelection(splitDismissEvent);
         mSplitSelectStateController.getSplitAnimationController()
-                .playPlaceholderDismissAnim(this);
+                .playPlaceholderDismissAnim(this, splitDismissEvent);
     }
 
     public <T extends OverviewActionsView> T getActionsView() {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index 3767cce..577eba6 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -20,6 +20,7 @@
 
 import android.content.Context;
 
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
@@ -27,6 +28,7 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 
 /**
  * Definition for AllApps state
@@ -49,6 +51,24 @@
     }
 
     @Override
+    public void onBackPressed(Launcher launcher) {
+        InteractionJankMonitorWrapper.begin(launcher.getAppsView(),
+                Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK);
+        super.onBackPressed(launcher);
+    }
+
+    @Override
+    protected void onBackPressCompleted(boolean success) {
+        if (success) {
+            // Animation was successful.
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK);
+        } else {
+            // Animation was canceled.
+            InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK);
+        }
+    }
+
+    @Override
     public String getDescription(Launcher launcher) {
         return launcher.getAppsView().getDescription();
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 5db5d17..b5914a1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -186,7 +186,7 @@
         switch (ev.getAction()) {
             case MotionEvent.ACTION_DOWN:
                 InteractionJankMonitorWrapper.begin(
-                        mLauncher.getRootView(), Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS);
+                        mLauncher.getRootView(), Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS, /*tag=*/ "swipe");
                 InteractionJankMonitorWrapper.begin(
                         mLauncher.getRootView(), Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE);
                 break;
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index 4e6b23f..e6febff 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.Flags.enableOverviewIconMenu;
 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.annotation.Nullable;
 import android.app.ActivityManager;
@@ -41,12 +42,12 @@
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.util.CancellableTask;
 import com.android.quickstep.util.TaskKeyLruCache;
 import com.android.quickstep.util.TaskVisualsChangeListener;
 import com.android.systemui.shared.recents.model.Task;
@@ -109,21 +110,17 @@
             callback.accept(task);
             return null;
         }
-        CancellableTask<TaskCacheEntry> request = new CancellableTask<TaskCacheEntry>() {
-            @Override
-            public TaskCacheEntry getResultOnBg() {
-                return getCacheEntry(task);
-            }
-
-            @Override
-            public void handleResult(TaskCacheEntry result) {
-                task.icon = result.icon;
-                task.titleDescription = result.contentDescription;
-                task.title = result.title;
-                callback.accept(task);
-                dispatchIconUpdate(task.key.id);
-            }
-        };
+        CancellableTask<TaskCacheEntry> request = new CancellableTask<>(
+                () -> getCacheEntry(task),
+                MAIN_EXECUTOR,
+                result -> {
+                    task.icon = result.icon;
+                    task.titleDescription = result.contentDescription;
+                    task.title = result.title;
+                    callback.accept(task);
+                    dispatchIconUpdate(task.key.id);
+                }
+        );
         mBgExecutor.execute(request);
         return request;
     }
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 9e21595..b7cbb47 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -16,6 +16,7 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.Flags.enableGridOnlyOverview;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -23,8 +24,8 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.util.CancellableTask;
 import com.android.quickstep.util.TaskKeyByLastActiveTimeCache;
 import com.android.quickstep.util.TaskKeyCache;
 import com.android.quickstep.util.TaskKeyLruCache;
@@ -195,33 +196,29 @@
             return null;
         }
 
-        CancellableTask<ThumbnailData> request = new CancellableTask<>() {
-            @Override
-            public ThumbnailData getResultOnBg() {
-                ThumbnailData thumbnailData = ActivityManagerWrapper.getInstance().getTaskThumbnail(
-                        key.id, lowResolution);
-                if (thumbnailData.thumbnail != null) {
-                    return thumbnailData;
-                }
-                return ActivityManagerWrapper.getInstance().takeTaskThumbnail(key.id);
-            }
-
-            @Override
-            public void handleResult(ThumbnailData result) {
-                // Avoid an async timing issue that a low res entry replaces an existing high res
-                // entry in high res enabled state, so we check before putting it to cache
-                if (enableGridOnlyOverview() && result.reducedResolution
-                        && getHighResLoadingState().isEnabled()) {
-                    ThumbnailData cachedThumbnail = mCache.getAndInvalidateIfModified(key);
-                    if (cachedThumbnail != null && cachedThumbnail.thumbnail != null
-                            && !cachedThumbnail.reducedResolution) {
-                        return;
+        CancellableTask<ThumbnailData> request = new CancellableTask<>(
+                () -> {
+                    ThumbnailData thumbnailData = ActivityManagerWrapper.getInstance()
+                            .getTaskThumbnail(key.id, lowResolution);
+                    return thumbnailData.thumbnail != null ? thumbnailData
+                            : ActivityManagerWrapper.getInstance().takeTaskThumbnail(key.id);
+                },
+                MAIN_EXECUTOR,
+                result -> {
+                    // Avoid an async timing issue that a low res entry replaces an existing high
+                    // res entry in high res enabled state, so we check before putting it to cache
+                    if (enableGridOnlyOverview() && result.reducedResolution
+                            && getHighResLoadingState().isEnabled()) {
+                        ThumbnailData newCachedThumbnail = mCache.getAndInvalidateIfModified(key);
+                        if (newCachedThumbnail != null && newCachedThumbnail.thumbnail != null
+                                && !newCachedThumbnail.reducedResolution) {
+                            return;
+                        }
                     }
+                    mCache.put(key, result);
+                    callback.accept(result);
                 }
-                mCache.put(key, result);
-                callback.accept(result);
-            }
-        };
+        );
         mBgExecutor.execute(request);
         return request;
     }
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index c2cd11c..03e6c99 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -68,6 +68,7 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
 import com.android.quickstep.util.MultiValueUpdateListener;
@@ -207,7 +208,8 @@
         BaseActivity baseActivity = BaseActivity.fromContext(context);
         DeviceProfile dp = baseActivity.getDeviceProfile();
         boolean showAsGrid = dp.isTablet;
-        boolean parallaxCenterAndAdjacentTask = taskIndex != recentsView.getCurrentPage();
+        boolean parallaxCenterAndAdjacentTask =
+                !showAsGrid && taskIndex != recentsView.getCurrentPage();
         int taskRectTranslationPrimary = recentsView.getScrollOffset(taskIndex);
         int taskRectTranslationSecondary = showAsGrid ? (int) v.getGridTranslationY() : 0;
 
@@ -643,6 +645,21 @@
                         recentsView.post(() -> {
                             stateManager.moveToRestState();
                             stateManager.reapplyState();
+
+                            // We may have notified launcher is not visible so that taskbar can
+                            // stash immediately. Now that the animation is over, we can update
+                            // that launcher is still visible.
+                            TaskbarUIController controller = recentsView.getSizeStrategy()
+                                    .getTaskbarController();
+                            if (controller != null) {
+                                boolean launcherVisible = true;
+                                for (RemoteAnimationTarget target : appTargets) {
+                                    launcherVisible &= target.isTranslucent;
+                                }
+                                if (launcherVisible) {
+                                    controller.onLauncherVisibilityChanged(true);
+                                }
+                            }
                         });
                     });
                 }
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index 8f9395f..b846323 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -174,10 +174,10 @@
 
                     @Nullable Task foundTask2 = foundTasks[1];
                     if (foundTask2 != null) {
-                        mSplitSelectStateController.setSecondTask(foundTask2);
+                        mSplitSelectStateController.setSecondTask(foundTask2, app2);
                     } else {
                         mSplitSelectStateController.setSecondTask(
-                                app2.intent, app2.user);
+                                app2.intent, app2.user, app2);
                     }
 
                     mSplitSelectStateController.setLaunchingIconView(appPairIcon);
diff --git a/quickstep/src/com/android/quickstep/util/CancellableTask.java b/quickstep/src/com/android/quickstep/util/CancellableTask.java
deleted file mode 100644
index a6e2e81..0000000
--- a/quickstep/src/com/android/quickstep/util/CancellableTask.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import androidx.annotation.UiThread;
-import androidx.annotation.WorkerThread;
-
-/**
- * Utility class to executore a task on background and post the result on UI thread
- */
-public abstract class CancellableTask<T> implements Runnable {
-
-    private boolean mCancelled = false;
-
-    @Override
-    public final void run() {
-        if (mCancelled) {
-            return;
-        }
-        T result = getResultOnBg();
-        if (mCancelled) {
-            return;
-        }
-        MAIN_EXECUTOR.execute(() -> {
-            if (mCancelled) {
-                return;
-            }
-            handleResult(result);
-        });
-    }
-
-    /**
-     * Called on the worker thread to process the request. The return object is passed to
-     * {@link #handleResult(Object)}
-     */
-    @WorkerThread
-    public abstract T getResultOnBg();
-
-    /**
-     * Called on the UI thread to handle the final result.
-     * @param result
-     */
-    @UiThread
-    public abstract void handleResult(T result);
-
-    /**
-     * Cancels the request. If it is called before {@link #handleResult(Object)}, that method
-     * will not be called
-     */
-    public void cancel() {
-        mCancelled = true;
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 0ee27be..1eb71fc 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -40,22 +40,26 @@
 import androidx.annotation.VisibleForTesting
 import com.android.app.animation.Interpolators
 import com.android.launcher3.DeviceProfile
+import com.android.launcher3.Flags.enableOverviewIconMenu
 import com.android.launcher3.Launcher
 import com.android.launcher3.QuickstepTransitionManager
 import com.android.launcher3.Utilities
 import com.android.launcher3.anim.PendingAnimation
 import com.android.launcher3.apppairs.AppPairIcon
 import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.logging.StatsLogManager.EventEnum
 import com.android.launcher3.statehandlers.DepthController
 import com.android.launcher3.statemanager.StateManager
 import com.android.launcher3.statemanager.StatefulActivity
 import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE
 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource
 import com.android.launcher3.views.BaseDragLayer
 import com.android.quickstep.TaskViewUtils
 import com.android.quickstep.views.FloatingAppPairView
 import com.android.quickstep.views.FloatingTaskView
 import com.android.quickstep.views.GroupedTaskView
+import com.android.quickstep.views.IconAppChipView
 import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.SplitInstructionsView
 import com.android.quickstep.views.TaskThumbnailView
@@ -155,16 +159,36 @@
         val iconView: View = taskIdAttributeContainer.iconView.asView()
         builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLASH_ALPHA, 1f))
         thumbnail.setShowSplashForSplitSelection(true)
+        // With the new `IconAppChipView`, we always want to keep the chip pinned to the
+        // top left of the task / thumbnail.
+        if (enableOverviewIconMenu()) {
+            builder.add(
+                ObjectAnimator.ofFloat(
+                    (iconView as IconAppChipView).splitTranslationX,
+                    MULTI_PROPERTY_VALUE,
+                    0f
+                )
+            )
+            builder.add(
+                ObjectAnimator.ofFloat(
+                    iconView.splitTranslationY,
+                    MULTI_PROPERTY_VALUE,
+                    0f
+                )
+            )
+        }
         if (deviceProfile.isLeftRightSplit) {
             // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0
             val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f
-            val centerIconTranslationX: Float = (taskViewWidth - iconView.width) / 2f
             val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width
             builder.add(ObjectAnimator.ofFloat(thumbnail,
                     TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, centerThumbnailTranslationX))
-            // icons are anchored from Gravity.END, so need to use negative translation
-            builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X,
+            if (!enableOverviewIconMenu()) {
+                // icons are anchored from Gravity.END, so need to use negative translation
+                val centerIconTranslationX: Float = (taskViewWidth - iconView.width) / 2f
+                builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X,
                     -centerIconTranslationX))
+            }
             builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_X, finalScaleX))
 
             // Reset other dimensions
@@ -183,7 +207,6 @@
             // asymmetry causes problems..
 
             // Icon defaults to center | horizontal, we add additional translation for split
-            val centerIconTranslationX = 0f
             var centerThumbnailTranslationY: Float
 
             // TODO(b/271468547), primary thumbnail has layout margin above it, so secondary
@@ -200,9 +223,10 @@
             builder.add(ObjectAnimator.ofFloat(thumbnail,
                     TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, centerThumbnailTranslationY))
 
-            // icons are anchored from Gravity.END, so need to use negative translation
-            builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X,
-                    centerIconTranslationX))
+            if (!enableOverviewIconMenu())  {
+                // icons are anchored from Gravity.END, so need to use negative translation
+                builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, 0f))
+            }
             builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_Y, finalScaleY))
 
             // Reset other dimensions
@@ -213,17 +237,21 @@
     }
 
     /** Does not play any animation if user is not currently in split selection state. */
-    fun playPlaceholderDismissAnim(launcher: StatefulActivity<*>) {
+    fun playPlaceholderDismissAnim(launcher: StatefulActivity<*>, splitDismissEvent: EventEnum) {
         if (!splitSelectStateController.isSplitSelectActive) {
             return
         }
 
-        val anim = createPlaceholderDismissAnim(launcher)
+        val anim = createPlaceholderDismissAnim(launcher, splitDismissEvent)
         anim.start()
     }
 
-    /** Returns [AnimatorSet] which slides initial split placeholder view offscreen. */
-    fun createPlaceholderDismissAnim(launcher: StatefulActivity<*>) : AnimatorSet {
+    /**
+     * Returns [AnimatorSet] which slides initial split placeholder view offscreen and logs an event
+     * for why split is being dismissed
+     */
+    fun createPlaceholderDismissAnim(launcher: StatefulActivity<*>,
+                                     splitDismissEvent: EventEnum) : AnimatorSet {
         val animatorSet = AnimatorSet()
         val recentsView : RecentsView<*, *> = launcher.getOverviewPanel()
         val floatingTask: FloatingTaskView = splitSelectStateController.firstFloatingTaskView
@@ -260,6 +288,7 @@
                         splitSelectStateController.splitInstructionsView)
             }
         })
+        splitSelectStateController.logExitReason(splitDismissEvent)
         return animatorSet
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
index c013483..06edb14 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
@@ -87,6 +87,7 @@
     @StagePosition
     private var initialStagePosition: Int = STAGE_POSITION_UNDEFINED
     private var itemInfo: ItemInfo? = null
+    private var secondItemInfo: ItemInfo? = null
     private var splitEvent: EventEnum? = null
 
     private var initialTaskId: Int = INVALID_TASK_ID
@@ -144,8 +145,9 @@
      * To be called as soon as user selects the second task (even if animations aren't complete)
      * @param taskId The second task that will be launched.
      */
-    fun setSecondTask(taskId: Int) {
+    fun setSecondTask(taskId: Int, itemInfo: ItemInfo) {
         secondTaskId = taskId
+        secondItemInfo = itemInfo
     }
 
     /**
@@ -153,9 +155,10 @@
      * @param intent The second intent that will be launched.
      * @param user The user of that intent.
      */
-    fun setSecondTask(intent: Intent, user: UserHandle) {
+    fun setSecondTask(intent: Intent, user: UserHandle, itemInfo: ItemInfo) {
         secondIntent = intent
         secondUser = user
+        secondItemInfo = itemInfo
     }
 
     /**
@@ -163,9 +166,10 @@
      * Sets [secondUser] from that of the pendingIntent
      * @param pendingIntent The second PendingIntent that will be launched.
      */
-    fun setSecondTask(pendingIntent: PendingIntent) {
+    fun setSecondTask(pendingIntent: PendingIntent, itemInfo: ItemInfo) {
         secondPendingIntent = pendingIntent
         secondUser = pendingIntent.creatorUserHandle
+        secondItemInfo = itemInfo
     }
 
     /**
@@ -173,8 +177,8 @@
      * an extra intent from their RemoteResponse.
      * See [android.widget.RemoteViews.RemoteResponse.getLaunchOptions].first
      */
-    fun setSecondWidget(pendingIntent: PendingIntent, widgetIntent: Intent?) {
-        setSecondTask(pendingIntent)
+    fun setSecondWidget(pendingIntent: PendingIntent, widgetIntent: Intent?, itemInfo: ItemInfo) {
+        setSecondTask(pendingIntent, itemInfo)
         widgetSecondIntent = widgetIntent
     }
 
@@ -407,6 +411,10 @@
         return itemInfo
     }
 
+    fun getSecondItemInfo(): ItemInfo? {
+        return secondItemInfo
+    }
+
     private fun isSecondTaskIntentSet(): Boolean {
         return secondTaskId != INVALID_TASK_ID || secondIntent != null
                 || secondPendingIntent != null
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 3c90e0c..f06418b 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -19,6 +19,10 @@
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTED_SECOND_APP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_COMPLETE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_INITIATED;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
@@ -151,6 +155,11 @@
     private SplitInstructionsView mSplitInstructionsView;
 
     private final List<SplitSelectionListener> mSplitSelectionListeners = new ArrayList<>();
+    /**
+     * Tracks metrics from when first app is selected to split launch or cancellation. This also
+     * gets passed over to shell when attempting to invoke split.
+     */
+    private Pair<InstanceId, com.android.launcher3.logging.InstanceId> mSessionInstanceIds;
 
     private final BackPressHandler mSplitBackHandler = new BackPressHandler() {
         @Override
@@ -162,7 +171,8 @@
         public void onBackInvoked() {
             // When exiting from split selection, leave current context to go to
             // homescreen as well
-            getSplitAnimationController().playPlaceholderDismissAnim(mContext);
+            getSplitAnimationController().playPlaceholderDismissAnim(mContext,
+                    LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
             if (mActivityBackCallback != null) {
                 mActivityBackCallback.run();
             }
@@ -203,6 +213,7 @@
             int alreadyRunningTask) {
         mSplitSelectDataHolder.setInitialTaskSelect(intent, stagePosition, itemInfo, splitEvent,
                 alreadyRunningTask);
+        createAndLogInstanceIdsForSession();
     }
 
     /**
@@ -213,6 +224,7 @@
             @StagePosition int stagePosition, @NonNull ItemInfo itemInfo,
             StatsLogManager.EventEnum splitEvent) {
         mSplitSelectDataHolder.setInitialTaskSelect(info, stagePosition, itemInfo, splitEvent);
+        createAndLogInstanceIdsForSession();
     }
 
     /**
@@ -330,14 +342,12 @@
      */
     public void launchSplitTasks(@PersistentSnapPosition int snapPosition,
             @Nullable Consumer<Boolean> callback) {
-        Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
-                LogUtils.getShellShareableInstanceId();
-        launchTasks(callback, false /* freezeTaskList */, snapPosition, instanceIds.first);
+        launchTasks(callback, false /* freezeTaskList */, snapPosition, mSessionInstanceIds.first);
 
         mStatsLogManager.logger()
-                .withItemInfo(mSplitSelectDataHolder.getItemInfo())
-                .withInstanceId(instanceIds.second)
-                .log(mSplitSelectDataHolder.getSplitEvent());
+                .withItemInfo(mSplitSelectDataHolder.getSecondItemInfo())
+                .withInstanceId(mSessionInstanceIds.second)
+                .log(LAUNCHER_SPLIT_SELECTED_SECOND_APP);
     }
 
     /**
@@ -364,11 +374,26 @@
     }
 
     /**
+     * Use to log an event when user exists split selection when the second app **IS NOT** selected.
+     * This must be called before playing any exit animations since most animations will call
+     * {@link #resetState()} which removes {@link #mSessionInstanceIds}.
+     */
+    public void logExitReason(StatsLogManager.EventEnum splitExitEvent) {
+        StatsLogManager.StatsLogger logger = mStatsLogManager.logger();
+        if (mSessionInstanceIds != null) {
+            logger.withInstanceId(mSessionInstanceIds.second);
+        } else {
+            Log.w(TAG, "Missing session instanceIds");
+        }
+        logger.log(splitExitEvent);
+    }
+
+    /**
      * To be called as soon as user selects the second task (even if animations aren't complete)
      * @param task The second task that will be launched.
      */
-    public void setSecondTask(Task task) {
-        mSplitSelectDataHolder.setSecondTask(task.key.id);
+    public void setSecondTask(Task task, ItemInfo itemInfo) {
+        mSplitSelectDataHolder.setSecondTask(task.key.id, itemInfo);
     }
 
     /**
@@ -376,20 +401,20 @@
      * @param intent The second intent that will be launched.
      * @param user The user of that intent.
      */
-    public void setSecondTask(Intent intent, UserHandle user) {
-        mSplitSelectDataHolder.setSecondTask(intent, user);
+    public void setSecondTask(Intent intent, UserHandle user, ItemInfo itemInfo) {
+        mSplitSelectDataHolder.setSecondTask(intent, user, itemInfo);
     }
 
     /**
      * To be called as soon as user selects the second app (even if animations aren't complete)
      * @param pendingIntent The second PendingIntent that will be launched.
      */
-    public void setSecondTask(PendingIntent pendingIntent) {
-        mSplitSelectDataHolder.setSecondTask(pendingIntent);
+    public void setSecondTask(PendingIntent pendingIntent, ItemInfo itemInfo) {
+        mSplitSelectDataHolder.setSecondTask(pendingIntent, itemInfo);
     }
 
     public void setSecondWidget(PendingIntent pendingIntent, Intent widgetIntent) {
-        mSplitSelectDataHolder.setSecondWidget(pendingIntent, widgetIntent);
+        mSplitSelectDataHolder.setSecondWidget(pendingIntent, widgetIntent, null /*itemInfo*/);
     }
 
     /**
@@ -578,7 +603,7 @@
         final RemoteTransition remoteTransition = new RemoteTransition(animationRunner,
                 ActivityThread.currentActivityThread().getApplicationThread(),
                 "LaunchAppFullscreen");
-        InstanceId instanceId = LogUtils.getShellShareableInstanceId().first;
+        InstanceId instanceId = mSessionInstanceIds.first;
         if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
             switch (launchData.getSplitLaunchType()) {
                 case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId,
@@ -630,6 +655,26 @@
                 ActivityThread.currentActivityThread().getApplicationThread());
     }
 
+    /**
+     * Will initialize {@link #mSessionInstanceIds} if null and log the first split event from
+     * {@link #mSplitSelectDataHolder}
+     */
+    private void createAndLogInstanceIdsForSession() {
+        if (mSessionInstanceIds != null) {
+            Log.w(TAG, "SessionIds should be null");
+        }
+        // Log separately the start of the session and then the first app selected
+        mSessionInstanceIds = LogUtils.getShellShareableInstanceId();
+        mStatsLogManager.logger()
+                .withInstanceId(mSessionInstanceIds.second)
+                .log(LAUNCHER_SPLIT_SELECTION_INITIATED);
+
+        mStatsLogManager.logger()
+                .withItemInfo(mSplitSelectDataHolder.getItemInfo())
+                .withInstanceId(mSessionInstanceIds.second)
+                .log(mSplitSelectDataHolder.getSplitEvent());
+    }
+
     public @StagePosition int getActiveSplitStagePosition() {
         return mSplitSelectDataHolder.getInitialStagePosition();
     }
@@ -804,6 +849,13 @@
         mFirstFloatingTaskView = null;
         mSplitInstructionsView = null;
         mLaunchingFirstAppFullscreen = false;
+
+        if (mSessionInstanceIds != null) {
+            mStatsLogManager.logger()
+                    .withInstanceId(mSessionInstanceIds.second)
+                    .log(LAUNCHER_SPLIT_SELECTION_COMPLETE);
+        }
+        mSessionInstanceIds = null;
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
index 50a5a83..445a540 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
@@ -45,6 +45,7 @@
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.quickstep.views.FloatingTaskView;
@@ -134,7 +135,7 @@
             return false;
         }
 
-        mController.setSecondTask(intent, user);
+        mController.setSecondTask(intent, user, (ItemInfo) tag);
 
         startWorkspaceAnimation(view, null /*bitmap*/, bitmapInfo.newIcon(mLauncher));
         return true;
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index 6b00473..4c7d5c4 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -44,10 +44,10 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.desktop.DesktopRecentsTransitionController;
 import com.android.launcher3.icons.IconProvider;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.RunnableList;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskThumbnailCache;
-import com.android.quickstep.util.CancellableTask;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -63,7 +63,6 @@
 import java.util.List;
 import java.util.function.Consumer;
 
-
 /**
  * TaskView that contains all tasks that are part of the desktop.
  */
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index ad4b292..10ef47c 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -24,6 +24,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
@@ -31,7 +32,6 @@
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskIconCache;
 import com.android.quickstep.TaskThumbnailCache;
-import com.android.quickstep.util.CancellableTask;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.systemui.shared.recents.model.Task;
@@ -45,7 +45,6 @@
 import java.util.HashMap;
 import java.util.function.Consumer;
 
-
 /**
  * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks
  *
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.java b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
index cb3566e..ba42594 100644
--- a/quickstep/src/com/android/quickstep/views/IconAppChipView.java
+++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
@@ -30,7 +30,6 @@
 import android.content.res.Resources;
 import android.graphics.Outline;
 import android.graphics.Rect;
-import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.View;
@@ -98,11 +97,25 @@
     private final MultiPropertyFactory<View> mViewTranslationY;
 
     /**
+     * Gets the view split x-axis translation
+     */
+    public MultiPropertyFactory<View>.MultiProperty getSplitTranslationX() {
+        return mViewTranslationX.get(INDEX_SPLIT_TRANSLATION);
+    }
+
+    /**
      * Sets the view split x-axis translation
      * @param translationX x-axis translation
      */
     public void setSplitTranslationX(float translationX) {
-        mViewTranslationX.get(INDEX_SPLIT_TRANSLATION).setValue(translationX);
+        getSplitTranslationX().setValue(translationX);
+    }
+
+    /**
+     * Gets the view split y-axis translation
+     */
+    public MultiPropertyFactory<View>.MultiProperty getSplitTranslationY() {
+        return mViewTranslationY.get(INDEX_SPLIT_TRANSLATION);
     }
 
     /**
@@ -110,7 +123,7 @@
      * @param translationY y-axis translation
      */
     public void setSplitTranslationY(float translationY) {
-        mViewTranslationY.get(INDEX_SPLIT_TRANSLATION).setValue(translationY);
+        getSplitTranslationY().setValue(translationY);
     }
 
     /**
@@ -121,36 +134,12 @@
     }
 
     /**
-     * Translate the View on the X-axis without overriding the raw translation.
-     * This function is used for the menu split animation. It allows external animations to
-     * translate this view without affecting the value of the original translation. Thus,
-     * it is possible to restore the initial translation value.
-     *
-     * @param translationX Animated translation to be aggregated to the raw translation.
-     */
-    public void setMenuTranslationX(float translationX) {
-        mViewTranslationX.get(INDEX_MENU_TRANSLATION).setValue(translationX);
-    }
-
-    /**
      * Gets the menu y-axis translation for split task
      */
     public MultiPropertyFactory<View>.MultiProperty getMenuTranslationY() {
         return mViewTranslationY.get(INDEX_MENU_TRANSLATION);
     }
 
-    /**
-     * Translate the View on the Y-axis without overriding the raw translation.
-     * This function is used for the menu split animation. It allows external animations to
-     * translate this view without affecting the value of the original translation. Thus,
-     * it is possible to restore the initial translation value.
-     *
-     * @param translationY Animated translation to be aggregated to the raw translation.
-     */
-    public void setMenuTranslationY(float translationY) {
-        mViewTranslationY.get(INDEX_MENU_TRANSLATION).setValue(translationY);
-    }
-
     public IconAppChipView(Context context) {
         this(context, null);
     }
@@ -330,6 +319,7 @@
         LayoutParams iconArrowParams = (LayoutParams) mIconArrowView.getLayoutParams();
         int arrowMarginStart = collapsedBackgroundBounds.right - mArrowMarginEnd - mArrowSize;
         orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart);
+        mIconArrowView.setPivotY(iconArrowParams.height / 2f);
         mIconArrowView.setLayoutParams(iconArrowParams);
 
         // This method is called twice sometimes (like when rotating split tasks). It is called
@@ -378,7 +368,6 @@
         if (isRevealing) {
             boolean isRtl = isLayoutRtl();
             bringToFront();
-            ((AnimatedVectorDrawable) mIconArrowView.getDrawable()).start();
             // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
             Animator expandedTextRevealAnim = ViewAnimationUtils.createCircularReveal(
                     mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
@@ -412,10 +401,10 @@
                             textTranslationXWithRtl),
                     ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 0),
                     ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 1),
-                    ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, arrowTranslationWithRtl));
+                    ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, arrowTranslationWithRtl),
+                    ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, -1));
             mAnimator.setDuration(MENU_BACKGROUND_REVEAL_DURATION);
         } else {
-            ((AnimatedVectorDrawable) mIconArrowView.getDrawable()).reverse();
             // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
             Animator expandedTextClipAnim = ViewAnimationUtils.createCircularReveal(
                     mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
@@ -436,7 +425,8 @@
                     ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X, 0),
                     ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 1),
                     ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 0),
-                    ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0));
+                    ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0),
+                    ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, 1));
             mAnimator.setDuration(MENU_BACKGROUND_HIDE_DURATION);
         }
 
@@ -458,19 +448,6 @@
         return new Rect(0, 0, mExpandedMenuDefaultWidth, mExpandedMenuDefaultHeight);
     }
 
-    protected void reset() {
-        mBackgroundRelativeLtrLocation.set(getCollapsedBackgroundLtrBounds());
-        mIconView.setScaleX(1);
-        mIconView.setScaleY(1);
-        mIconTextCollapsedView.setTranslationX(0);
-        mIconTextExpandedView.setTranslationX(0);
-        mIconTextCollapsedView.setAlpha(1);
-        mIconTextExpandedView.setAlpha(0);
-        mIconArrowView.setTranslationX(0);
-        ((AnimatedVectorDrawable) mIconArrowView.getDrawable()).reset();
-        mAnimator = null;
-    }
-
     private void cancelInProgressAnimations() {
         // We null the `AnimatorSet` because it holds references to the `Animators` which aren't
         // expecting to be mutable and will cause a crash if they are re-used.
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index a27875a..97f3d81 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -25,6 +25,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
 import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
 
 import android.annotation.TargetApi;
@@ -90,7 +91,7 @@
         stateManager.goToState(NORMAL, animated);
         if (FeatureFlags.enableSplitContextually()) {
             mSplitSelectStateController.getSplitAnimationController()
-                    .playPlaceholderDismissAnim(mActivity);
+                    .playPlaceholderDismissAnim(mActivity, LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
         }
         AbstractFloatingView.closeAllOpenViews(mActivity, animated);
     }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 79dfa19..99c42a7 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -89,7 +89,6 @@
 import android.graphics.Bitmap;
 import android.graphics.BlendMode;
 import android.graphics.Canvas;
-import android.graphics.Color;
 import android.graphics.Matrix;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -148,14 +147,15 @@
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.desktop.DesktopRecentsTransitionController;
-import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.touch.OverScroll;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.DynamicResource;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
@@ -442,6 +442,8 @@
 
     private static final float SIGNIFICANT_MOVE_SCREEN_WIDTH_PERCENTAGE = 0.15f;
 
+    private static final float FOREGROUND_SCRIM_TINT = 0.32f;
+
     protected final RecentsOrientedState mOrientationState;
     protected final BaseActivityInterface<STATE_TYPE, ACTIVITY_TYPE> mSizeStrategy;
     @Nullable
@@ -588,8 +590,7 @@
                 return;
             }
             Task.TaskKey taskKey = taskView.getTask().key;
-            UI_HELPER_EXECUTOR.execute(new HandlerRunnable<>(
-                    UI_HELPER_EXECUTOR.getHandler(),
+            UI_HELPER_EXECUTOR.execute(new CancellableTask<>(
                     () -> PackageManagerWrapper.getInstance()
                             .getActivityInfo(taskKey.getComponent(), taskKey.userId) == null,
                     MAIN_EXECUTOR,
@@ -2121,6 +2122,7 @@
 
         // Force TaskView to update size from thumbnail
         updateTaskSize();
+        updatePivots();
     }
 
     /**
@@ -2456,19 +2458,21 @@
         mTaskListChangeId = -1;
         mFocusedTaskViewId = -1;
 
-        if (mRecentsAnimationController != null) {
-            if (mEnableDrawingLiveTile) {
-                // We are still drawing the live tile, finish it now to clean up.
+        Log.d(TAG, "reset - mEnableDrawingLiveTile: " + mEnableDrawingLiveTile
+                + ", mRecentsAnimationController: " + mRecentsAnimationController);
+        if (mEnableDrawingLiveTile) {
+            if (mRecentsAnimationController != null) {
+                // We owns mRecentsAnimationController, finish it now to clean up.
                 finishRecentsAnimation(true /* toRecents */, null);
             } else {
-                mRecentsAnimationController = null;
+                // Only clean up target set if we no longer owns mRecentsAnimationController.
+                runActionOnRemoteHandles(remoteTargetHandle ->
+                        remoteTargetHandle.getTransformParams().setTargetSet(null));
             }
+            setEnableDrawingLiveTile(false);
         }
-        setEnableDrawingLiveTile(false);
-        runActionOnRemoteHandles(remoteTargetHandle -> {
-            remoteTargetHandle.getTransformParams().setTargetSet(null);
-            remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(false);
-        });
+        runActionOnRemoteHandles(remoteTargetHandle ->
+                remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(false));
         if (!FeatureFlags.enableSplitContextually()) {
             resetFromSplitSelectionState();
         }
@@ -2662,6 +2666,7 @@
     public void onPrepareGestureEndAnimation(
             @Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget,
             TaskViewSimulator[] taskViewSimulators) {
+        Log.d(TAG, "onPrepareGestureEndAnimation - endTarget: " + endTarget);
         mCurrentGestureEndTarget = endTarget;
         boolean isOverviewEndTarget = endTarget == GestureState.GestureEndTarget.RECENTS;
         if (isOverviewEndTarget) {
@@ -2717,6 +2722,7 @@
 
         setEnableFreeScroll(true);
         setEnableDrawingLiveTile(mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS);
+        Log.d(TAG, "onGestureAnimationEnd - mEnableDrawingLiveTile: " + mEnableDrawingLiveTile);
         setRunningTaskHidden(false);
         animateUpTaskIconScale();
         animateActionsViewIn();
@@ -4809,7 +4815,8 @@
      *          false otherwise
      */
     public boolean confirmSplitSelect(TaskView containerTaskView, Task task, Drawable drawable,
-            View secondView, @Nullable Bitmap thumbnail, Intent intent, UserHandle user) {
+            View secondView, @Nullable Bitmap thumbnail, Intent intent, UserHandle user,
+            ItemInfo itemInfo) {
         if (canLaunchFullscreenTask()) {
             return false;
         }
@@ -4828,9 +4835,9 @@
                                 + ") is not dockable / does not support splitscreen"));
                 return true;
             }
-            mSplitSelectStateController.setSecondTask(task);
+            mSplitSelectStateController.setSecondTask(task, itemInfo);
         } else {
-            mSplitSelectStateController.setSecondTask(intent, user);
+            mSplitSelectStateController.setSecondTask(intent, user, itemInfo);
         }
 
         RectF secondTaskStartingBounds = new RectF();
@@ -5318,6 +5325,9 @@
     // TODO: To be removed in a follow up CL
     public void setRecentsAnimationTargets(RecentsAnimationController recentsAnimationController,
             RecentsAnimationTargets recentsAnimationTargets) {
+        Log.d(TAG, "setRecentsAnimationTargets "
+                + "- recentsAnimationController: " + recentsAnimationController
+                + ", recentsAnimationTargets: " + recentsAnimationTargets);
         mRecentsAnimationController = recentsAnimationController;
         mSplitSelectStateController.setRecentsAnimationRunning(true);
         if (recentsAnimationTargets == null || recentsAnimationTargets.apps.length == 0) {
@@ -5382,6 +5392,8 @@
 
     public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
             @Nullable Runnable onFinishComplete) {
+        Log.d(TAG, "finishRecentsAnimation - mRecentsAnimationController: "
+                + mRecentsAnimationController);
         // TODO(b/197232424#comment#10) Move this back into onRecentsAnimationComplete(). Maybe?
         cleanupRemoteTargets();
 
@@ -5936,7 +5948,8 @@
             return;
         }
 
-        mTintingAnimator = ObjectAnimator.ofFloat(this, COLOR_TINT, show ? 0.5f : 0f);
+        mTintingAnimator = ObjectAnimator.ofFloat(this, COLOR_TINT,
+                show ? FOREGROUND_SCRIM_TINT : 0f);
         mTintingAnimator.setAutoCancel(true);
         mTintingAnimator.start();
     }
@@ -5979,6 +5992,7 @@
     }
 
     public void cleanupRemoteTargets() {
+        Log.d(TAG, "cleanupRemoteTargets");
         mRemoteTargetHandles = null;
     }
 
@@ -6183,9 +6197,7 @@
 
     /** Get the color used for foreground scrimming the RecentsView for sharing. */
     public static int getForegroundScrimDimColor(Context context) {
-        int baseColor = Themes.getAttrColor(context, R.attr.overviewScrimColor);
-        // The Black blending is temporary until we have the proper color token.
-        return ColorUtils.blendARGB(Color.BLACK, baseColor, 0.25f);
+        return context.getColor(R.color.overview_foreground_scrim_color);
     }
 
     /** Get the RecentsAnimationController */
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index c4b93b7..a11a913 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -16,6 +16,8 @@
 
 package com.android.quickstep.views;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
@@ -39,6 +41,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.quickstep.util.SplitSelectStateController;
 
 /**
  * A rounded rectangular component containing a single TextView.
@@ -130,8 +133,11 @@
     }
 
     private void exitSplitSelection() {
-        ((RecentsView) mLauncher.getOverviewPanel()).getSplitSelectController()
-                .getSplitAnimationController().playPlaceholderDismissAnim(mLauncher);
+        SplitSelectStateController splitSelectController =
+                ((RecentsView) mLauncher.getOverviewPanel()).getSplitSelectController();
+
+        splitSelectController.getSplitAnimationController().playPlaceholderDismissAnim(mLauncher,
+                LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON);
         mLauncher.getStateManager().goToState(LauncherState.NORMAL);
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index 39f1e46..a5e8989 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -113,7 +113,7 @@
 
     @Override
     protected void handleClose(boolean animate) {
-        if (animate) {
+        if (animate || enableOverviewIconMenu()) {
             animateClose();
         } else {
             closeComplete();
@@ -305,6 +305,8 @@
                 : Interpolators.DECELERATE);
 
         if (enableOverviewIconMenu()) {
+            IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView();
+
             float additionalTranslationY = 0;
             if (((RecentsView) mActivity.getOverviewPanel()).isOnGridBottomRow(mTaskView)) {
                 // Animate menu up for enough room to display full menu when task on bottom row.
@@ -320,34 +322,33 @@
                             : mMenuTranslationYBeforeOpen + additionalTranslationY);
             translationYAnim.setInterpolator(EMPHASIZED);
 
-            IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView();
             ObjectAnimator menuTranslationYAnim = ObjectAnimator.ofFloat(
                     iconAppChip.getMenuTranslationY(),
                     MULTI_PROPERTY_VALUE, closing ? 0 : additionalTranslationY);
             menuTranslationYAnim.setInterpolator(EMPHASIZED);
 
-            mOpenCloseAnimator.playTogether(translationYAnim, menuTranslationYAnim);
-        }
-        // Animate menu and icon when split task would display off the side of the screen.
-        if (enableOverviewIconMenu() && mActivity.getDeviceProfile().isLandscape
-                && mTaskContainer.getStagePosition() == STAGE_POSITION_BOTTOM_OR_RIGHT) {
-            float additionalTranslationX = Math.max(
-                    getTranslationX() + getWidth() - (mActivity.getDeviceProfile().widthPx
-                            - getResources().getDimensionPixelSize(
-                            R.dimen.task_menu_edge_padding) * 2), 0);
+            float additionalTranslationX = 0;
+            if (mActivity.getDeviceProfile().isLandscape
+                    && mTaskContainer.getStagePosition() == STAGE_POSITION_BOTTOM_OR_RIGHT) {
+                // Animate menu and icon when split task would display off the side of the screen.
+                additionalTranslationX = Math.max(
+                        getTranslationX() + getWidth() - (mActivity.getDeviceProfile().widthPx
+                                - getResources().getDimensionPixelSize(
+                                R.dimen.task_menu_edge_padding) * 2), 0);
+            }
 
             ObjectAnimator translationXAnim = ObjectAnimator.ofFloat(this, TRANSLATION_X,
                     closing ? mMenuTranslationXBeforeOpen
                             : mMenuTranslationXBeforeOpen - additionalTranslationX);
             translationXAnim.setInterpolator(EMPHASIZED);
 
-            IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView();
             ObjectAnimator menuTranslationXAnim = ObjectAnimator.ofFloat(
                     iconAppChip.getMenuTranslationX(),
                     MULTI_PROPERTY_VALUE, closing ? 0 : -additionalTranslationX);
             menuTranslationXAnim.setInterpolator(EMPHASIZED);
 
-            mOpenCloseAnimator.playTogether(translationXAnim, menuTranslationXAnim);
+            mOpenCloseAnimator.playTogether(translationYAnim, translationXAnim,
+                    menuTranslationXAnim);
         }
 
         mOpenCloseAnimator.playTogether(mRevealAnimator,
@@ -377,21 +378,10 @@
 
     private void closeComplete() {
         mIsOpen = false;
-        resetOverviewIconMenu();
         mActivity.getDragLayer().removeView(this);
         mRevealAnimator = null;
     }
 
-    private void resetOverviewIconMenu() {
-        if (enableOverviewIconMenu()) {
-            IconAppChipView iconAppChipView = (IconAppChipView) mTaskContainer.getIconView();
-            iconAppChipView.reset();
-            iconAppChipView.setMenuTranslationX(0);
-            iconAppChipView.setMenuTranslationY(0);
-            setTranslationY(mMenuTranslationYBeforeOpen);
-        }
-    }
-
     private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
         float radius = TaskCornerRadius.get(mContext);
         Rect fromRect = new Rect(
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 55da160..085c502 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -54,7 +54,6 @@
 import android.graphics.Canvas;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.util.AttributeSet;
@@ -89,6 +88,7 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.ActivityOptionsWrapper;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SplitConfigurationOptions;
@@ -107,7 +107,6 @@
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.BorderAnimator;
-import com.android.quickstep.util.CancellableTask;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.util.TaskCornerRadius;
@@ -135,8 +134,6 @@
     private static final String TAG = TaskView.class.getSimpleName();
     private static final boolean DEBUG = false;
 
-    private static final RectF EMPTY_RECT_F = new RectF();
-
     public static final int FLAG_UPDATE_ICON = 1;
     public static final int FLAG_UPDATE_THUMBNAIL = FLAG_UPDATE_ICON << 1;
     public static final int FLAG_UPDATE_CORNER_RADIUS = FLAG_UPDATE_THUMBNAIL << 1;
@@ -836,7 +833,7 @@
             return getRecentsView().confirmSplitSelect(this, container.getTask(),
                     container.getIconView().getDrawable(), container.getThumbnailView(),
                     container.getThumbnailView().getThumbnail(), /* intent */ null,
-                    /* user */ null);
+                    /* user */ null, container.getItemInfo());
         }
         return false;
     }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
index 7c1b7f3..3f3b9ed 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
@@ -29,7 +29,6 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.util.rule.TestStabilityRule;
 
-import org.junit.Before;
 import org.junit.Test;
 
 /**
@@ -41,12 +40,6 @@
     private static final String CALCULATOR_APP_PACKAGE =
             resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
 
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        initialize(this);
-    }
-
     @PlatinumTest(focusArea = "launcher")
     @Test
     public void testOverviewActionsMenu() {
diff --git a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
index 7e274e8..b31f470 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
@@ -22,7 +22,6 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 
@@ -40,7 +39,6 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        AbstractLauncherUiTest.initialize(this);
         // b/143488140
         mLauncher.goHome();
         // Start an activity where the gestures start.
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
index 36c591e..7c2d41d 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
@@ -23,7 +23,6 @@
 
 import com.android.launcher3.tapl.KeyboardQuickSwitch;
 import com.android.launcher3.taskbar.KeyboardQuickSwitchController;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
 
 import org.junit.Assume;
 import org.junit.Test;
@@ -66,7 +65,6 @@
     public void setUp() throws Exception {
         Assume.assumeTrue(mLauncher.isTablet());
         super.setUp();
-        AbstractLauncherUiTest.initialize(this);
         startAppFast(CALCULATOR_APP_PACKAGE);
         startTestActivity(2);
     }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index cbb4894..ad8535f 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -44,7 +44,6 @@
 import com.android.launcher3.tapl.OverviewActions;
 import com.android.launcher3.tapl.OverviewTask;
 import com.android.launcher3.tapl.SelectModeButtons;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
@@ -70,7 +69,6 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        AbstractLauncherUiTest.initialize(this);
         executeOnLauncher(launcher -> {
             RecentsView recentsView = launcher.getOverviewPanel();
             recentsView.getPagedViewOrientedState().forceAllowRotationForTesting(true);
@@ -305,6 +303,7 @@
     @NavigationModeSwitch
     @PortraitLandscape
     @ScreenRecord // b/313464374
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/325659406
     public void testQuickSwitchFromApp() throws Exception {
         startTestActivity(2);
         startTestActivity(3);
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
index b7546c7..ec8e00d 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
@@ -33,7 +33,6 @@
 import com.android.launcher3.tapl.Overview;
 import com.android.launcher3.tapl.Taskbar;
 import com.android.launcher3.tapl.TaskbarAppIcon;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
@@ -41,7 +40,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -59,7 +57,6 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        AbstractLauncherUiTest.initialize(this);
 
         if (mLauncher.isTablet()) {
             mLauncher.enableBlockTimeout(true);
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
index b0e91e4..aa8c7b5 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
@@ -31,12 +31,10 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.tapl.LauncherInstrumentation.TrackpadGestureType;
 import com.android.launcher3.tapl.Workspace;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 
 import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -47,12 +45,6 @@
     private static final String READ_DEVICE_CONFIG_PERMISSION =
             "android.permission.READ_DEVICE_CONFIG";
 
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        AbstractLauncherUiTest.initialize(this);
-    }
-
     @After
     public void tearDown() {
         mLauncher.setTrackpadGestureType(TrackpadGestureType.NONE);
diff --git a/quickstep/tests/src/com/android/quickstep/TaplViewInflationDuringSwipeUp.java b/quickstep/tests/src/com/android/quickstep/TaplViewInflationDuringSwipeUp.java
index f76e17a..6093816 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplViewInflationDuringSwipeUp.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplViewInflationDuringSwipeUp.java
@@ -56,7 +56,6 @@
 import com.android.launcher3.testcomponent.ListViewService;
 import com.android.launcher3.testcomponent.ListViewService.SimpleViewsFactory;
 import com.android.launcher3.testcomponent.TestCommandReceiver;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
@@ -93,8 +92,6 @@
 
     @Before
     public void setUp() throws Exception {
-        super.setUp();
-
         // Workaround for b/142351228, when there are no activities, the system may not destroy the
         // activity correctly for activities under instrumentation, which can leave two concurrent
         // activities, which changes the order in which the activities are cleaned up (overlapping
@@ -102,7 +99,7 @@
         // is started only after starting another app.
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
 
-        AbstractLauncherUiTest.initialize(this);
+        super.setUp();
 
         mModel = LauncherAppState.getInstance(mTargetContext).getModel();
         Executors.MODEL_EXECUTOR.submit(mModel.getModelDbController()::createEmptyDB).get();
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt
index c6c5be4..b4f1692 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt
@@ -60,6 +60,7 @@
     private val sampleShortcut = Intent()
     private val sampleShortcut2 = Intent()
     private val sampleItemInfo = ItemInfo()
+    private val sampleItemInfo2 = ItemInfo()
     private val samplePackage =
         AbstractLauncherUiTest.resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)
 
@@ -133,7 +134,7 @@
             null,
             INVALID_TASK_ID
         )
-        splitSelectDataHolder.setSecondTask(sampleTaskId)
+        splitSelectDataHolder.setSecondTask(sampleTaskId, sampleItemInfo2)
         assertTrue(splitSelectDataHolder.isBothSplitAppsConfirmed())
     }
 
@@ -145,7 +146,7 @@
             null,
             null
         )
-        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser)
+        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser, sampleItemInfo2)
         assertTrue(splitSelectDataHolder.isBothSplitAppsConfirmed())
     }
 
@@ -158,7 +159,7 @@
             null,
             INVALID_TASK_ID
         )
-        splitSelectDataHolder.setSecondTask(sampleShortcut, sampleUser)
+        splitSelectDataHolder.setSecondTask(sampleShortcut, sampleUser, sampleItemInfo2)
         assertTrue(splitSelectDataHolder.isBothSplitAppsConfirmed())
     }
 
@@ -170,7 +171,7 @@
             sampleItemInfo,
             null
         )
-        splitSelectDataHolder.setSecondTask(sampleTaskId2)
+        splitSelectDataHolder.setSecondTask(sampleTaskId2, sampleItemInfo2)
         val launchData = splitSelectDataHolder.getSplitLaunchData()
 
         assertEquals(launchData.splitLaunchType, SPLIT_TASK_TASK)
@@ -194,7 +195,7 @@
             sampleItemInfo,
             null
         )
-        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser)
+        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser, sampleItemInfo2)
         val launchData = splitSelectDataHolder.getSplitLaunchData()
 
         assertEquals(launchData.splitLaunchType, SPLIT_TASK_PENDINGINTENT)
@@ -218,7 +219,7 @@
             sampleItemInfo,
             null
         )
-        splitSelectDataHolder.setSecondTask(sampleShortcut, sampleUser)
+        splitSelectDataHolder.setSecondTask(sampleShortcut, sampleUser, sampleItemInfo2)
         val launchData = splitSelectDataHolder.getSplitLaunchData()
 
         assertEquals(launchData.splitLaunchType, SPLIT_TASK_SHORTCUT)
@@ -243,7 +244,7 @@
             null,
             INVALID_TASK_ID
         )
-        splitSelectDataHolder.setSecondTask(sampleTaskId)
+        splitSelectDataHolder.setSecondTask(sampleTaskId, sampleItemInfo2)
         val launchData = splitSelectDataHolder.getSplitLaunchData()
 
         assertEquals(launchData.splitLaunchType, SPLIT_PENDINGINTENT_TASK)
@@ -268,7 +269,7 @@
             null,
             INVALID_TASK_ID
         )
-        splitSelectDataHolder.setSecondTask(sampleTaskId)
+        splitSelectDataHolder.setSecondTask(sampleTaskId, sampleItemInfo2)
         val launchData = splitSelectDataHolder.getSplitLaunchData()
 
         assertEquals(launchData.splitLaunchType, SPLIT_SHORTCUT_TASK)
@@ -293,7 +294,7 @@
             null,
             INVALID_TASK_ID
         )
-        splitSelectDataHolder.setSecondTask(sampleIntent2, sampleUser)
+        splitSelectDataHolder.setSecondTask(sampleIntent2, sampleUser, sampleItemInfo2)
         val launchData = splitSelectDataHolder.getSplitLaunchData()
 
         assertEquals(launchData.splitLaunchType, SPLIT_PENDINGINTENT_PENDINGINTENT)
@@ -388,7 +389,7 @@
             null,
             null
         )
-        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser)
+        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser, sampleItemInfo2)
         splitSelectDataHolder.resetState()
         assertFalse(splitSelectDataHolder.isSplitSelectActive())
     }
@@ -402,7 +403,7 @@
                 null,
                 INVALID_TASK_ID
         )
-        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser)
+        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser, sampleItemInfo2)
         splitSelectDataHolder.resetState()
         assertFalse(splitSelectDataHolder.isSplitSelectActive())
     }
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index 1e39a34..18b1ea0 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -27,6 +27,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.LauncherState
 import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.logging.StatsLogManager.StatsLogger
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.statehandlers.DepthController
 import com.android.launcher3.statemanager.StateManager
@@ -45,6 +46,8 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.`when`
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
@@ -56,13 +59,14 @@
     private val systemUiProxy: SystemUiProxy = mock()
     private val depthController: DepthController = mock()
     private val statsLogManager: StatsLogManager = mock()
+    private val statsLogger: StatsLogger = mock()
     private val stateManager: StateManager<LauncherState> = mock()
     private val handler: Handler = mock()
     private val context: StatefulActivity<*> = mock()
     private val recentsModel: RecentsModel = mock()
     private val pendingIntent: PendingIntent = mock()
 
-    lateinit var splitSelectStateController: SplitSelectStateController
+    private lateinit var splitSelectStateController: SplitSelectStateController
 
     private val primaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId)
     private val nonPrimaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId + 10)
@@ -74,6 +78,9 @@
 
     @Before
     fun setup() {
+        `when`(statsLogManager.logger()).thenReturn(statsLogger)
+        `when`(statsLogger.withInstanceId(any())).thenReturn(statsLogger)
+        `when`(statsLogger.withItemInfo(any())).thenReturn(statsLogger)
         splitSelectStateController =
             SplitSelectStateController(
                 context,
@@ -593,9 +600,10 @@
     @Test
     fun secondPendingIntentSet() {
         val itemInfo = ItemInfo()
+        val itemInfo2 = ItemInfo()
         whenever(pendingIntent.creatorUserHandle).thenReturn(primaryUserHandle)
         splitSelectStateController.setInitialTaskSelect(null, 0, itemInfo, null, 1)
-        splitSelectStateController.setSecondTask(pendingIntent)
+        splitSelectStateController.setSecondTask(pendingIntent, itemInfo2)
         assertTrue(splitSelectStateController.isBothSplitAppsConfirmed)
     }
 
diff --git a/res/drawable-sw720dp/ic_transient_taskbar_all_apps_search_button.xml b/res/drawable-sw720dp/ic_transient_taskbar_all_apps_search_button.xml
index eb5b41e..8510c66 100644
--- a/res/drawable-sw720dp/ic_transient_taskbar_all_apps_search_button.xml
+++ b/res/drawable-sw720dp/ic_transient_taskbar_all_apps_search_button.xml
@@ -20,17 +20,7 @@
     android:viewportHeight="52"
     android:viewportWidth="52">
 
-  <path
-      android:fillColor="#FF000000"
-      android:pathData="M18.25,17.75m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0" />
-  <path
-      android:fillColor="#FF000000"
-      android:pathData="M18.25,34.25m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0" />
-  <path
-      android:fillColor="#FF000000"
-      android:pathData="M34.75,17.75m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0" />
-  <path
-      android:fillColor="#FF000000"
-      android:fillType="evenOdd"
-      android:pathData="M37.75,34.25C37.75,32.6 36.4,31.25 34.75,31.25C33.1,31.25 31.75,32.6 31.75,34.25C31.75,35.9 33.1,37.25 34.75,37.25C36.4,37.25 37.75,35.9 37.75,34.25ZM28.75,34.25C28.75,30.935 31.435,28.25 34.75,28.25C38.065,28.25 40.75,30.935 40.75,34.25C40.75,35.361 40.449,36.401 39.923,37.292L44.5,41.884L42.386,43.999L37.795,39.422C36.902,39.948 35.862,40.25 34.75,40.25C31.435,40.25 28.75,37.565 28.75,34.25Z" />
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M41.46,42.463L37.063,38.104C36.633,38.398 36.166,38.621 35.663,38.773C35.16,38.925 34.634,39.001 34.084,39.001C32.572,39.001 31.279,38.464 30.203,37.389C29.126,36.314 28.588,35.021 28.588,33.512C28.588,32.002 29.126,30.71 30.201,29.636C31.276,28.562 32.568,28.025 34.078,28.025C35.588,28.025 36.879,28.562 37.953,29.635C39.027,30.708 39.564,31.999 39.564,33.506C39.564,34.071 39.485,34.604 39.325,35.108C39.165,35.611 38.938,36.078 38.644,36.509L43.026,40.882L41.46,42.463ZM18.045,39.015C16.538,39.015 15.248,38.479 14.174,37.404C13.101,36.331 12.564,35.031 12.564,33.506C12.564,31.999 13.101,30.708 14.174,29.635C15.248,28.562 16.538,28.025 18.045,28.025C19.57,28.025 20.87,28.562 21.944,29.635C23.018,30.708 23.555,31.999 23.555,33.506C23.555,35.031 23.018,36.331 21.944,37.404C20.87,38.479 19.57,39.015 18.045,39.015ZM34.084,36.766C34.972,36.766 35.733,36.45 36.365,35.819C36.998,35.189 37.314,34.417 37.314,33.506C37.314,32.618 36.998,31.857 36.365,31.224C35.733,30.591 34.972,30.275 34.084,30.275C33.172,30.275 32.401,30.591 31.77,31.224C31.139,31.857 30.824,32.618 30.824,33.506C30.824,34.417 31.139,35.189 31.77,35.819C32.401,36.45 33.172,36.766 34.084,36.766ZM18.045,23.006C16.538,23.006 15.248,22.469 14.174,21.395C13.101,20.321 12.564,19.021 12.564,17.496C12.564,15.989 13.101,14.699 14.174,13.626C15.248,12.552 16.538,12.016 18.045,12.016C19.57,12.016 20.87,12.552 21.944,13.626C23.018,14.699 23.555,15.989 23.555,17.496C23.555,19.021 23.018,20.321 21.944,21.395C20.87,22.469 19.57,23.006 18.045,23.006ZM34.084,23.006C32.559,23.006 31.259,22.469 30.185,21.395C29.111,20.321 28.574,19.021 28.574,17.496C28.574,15.989 29.111,14.699 30.185,13.626C31.259,12.552 32.559,12.016 34.084,12.016C35.591,12.016 36.881,12.552 37.954,13.626C39.028,14.699 39.564,15.989 39.564,17.496C39.564,19.021 39.028,20.321 37.954,21.395C36.881,22.469 35.591,23.006 34.084,23.006Z" />
 </vector>
\ No newline at end of file
diff --git a/res/drawable/ic_lock.xml b/res/drawable/ic_lock.xml
new file mode 100644
index 0000000..055e6b4
--- /dev/null
+++ b/res/drawable/ic_lock.xml
@@ -0,0 +1,24 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="20dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+    <path
+        android:fillColor="?attr/materialColorOnPrimaryFixed"
+        android:pathData="M263.72,864Q234,864 213,842.85Q192,821.7 192,792L192,408Q192,378.3 213.15,357.15Q234.3,336 264,336L288,336L288,240Q288,160.32 344.23,104.16Q400.45,48 480.23,48Q560,48 616,104.16Q672,160.32 672,240L672,336L696,336Q725.7,336 746.85,357.15Q768,378.3 768,408L768,792Q768,821.7 746.84,842.85Q725.68,864 695.96,864L263.72,864ZM264,792L696,792Q696,792 696,792Q696,792 696,792L696,408Q696,408 696,408Q696,408 696,408L264,408Q264,408 264,408Q264,408 264,408L264,792Q264,792 264,792Q264,792 264,792ZM480.21,672Q510,672 531,650.79Q552,629.58 552,599.79Q552,570 530.79,549Q509.58,528 479.79,528Q450,528 429,549.21Q408,570.42 408,600.21Q408,630 429.21,651Q450.42,672 480.21,672ZM360,336L600,336L600,240Q600,190 565,155Q530,120 480,120Q430,120 395,155Q360,190 360,240L360,336ZM264,792Q264,792 264,792Q264,792 264,792L264,408Q264,408 264,408Q264,408 264,408L264,408Q264,408 264,408Q264,408 264,408L264,792Q264,792 264,792Q264,792 264,792L264,792Z"/>
+</vector>
diff --git a/res/drawable/bg_ps_settings_button.xml b/res/drawable/ic_ps_settings.xml
similarity index 93%
rename from res/drawable/bg_ps_settings_button.xml
rename to res/drawable/ic_ps_settings.xml
index c06e0c0..47edeb8 100644
--- a/res/drawable/bg_ps_settings_button.xml
+++ b/res/drawable/ic_ps_settings.xml
@@ -15,13 +15,10 @@
   -->
 
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/ps_button_height"
+    android:width="@dimen/ps_button_width"
     android:height="@dimen/ps_button_height"
     android:viewportWidth="40"
     android:viewportHeight="40">
-    <path
-        android:pathData="M20,0L20,0A20,20 0,0 1,40 20L40,20A20,20 0,0 1,20 40L20,40A20,20 0,0 1,0 20L0,20A20,20 0,0 1,20 0z"
-        android:fillColor="?attr/materialColorSurfaceBright"/>
     <group>
         <clip-path
             android:pathData="M10,10h20v20h-20z"/>
diff --git a/res/drawable/ic_taskbar_all_apps_search_button.xml b/res/drawable/ic_taskbar_all_apps_search_button.xml
index 5b4b01b..49667b7 100644
--- a/res/drawable/ic_taskbar_all_apps_search_button.xml
+++ b/res/drawable/ic_taskbar_all_apps_search_button.xml
@@ -19,17 +19,7 @@
     android:autoMirrored="true"
     android:viewportHeight="44"
     android:viewportWidth="44">
-  <path
-      android:fillColor="#FF000000"
-      android:pathData="M14.333,14.333m-5.333,0a5.333,5.333 0,1 1,10.667 0a5.333,5.333 0,1 1,-10.667 0" />
-  <path
-      android:fillColor="#FF000000"
-      android:pathData="M14.333,28.997m-5.333,0a5.333,5.333 0,1 1,10.667 0a5.333,5.333 0,1 1,-10.667 0" />
-  <path
-      android:fillColor="#FF000000"
-      android:pathData="M28.999,14.333m-5.333,0a5.333,5.333 0,1 1,10.667 0a5.333,5.333 0,1 1,-10.667 0" />
-  <path
-      android:fillColor="#FF000000"
-      android:fillType="evenOdd"
-      android:pathData="M31.668,29.005C31.668,27.538 30.468,26.338 29.001,26.338C27.535,26.338 26.335,27.538 26.335,29.005C26.335,30.472 27.535,31.672 29.001,31.672C30.468,31.672 31.668,30.472 31.668,29.005ZM23.668,29.005C23.668,26.059 26.055,23.672 29.001,23.672C31.948,23.672 34.335,26.059 34.335,29.005C34.335,29.992 34.067,30.917 33.6,31.709L37.669,35.791L35.789,37.671L31.708,33.602C30.915,34.07 29.989,34.339 29.001,34.339C26.055,34.339 23.668,31.952 23.668,29.005Z" />
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M35.351,36.744L31.443,32.869C31.06,33.131 30.645,33.329 30.198,33.464C29.751,33.599 29.283,33.667 28.794,33.667C27.451,33.667 26.301,33.189 25.344,32.233C24.388,31.278 23.91,30.129 23.91,28.787C23.91,27.445 24.387,26.297 25.343,25.343C26.299,24.388 27.447,23.91 28.789,23.91C30.131,23.91 31.279,24.388 32.234,25.341C33.189,26.296 33.666,27.442 33.666,28.782C33.666,29.284 33.595,29.759 33.453,30.206C33.311,30.653 33.11,31.069 32.848,31.451L36.743,35.339L35.351,36.744ZM14.538,33.68C13.198,33.68 12.051,33.202 11.097,32.248C10.143,31.293 9.666,30.138 9.666,28.782C9.666,27.442 10.143,26.296 11.097,25.341C12.051,24.388 13.198,23.91 14.538,23.91C15.893,23.91 17.049,24.388 18.003,25.341C18.958,26.296 19.435,27.442 19.435,28.782C19.435,30.138 18.958,31.293 18.003,32.248C17.049,33.202 15.893,33.68 14.538,33.68ZM28.794,31.68C29.584,31.68 30.26,31.399 30.822,30.839C31.385,30.278 31.666,29.593 31.666,28.782C31.666,27.992 31.385,27.316 30.822,26.754C30.26,26.192 29.584,25.91 28.794,25.91C27.984,25.91 27.298,26.192 26.738,26.754C26.177,27.316 25.897,27.992 25.897,28.782C25.897,29.593 26.177,30.278 26.738,30.839C27.298,31.399 27.984,31.68 28.794,31.68ZM14.538,19.449C13.198,19.449 12.051,18.972 11.097,18.017C10.143,17.062 9.666,15.907 9.666,14.552C9.666,13.212 10.143,12.065 11.097,11.111C12.051,10.157 13.198,9.68 14.538,9.68C15.893,9.68 17.049,10.157 18.003,11.111C18.958,12.065 19.435,13.212 19.435,14.552C19.435,15.907 18.958,17.062 18.003,18.017C17.049,18.972 15.893,19.449 14.538,19.449ZM28.794,19.449C27.439,19.449 26.284,18.972 25.329,18.017C24.374,17.062 23.897,15.907 23.897,14.552C23.897,13.212 24.374,12.065 25.329,11.111C26.284,10.157 27.439,9.68 28.794,9.68C30.134,9.68 31.281,10.157 32.235,11.111C33.189,12.065 33.666,13.212 33.666,14.552C33.666,15.907 33.189,17.062 32.235,18.017C31.281,18.972 30.134,19.449 28.794,19.449Z" />
 </vector>
diff --git a/res/drawable/ic_transient_taskbar_all_apps_search_button.xml b/res/drawable/ic_transient_taskbar_all_apps_search_button.xml
index 85a17f5..b422995 100644
--- a/res/drawable/ic_transient_taskbar_all_apps_search_button.xml
+++ b/res/drawable/ic_transient_taskbar_all_apps_search_button.xml
@@ -19,17 +19,8 @@
     android:autoMirrored="true"
     android:viewportHeight="48"
     android:viewportWidth="48">
-  <path
-      android:fillColor="#FF000000"
-      android:pathData="M16.833,16.333m-5.333,0a5.333,5.333 0,1 1,10.667 0a5.333,5.333 0,1 1,-10.667 0" />
-  <path
-      android:fillColor="#FF000000"
-      android:pathData="M16.833,30.997m-5.333,0a5.333,5.333 0,1 1,10.667 0a5.333,5.333 0,1 1,-10.667 0" />
-  <path
-      android:fillColor="#FF000000"
-      android:pathData="M31.499,16.333m-5.333,0a5.333,5.333 0,1 1,10.667 0a5.333,5.333 0,1 1,-10.667 0" />
-  <path
-      android:fillColor="#FF000000"
-      android:fillType="evenOdd"
-      android:pathData="M34.168,31.005C34.168,29.538 32.968,28.338 31.501,28.338C30.035,28.338 28.835,29.538 28.835,31.005C28.835,32.472 30.035,33.672 31.501,33.672C32.968,33.672 34.168,32.472 34.168,31.005ZM26.168,31.005C26.168,28.059 28.555,25.672 31.501,25.672C34.448,25.672 36.835,28.059 36.835,31.005C36.835,31.992 36.567,32.917 36.1,33.709L40.169,37.791L38.289,39.671L34.208,35.602C33.415,36.07 32.489,36.339 31.501,36.339C28.555,36.339 26.168,33.952 26.168,31.005Z" />
-</vector>
+
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M38.351,38.744L34.443,34.869C34.06,35.131 33.645,35.329 33.198,35.464C32.751,35.599 32.283,35.667 31.794,35.667C30.451,35.667 29.301,35.189 28.344,34.233C27.388,33.278 26.91,32.129 26.91,30.787C26.91,29.445 27.387,28.297 28.343,27.343C29.299,26.388 30.447,25.91 31.789,25.91C33.131,25.91 34.279,26.388 35.234,27.341C36.189,28.296 36.666,29.442 36.666,30.782C36.666,31.284 36.595,31.759 36.453,32.206C36.311,32.653 36.11,33.069 35.848,33.451L39.743,37.339L38.351,38.744ZM17.538,35.68C16.198,35.68 15.051,35.202 14.097,34.248C13.143,33.293 12.666,32.138 12.666,30.782C12.666,29.442 13.143,28.296 14.097,27.341C15.051,26.388 16.198,25.91 17.538,25.91C18.893,25.91 20.049,26.388 21.003,27.341C21.958,28.296 22.435,29.442 22.435,30.782C22.435,32.138 21.958,33.293 21.003,34.248C20.049,35.202 18.893,35.68 17.538,35.68ZM31.794,33.68C32.584,33.68 33.26,33.399 33.822,32.839C34.385,32.278 34.666,31.593 34.666,30.782C34.666,29.992 34.385,29.316 33.822,28.754C33.26,28.192 32.584,27.91 31.794,27.91C30.984,27.91 30.298,28.192 29.738,28.754C29.177,29.316 28.897,29.992 28.897,30.782C28.897,31.593 29.177,32.278 29.738,32.839C30.298,33.399 30.984,33.68 31.794,33.68ZM17.538,21.449C16.198,21.449 15.051,20.972 14.097,20.017C13.143,19.062 12.666,17.907 12.666,16.552C12.666,15.212 13.143,14.065 14.097,13.111C15.051,12.157 16.198,11.68 17.538,11.68C18.893,11.68 20.049,12.157 21.003,13.111C21.958,14.065 22.435,15.212 22.435,16.552C22.435,17.907 21.958,19.062 21.003,20.017C20.049,20.972 18.893,21.449 17.538,21.449ZM31.794,21.449C30.439,21.449 29.284,20.972 28.329,20.017C27.374,19.062 26.897,17.907 26.897,16.552C26.897,15.212 27.374,14.065 28.329,13.111C29.284,12.157 30.439,11.68 31.794,11.68C33.134,11.68 34.281,12.157 35.235,13.111C36.189,14.065 36.666,15.212 36.666,16.552C36.666,17.907 36.189,19.062 35.235,20.017C34.281,20.972 33.134,21.449 31.794,21.449Z" />
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ps_lock_background.xml b/res/drawable/ps_lock_background.xml
new file mode 100644
index 0000000..b81c23f
--- /dev/null
+++ b/res/drawable/ps_lock_background.xml
@@ -0,0 +1,27 @@
+<?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.
+  -->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:inset="4dp">
+    <shape android:shape="rectangle">
+        <corners android:radius="@dimen/ps_lock_corner_radius" />
+        <solid android:color="?attr/materialColorPrimaryFixedDim" />
+        <padding
+            android:left="@dimen/ps_lock_button_background_padding"
+            android:right="@dimen/ps_lock_button_background_padding" />
+    </shape>
+</inset>
\ No newline at end of file
diff --git a/res/drawable/ps_settings_background.xml b/res/drawable/ps_settings_background.xml
new file mode 100644
index 0000000..b0c6b5b
--- /dev/null
+++ b/res/drawable/ps_settings_background.xml
@@ -0,0 +1,23 @@
+<?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.
+  -->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:inset="4dp">
+    <shape android:shape="rectangle">
+        <corners android:radius="@dimen/ps_lock_corner_radius" />
+        <solid android:color="?attr/materialColorSurfaceBright" />
+    </shape>
+</inset>
\ No newline at end of file
diff --git a/res/layout/private_space_header.xml b/res/layout/private_space_header.xml
index 24e290d..0b0af87 100644
--- a/res/layout/private_space_header.xml
+++ b/res/layout/private_space_header.xml
@@ -18,6 +18,7 @@
 <RelativeLayout
         android:id="@+id/ps_header_layout"
         xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
         android:layout_width="match_parent"
         android:layout_height="@dimen/ps_header_height"
         android:background="@drawable/bg_ps_header"
@@ -25,27 +26,53 @@
         android:gravity="center_vertical"
         android:orientation="horizontal">
 
-    <ImageButton
-        android:id="@+id/ps_lock_unlock_button"
+    <LinearLayout
+        android:id="@+id/settingsAndLockGroup"
         android:layout_width="wrap_content"
-        android:layout_height="@dimen/ps_header_image_height"
+        android:layout_height="wrap_content"
+        android:layout_centerVertical="true"
+        android:gravity="center_vertical"
         android:layout_alignParentEnd="true"
-        android:layout_centerVertical="true"
-        android:background="@android:color/transparent"
-        android:layout_marginEnd="@dimen/ps_header_layout_margin"
-        android:contentDescription="@string/ps_container_lock_unlock_button" />
-
-    <ImageButton
-        android:id="@+id/ps_settings_button"
-        android:layout_width="wrap_content"
-        android:layout_height="@dimen/ps_header_image_height"
-        android:layout_toStartOf="@+id/ps_lock_unlock_button"
-        android:layout_centerVertical="true"
-        android:background="@android:color/transparent"
-        android:layout_marginEnd="@dimen/ps_header_settings_icon_margin_end"
-        android:src="@drawable/bg_ps_settings_button"
-        android:contentDescription="@string/ps_container_settings" />
-
+        android:animateLayoutChanges="true">
+        <ImageButton
+            android:id="@+id/ps_settings_button"
+            android:layout_width="@dimen/ps_header_image_height"
+            android:layout_height="@dimen/ps_header_image_height"
+            android:background="@drawable/ps_settings_background"
+            android:layout_marginEnd="@dimen/ps_header_settings_icon_margin_end"
+            android:src="@drawable/ic_ps_settings"
+            android:contentDescription="@string/ps_container_settings" />
+        <LinearLayout
+            android:id="@+id/ps_lock_unlock_button"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/ps_header_image_height"
+            android:background="@drawable/ps_lock_background"
+            android:gravity="center_vertical"
+            android:layout_marginEnd="@dimen/ps_header_layout_margin"
+            android:contentDescription="@string/ps_container_lock_unlock_button">
+            <ImageView
+                android:id="@+id/lock_icon"
+                android:layout_width="@dimen/ps_lock_icon_size"
+                android:layout_height="@dimen/ps_lock_icon_size"
+                android:layout_marginTop="@dimen/ps_lock_icon_margin_top"
+                android:layout_marginBottom="@dimen/ps_lock_icon_margin_bottom"
+                android:importantForAccessibility="no"
+                android:src="@drawable/ic_lock"
+                app:tint="@color/material_color_primary_fixed_dim"
+                android:scaleType="center"/>
+            <TextView
+                android:id="@+id/lock_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="@dimen/ps_lock_icon_text_margin_start_expanded"
+                android:layout_marginEnd="@dimen/ps_lock_icon_text_margin_end_expanded"
+                android:textColor="@color/material_color_on_primary_fixed"
+                android:textSize="14sp"
+                android:text="@string/ps_container_lock_title"
+                android:visibility="gone"
+                style="@style/TextHeadline"/>
+        </LinearLayout>
+    </LinearLayout>
     <ImageView
         android:id="@+id/ps_transition_image"
         android:layout_width="wrap_content"
@@ -63,6 +90,7 @@
         android:layout_height="@dimen/ps_header_text_height"
         android:layout_alignParentStart="true"
         android:layout_centerVertical="true"
+        android:layout_toStartOf="@+id/settingsAndLockGroup"
         android:gravity="center_vertical"
         android:layout_marginStart="@dimen/ps_header_layout_margin"
         android:text="@string/ps_container_title"
diff --git a/res/layout/widget_recommendations.xml b/res/layout/widget_recommendations.xml
new file mode 100644
index 0000000..89821ac
--- /dev/null
+++ b/res/layout/widget_recommendations.xml
@@ -0,0 +1,58 @@
+<?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.
+  -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto">
+
+    <!--
+    Shown when there are more than one pages
+    Note: on page change, using accessibility live region lets user know that the title has changed.
+    -->
+    <TextView
+        android:id="@+id/recommendations_page_title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="12dp"
+        android:layout_marginTop="16dp"
+        android:accessibilityLiveRegion="polite"
+        android:gravity="center_horizontal"
+        android:lineHeight="20sp"
+        android:textColor="?attr/widgetPickerTitleColor"
+        android:textFontWeight="500"
+        android:textSize="16sp"
+        android:visibility="gone" />
+    <!-- Shown when there are more than one pages -->
+    <com.android.launcher3.pageindicators.PageIndicatorDots
+        android:id="@+id/widget_recommendations_page_indicator"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:elevation="1dp"
+        android:visibility="gone" />
+    <!--
+     Note: importantForAccessibility = yes on this view ensures that with talkback, when user
+     swipes right on the last item in current page, they are taken to the next page. And, doing
+     the same on the last page, takes them to the next section e.g. apps list in single pane
+     picker.
+    -->
+    <com.android.launcher3.widget.picker.WidgetRecommendationsView
+        android:id="@+id/widget_recommendations_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:background="@drawable/widgets_surface_background"
+        android:importantForAccessibility="yes"
+        launcher:pageIndicator="@+id/widget_recommendations_page_indicator" />
+</merge>
\ No newline at end of file
diff --git a/res/layout/widget_recommendations_table.xml b/res/layout/widget_recommendations_table.xml
new file mode 100644
index 0000000..e3f0562
--- /dev/null
+++ b/res/layout/widget_recommendations_table.xml
@@ -0,0 +1,21 @@
+<?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.launcher3.widget.picker.WidgetsRecommendationTableLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingHorizontal="@dimen/widget_recommendations_table_horizontal_padding"
+    android:paddingVertical="@dimen/widget_recommendations_table_vertical_padding" />
diff --git a/res/layout/widgets_full_sheet_paged_view.xml b/res/layout/widgets_full_sheet_paged_view.xml
index 069d4bc..1d37043 100644
--- a/res/layout/widgets_full_sheet_paged_view.xml
+++ b/res/layout/widgets_full_sheet_paged_view.xml
@@ -73,15 +73,18 @@
             <include layout="@layout/widgets_search_bar" />
         </FrameLayout>
 
-        <com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
-            android:id="@+id/recommended_widget_table"
+        <!-- Shown when there are recommendations to display -->
+        <LinearLayout
+            android:id="@+id/widget_recommendations_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginTop="8dp"
-            android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin"
             android:background="@drawable/widgets_surface_background"
-            android:paddingVertical="@dimen/recommended_widgets_table_vertical_padding"
-            android:visibility="gone" />
+            android:orientation="vertical"
+            android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin"
+            android:visibility="gone">
+            <include layout="@layout/widget_recommendations" />
+        </LinearLayout>
 
         <com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip
             android:id="@+id/tabs"
diff --git a/res/layout/widgets_full_sheet_recyclerview.xml b/res/layout/widgets_full_sheet_recyclerview.xml
index 25bbad4..dca08ff 100644
--- a/res/layout/widgets_full_sheet_recyclerview.xml
+++ b/res/layout/widgets_full_sheet_recyclerview.xml
@@ -56,14 +56,17 @@
             <include layout="@layout/widgets_search_bar" />
         </FrameLayout>
 
-        <com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
-            android:id="@+id/recommended_widget_table"
+        <!-- Shown when there are recommendations to display -->
+        <LinearLayout
+            android:id="@+id/widget_recommendations_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginTop="8dp"
             android:background="@drawable/widgets_surface_background"
-            android:paddingVertical="@dimen/recommended_widgets_table_vertical_padding"
-            android:visibility="gone" />
+            android:orientation="vertical"
+            android:visibility="gone">
+            <include layout="@layout/widget_recommendations" />
+        </LinearLayout>
     </com.android.launcher3.views.StickyHeaderLayout>
 
 </merge>
\ No newline at end of file
diff --git a/res/layout/widgets_two_pane_sheet.xml b/res/layout/widgets_two_pane_sheet.xml
index f692e24..8e45740f 100644
--- a/res/layout/widgets_two_pane_sheet.xml
+++ b/res/layout/widgets_two_pane_sheet.xml
@@ -118,13 +118,16 @@
                         android:background="@drawable/widgets_surface_background"
                         android:importantForAccessibility="yes"
                         android:id="@+id/right_pane">
-                        <com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
-                            android:id="@+id/recommended_widget_table"
+                        <!-- Shown when there are recommendations to display -->
+                        <LinearLayout
+                            android:id="@+id/widget_recommendations_container"
                             android:layout_width="match_parent"
                             android:layout_height="wrap_content"
-                            android:paddingHorizontal=
-                                "@dimen/widget_list_horizontal_margin_two_pane"
-                            android:visibility="gone" />
+                            android:background="@drawable/widgets_surface_background"
+                            android:orientation="vertical"
+                            android:visibility="gone">
+                            <include layout="@layout/widget_recommendations" />
+                        </LinearLayout>
                     </LinearLayout>
                 </ScrollView>
             </FrameLayout>
diff --git a/res/values-sw720dp-land/dimens.xml b/res/values-sw720dp-land/dimens.xml
index 4d0ac38..dd58cee 100644
--- a/res/values-sw720dp-land/dimens.xml
+++ b/res/values-sw720dp-land/dimens.xml
@@ -32,7 +32,4 @@
 <!-- Widget picker-->
     <dimen name="widget_list_horizontal_margin">49dp</dimen>
     <dimen name="widget_list_horizontal_margin_two_pane">24dp</dimen>
-
-<!-- Bottom sheet-->
-    <dimen name="bottom_sheet_extra_top_padding">0dp</dimen>
 </resources>
diff --git a/res/values-sw720dp/dimens.xml b/res/values-sw720dp/dimens.xml
index 2b0382d..3c79588 100644
--- a/res/values-sw720dp/dimens.xml
+++ b/res/values-sw720dp/dimens.xml
@@ -38,9 +38,6 @@
 <!-- Widget picker-->
     <dimen name="widget_list_horizontal_margin">30dp</dimen>
 
-<!-- Bottom sheet-->
-    <dimen name="bottom_sheet_extra_top_padding">300dp</dimen>
-
     <!--  Folder spaces  -->
     <dimen name="folder_footer_horiz_padding">24dp</dimen>
 </resources>
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index 841e07b..e2f610e 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -103,6 +103,7 @@
     <color name="work_fab_icon_color">
         @android:color/system_accent1_900</color>
 
+    <color name="overview_foreground_scrim_color">@android:color/system_neutral1_1000</color>
 
     <color name="material_color_on_secondary_fixed_variant">@android:color/system_accent2_700</color>
     <color name="material_color_on_tertiary_fixed_variant">@android:color/system_accent3_700</color>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c187000..c101762 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -182,9 +182,8 @@
     <dimen name="widget_apps_tabs_vertical_padding">6dp</dimen>
     <dimen name="widget_picker_landscape_tablet_left_right_margin">117dp</dimen>
     <dimen name="widget_picker_two_panels_left_right_margin">0dp</dimen>
-
-    <dimen name="recommended_widgets_table_vertical_padding">8dp</dimen>
-
+    <dimen name="widget_recommendations_table_vertical_padding">8dp</dimen>
+    <dimen name="widget_recommendations_table_horizontal_padding">16dp</dimen>
     <!-- Bottom margin for the search and recommended widgets container without work profile -->
     <dimen name="search_and_recommended_widgets_container_bottom_margin">16dp</dimen>
     <!-- Bottom margin for the search and recommended widgets container with work profile -->
@@ -454,7 +453,6 @@
     <dimen name="padded_rounded_button_padding">8dp</dimen>
 
     <!-- Bottom sheet related parameters -->
-    <dimen name="bottom_sheet_extra_top_padding">0dp</dimen>
     <dimen name="bottom_sheet_handle_area_height">36dp</dimen>
     <dimen name="bottom_sheet_handle_width">32dp</dimen>
     <dimen name="bottom_sheet_handle_height">4dp</dimen>
@@ -476,15 +474,22 @@
     <dimen name="ps_container_corner_radius">24dp</dimen>
     <dimen name="ps_header_height">64dp</dimen>
     <dimen name="ps_header_relative_layout_height">48dp</dimen>
-    <dimen name="ps_header_image_height">36dp</dimen>
+    <dimen name="ps_header_image_height">48dp</dimen>
     <dimen name="ps_header_text_height">24dp</dimen>
     <dimen name="ps_header_layout_margin">16dp</dimen>
     <dimen name="ps_header_settings_icon_margin_end">8dp</dimen>
     <dimen name="ps_header_text_size">16sp</dimen>
-    <dimen name="ps_button_height">36dp</dimen>
-    <dimen name="ps_button_width">36dp</dimen>
+    <dimen name="ps_button_height">40dp</dimen>
+    <dimen name="ps_button_width">40dp</dimen>
     <dimen name="ps_lock_button_width">89dp</dimen>
     <dimen name="ps_app_divider_padding">16dp</dimen>
+    <dimen name="ps_lock_corner_radius">20dp</dimen>
+    <dimen name="ps_lock_icon_size">20dp</dimen>
+    <dimen name="ps_lock_icon_margin_top">10dp</dimen>
+    <dimen name="ps_lock_icon_margin_bottom">10dp</dimen>
+    <dimen name="ps_lock_icon_text_margin_start_expanded">8dp</dimen>
+    <dimen name="ps_lock_icon_text_margin_end_expanded">6dp</dimen>
+    <dimen name="ps_lock_button_background_padding">10dp</dimen>
 
     <!-- WindowManagerProxy -->
     <dimen name="max_width_and_height_of_small_display_cutout">136px</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a4e7ec4..379cdda 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -71,7 +71,7 @@
     <!-- Widget suggestions header title in the full widgets picker for large screen devices
     in landscape mode. [CHAR_LIMIT=50] -->
     <string name="suggested_widgets_header_title">Suggestions</string>
-    <string name="productivity_widget_recommendation_category_label">Boost your day</string>
+    <string name="productivity_widget_recommendation_category_label">Your Daily Essentials</string>
     <string name="news_widget_recommendation_category_label">News For You</string>
     <string name="social_and_entertainment_widget_recommendation_category_label">Your Chill Zone</string>
     <string name="fitness_widget_recommendation_category_label">Reach Your Fitness Goals</string>
@@ -465,6 +465,7 @@
     <string name="ps_container_settings">Private Space Settings</string>
     <!-- Description for Private Space Lock/Unlock button -->
     <string name="ps_container_lock_unlock_button">Lock/Unlock Private Space</string>
+    <string name="ps_container_lock_title">Lock</string>
     <!-- Description for Private Space Transition button -->
     <string name="ps_container_transition">Private Space Transitioning</string>
     <!-- Title for Private Space install app icon -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index b83be35..401155d 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -441,5 +441,6 @@
         <item name="android:textSize">16sp</item>
         <item name="android:textColor">@color/material_color_on_surface</item>
         <item name="android:fontFamily">google-sans-text-medium</item>
+        <item name="android:ellipsize">end</item>
     </style>
 </resources>
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index f72c556..e7b88dc 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -124,7 +124,8 @@
 
     /** Type of popups that should get exclusive accessibility focus. */
     public static final int TYPE_ACCESSIBLE = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE & ~TYPE_LISTENER
-            & ~TYPE_ALL_APPS_EDU & ~TYPE_TASKBAR_ALL_APPS & ~TYPE_PIN_IME_POPUP;
+            & ~TYPE_ALL_APPS_EDU & ~TYPE_TASKBAR_ALL_APPS & ~TYPE_PIN_IME_POPUP
+            & ~TYPE_WIDGET_RESIZE_FRAME;
 
     // These view all have particular operation associated with swipe down interaction.
     public static final int TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW = TYPE_WIDGETS_BOTTOM_SHEET |
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 2f0c096..259ddbd 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -69,13 +69,13 @@
 import com.android.launcher3.icons.FastBitmapDrawable;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.icons.PlaceHolderIconDrawable;
-import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.search.StringMatcherUtility;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.MultiTranslateDelegate;
 import com.android.launcher3.util.SafeCloseable;
@@ -190,7 +190,7 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mDisableRelayout = false;
 
-    private HandlerRunnable mIconLoadRequest;
+    private CancellableTask mIconLoadRequest;
 
     private boolean mEnableIconUpdateAnimation = false;
 
@@ -294,6 +294,12 @@
             mIconLoadRequest.cancel();
             mIconLoadRequest = null;
         }
+        // Reset any shifty arrangements in case animation is disrupted.
+        setPivotY(0);
+        setAlpha(1);
+        setScaleY(1);
+        setTranslationY(0);
+        setVisibility(VISIBLE);
     }
 
     private void cancelDotScaleAnim() {
@@ -419,11 +425,13 @@
     }
 
     /**
-     *  Only if actual text can be displayed in two line, the {@code true} value will be effective.
+     * Only if actual text can be displayed in two line, the {@code true} value will be effective.
      */
     protected boolean shouldUseTwoLine() {
-        return (FeatureFlags.enableTwolineAllapps() && isCurrentLanguageEnglish())
-                && (mDisplay == DISPLAY_ALL_APPS || mDisplay == DISPLAY_PREDICTION_ROW);
+        return FeatureFlags.enableTwolineAllapps() && isCurrentLanguageEnglish()
+                && (mDisplay == DISPLAY_ALL_APPS || mDisplay == DISPLAY_PREDICTION_ROW)
+                && (!Flags.enableTwolineToggle() || (Flags.enableTwolineToggle()
+                && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(getContext())));
     }
 
     protected boolean isCurrentLanguageEnglish() {
@@ -575,12 +583,12 @@
 
     /**
      * Find the appropriate text spacing to display the provided text
-     * @param paint the paint used by the text view
-     * @param text the text to display
-     * @param allowedWidthPx available space to render the text
-     * @param minSpacingEm minimum spacing allowed between characters
-     * @return the final textSpacing value
      *
+     * @param paint          the paint used by the text view
+     * @param text           the text to display
+     * @param allowedWidthPx available space to render the text
+     * @param minSpacingEm   minimum spacing allowed between characters
+     * @return the final textSpacing value
      * @see #setLetterSpacing(float)
      */
     private float findBestSpacingValue(TextPaint paint, String text, float allowedWidthPx,
@@ -808,13 +816,13 @@
      * iterating through the list of break points and determining if the strings between the break
      * points can fit within the line it is in. We will show the modified string if there is enough
      * horizontal and vertical space, otherwise this method will just return the original string.
-     *  Example assuming each character takes up one spot:
-     *  title = "Battery Stats", breakpoint = [6], stringPtr = 0, limitedWidth = 7
-     *  We get the current word -> from sublist(0, breakpoint[i]+1) so sublist (0,7) -> Battery,
-     *  now stringPtr = 7 then from sublist(7) the current string is " Stats" and the runningWidth
-     *  at this point exceeds limitedWidth and so we put " Stats" onto the next line (after checking
-     *  if the first char is a SPACE, we trim to append "Stats". So resulting string would be
-     *  "Battery\nStats"
+     * Example assuming each character takes up one spot:
+     * title = "Battery Stats", breakpoint = [6], stringPtr = 0, limitedWidth = 7
+     * We get the current word -> from sublist(0, breakpoint[i]+1) so sublist (0,7) -> Battery,
+     * now stringPtr = 7 then from sublist(7) the current string is " Stats" and the runningWidth
+     * at this point exceeds limitedWidth and so we put " Stats" onto the next line (after checking
+     * if the first char is a SPACE, we trim to append "Stats". So resulting string would be
+     * "Battery\nStats"
      */
     public static CharSequence modifyTitleToSupportMultiLine(int limitedWidth, int limitedHeight,
             CharSequence title, TextPaint paint, IntArray breakPoints, float spacingMultiplier,
@@ -828,25 +836,25 @@
         StringBuilder newString = new StringBuilder();
         paint.setLetterSpacing(MIN_LETTER_SPACING);
         int stringPtr = 0;
-        for (int i = 0; i < breakPoints.size()+1; i++) {
+        for (int i = 0; i < breakPoints.size() + 1; i++) {
             if (i < breakPoints.size()) {
-                currentWord = title.subSequence(stringPtr, breakPoints.get(i)+1);
+                currentWord = title.subSequence(stringPtr, breakPoints.get(i) + 1);
             } else {
                 // last word from recent breakpoint until the end of the string
                 currentWord = title.subSequence(stringPtr, title.length());
             }
-            currentWordWidth = paint.measureText(currentWord,0, currentWord.length());
+            currentWordWidth = paint.measureText(currentWord, 0, currentWord.length());
             runningWidth += currentWordWidth;
             if (runningWidth <= limitedWidth) {
                 newString.append(currentWord);
             } else {
-                if (i != 0)  {
+                if (i != 0) {
                     // If putting word onto a new line, make sure there is no space or new line
                     // character in the beginning of the current word and just put in the rest of
                     // the characters.
                     CharSequence lastCharacters = title.subSequence(stringPtr, title.length());
                     int beginningLetterType =
-                            Character.getType(Character.codePointAt(lastCharacters,0));
+                            Character.getType(Character.codePointAt(lastCharacters, 0));
                     if (beginningLetterType == Character.SPACE_SEPARATOR
                             || beginningLetterType == Character.LINE_SEPARATOR) {
                         lastCharacters = lastCharacters.length() > 1
@@ -867,7 +875,7 @@
                 // no need to look forward into the string if we've already finished processing
                 break;
             }
-            stringPtr = breakPoints.get(i)+1;
+            stringPtr = breakPoints.get(i) + 1;
         }
         return newString.toString();
     }
@@ -957,12 +965,12 @@
         return preloadDrawable;
     }
 
-    private boolean isIconDisabled(ItemInfoWithIcon info) {
-        if (info.isArchived()) {
-            return info.getProgressLevel() == 0
-                    && (info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0;
-        }
-        return info.getProgressLevel() == 0;
+    /**
+     * Returns true to grey the icon if the icon is either suspended or if the icon is pending
+     * download
+     */
+    public boolean isIconDisabled(ItemInfoWithIcon info) {
+        return info.isDisabled() || info.isPendingDownload();
     }
 
     public void applyDotState(ItemInfo itemInfo, boolean animate) {
@@ -1009,12 +1017,12 @@
             if ((info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) {
                 setContentDescription(getContext()
                         .getString(
-                            R.string.app_installing_title, info.title, percentageString));
+                                R.string.app_installing_title, info.title, percentageString));
             } else if ((info.runtimeStatusFlags
                     & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
                 setContentDescription(getContext()
                         .getString(
-                            R.string.app_downloading_title, info.title, percentageString));
+                                R.string.app_downloading_title, info.title, percentageString));
             }
         }
     }
@@ -1181,7 +1189,8 @@
     public SafeCloseable prepareDrawDragView() {
         resetIconScale();
         setForceHideDot(true);
-        return () -> { };
+        return () -> {
+        };
     }
 
     private void resetIconScale() {
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 563dfe2..4dd2fe3 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -416,15 +416,18 @@
         gridVisualizationPaddingY = res.getDimensionPixelSize(
                 R.dimen.grid_visualization_vertical_cell_spacing);
 
-        // Tablet portrait mode uses a single pane widget picker and extra padding may be applied on
-        // top to avoid making it look too elongated.
-        final boolean applyExtraTopPadding = isTablet
-                && !isLandscape
-                && (aspectRatio > MIN_ASPECT_RATIO_FOR_EXTRA_TOP_PADDING);
-        bottomSheetTopPadding = mInsets.top // statusbar height
-                + (applyExtraTopPadding ? res.getDimensionPixelSize(
-                R.dimen.bottom_sheet_extra_top_padding) : 0)
-                + (isTablet ? 0 : edgeMarginPx); // phones need edgeMarginPx additional padding
+        {
+            // In large screens, in portrait mode, a bottom sheet can appear too elongated, so, we
+            // apply additional padding.
+            final boolean applyExtraTopPadding = isTablet
+                    && !isLandscape
+                    && (aspectRatio > MIN_ASPECT_RATIO_FOR_EXTRA_TOP_PADDING);
+            final int derivedTopPadding = heightPx / 6;
+            bottomSheetTopPadding = mInsets.top // statusbar height
+                    + (applyExtraTopPadding ? derivedTopPadding : 0)
+                    + (isTablet ? 0 : edgeMarginPx); // phones need edgeMarginPx additional padding
+        }
+
         bottomSheetOpenDuration = res.getInteger(R.integer.config_bottomSheetOpenDuration);
         bottomSheetCloseDuration = res.getInteger(R.integer.config_bottomSheetCloseDuration);
         if (isTablet) {
@@ -714,7 +717,7 @@
         }
 
         // Calculate all of the remaining variables.
-        extraSpace = updateAvailableDimensions(res);
+        extraSpace = updateAvailableDimensions(context);
 
         calculateAndSetWorkspaceVerticalPadding(context, inv, extraSpace);
 
@@ -1006,14 +1009,14 @@
     /**
      * Returns the amount of extra (or unused) vertical space.
      */
-    private int updateAvailableDimensions(Resources res) {
+    private int updateAvailableDimensions(Context context) {
         iconCenterVertically = (mIsScalableGrid || mIsResponsiveGrid) && isVerticalBarLayout();
 
         if (mIsResponsiveGrid) {
             iconSizePx = mResponsiveWorkspaceCellSpec.getIconSize();
             iconTextSizePx = mResponsiveWorkspaceCellSpec.getIconTextSize();
             mIconDrawablePaddingOriginalPx = mResponsiveWorkspaceCellSpec.getIconDrawablePadding();
-            updateIconSize(1f, res);
+            updateIconSize(1f, context);
             updateWorkspacePadding();
             return 0;
         }
@@ -1023,7 +1026,7 @@
         iconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mMetrics));
         iconTextSizePx = pxFromSp(invIconTextSizeSp, mMetrics);
 
-        updateIconSize(1f, res);
+        updateIconSize(1f, context);
         updateWorkspacePadding();
 
         // Check to see if the icons fit within the available height.
@@ -1047,7 +1050,7 @@
 
         if (shouldScale) {
             float scale = Math.min(scaleX, scaleY);
-            updateIconSize(scale, res);
+            updateIconSize(scale, context);
             extraHeight = Math.max(0, maxHeight - getCellLayoutHeightSpecification());
         }
 
@@ -1093,7 +1096,7 @@
      * iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
      * hotseat sizes, workspaceSpringLoadedShrinkFactor, folderIconSizePx, and folderIconOffsetYPx.
      */
-    public void updateIconSize(float scale, Resources res) {
+    public void updateIconSize(float scale, Context context) {
         // Icon scale should never exceed 1, otherwise pixellation may occur.
         iconScale = Math.min(1f, scale);
         cellScaleToFit = scale;
@@ -1213,13 +1216,15 @@
         if (mIsResponsiveGrid) {
             updateAllAppsWithResponsiveMeasures();
         } else {
-            updateAllAppsIconSize(scale, res);
+            updateAllAppsIconSize(scale, context.getResources());
         }
         updateAllAppsContainerWidth();
         if (isVerticalLayout && !mIsResponsiveGrid) {
             hideWorkspaceLabelsIfNotEnoughSpace();
         }
-        if (FeatureFlags.enableTwolineAllapps()) {
+        if (FeatureFlags.enableTwolineAllapps()
+                && (!Flags.enableTwolineToggle() || (Flags.enableTwolineToggle()
+                && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(context)))) {
             // Add extra textHeight to the existing allAppsCellHeight.
             allAppsCellHeightPx += Utilities.calculateTextHeight(allAppsIconTextSizePx);
         }
diff --git a/src/com/android/launcher3/FastScrollRecyclerView.java b/src/com/android/launcher3/FastScrollRecyclerView.java
index 7ec0a89..51c7a05 100644
--- a/src/com/android/launcher3/FastScrollRecyclerView.java
+++ b/src/com/android/launcher3/FastScrollRecyclerView.java
@@ -201,7 +201,6 @@
         if (mScrollbar != null) {
             mScrollbar.reattachThumbToScroll();
         }
-        // Emphasized interpolators with 500ms duration
         smoothScrollBy(0, getAvailableScrollHeight(), Interpolators.EMPHASIZED, duration);
     }
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 2471254..edfef5e 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -27,6 +27,7 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_ICON_SURFACE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
+import static com.android.launcher3.Flags.enableAddAppWidgetViaConfigActivityV2;
 import static com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WIDGET_TRANSITION;
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
@@ -77,6 +78,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_EXIT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONRESUME;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONSTOP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPERIGHT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RECONFIGURED;
@@ -116,6 +118,8 @@
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -828,7 +832,7 @@
                 announceForAccessibility(R.string.item_added_to_workspace);
                 break;
             case REQUEST_CREATE_APPWIDGET:
-                completeAddAppWidget(appWidgetId, info, null, null);
+                completeAddAppWidget(appWidgetId, info, null, null, false, null);
                 break;
             case REQUEST_RECONFIGURE_APPWIDGET:
                 getStatsLogManager().logger().withItemInfo(info).log(LAUNCHER_WIDGET_RECONFIGURED);
@@ -1015,11 +1019,18 @@
         AppWidgetHostView boundWidget = null;
         if (resultCode == RESULT_OK) {
             animationType = Workspace.COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION;
-            final AppWidgetHostView layout = mAppWidgetHolder.createView(appWidgetId,
-                    requestArgs.getWidgetHandler().getProviderInfo(this));
+
+            // Now that we are exiting the config activity with RESULT_OK.
+            // If FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2 is enabled, we can retrieve the
+            // PendingAppWidgetHostView from LauncherWidgetHolder (it was added to
+            // LauncherWidgetHolder when starting the config activity).
+            final AppWidgetHostView layout = enableAddAppWidgetViaConfigActivityV2()
+                    ? getWorkspace().getWidgetForAppWidgetId(appWidgetId)
+                    : mAppWidgetHolder.createView(appWidgetId,
+                            requestArgs.getWidgetHandler().getProviderInfo(this));
             boundWidget = layout;
             onCompleteRunnable = () -> {
-                completeAddAppWidget(appWidgetId, requestArgs, layout, null);
+                completeAddAppWidget(appWidgetId, requestArgs, layout, null, false, null);
                 if (!isInState(EDIT_MODE)) {
                     mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
                 }
@@ -1449,14 +1460,15 @@
      */
     @Thunk
     void completeAddAppWidget(int appWidgetId, ItemInfo itemInfo,
-            AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo) {
+            @Nullable AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo,
+            boolean showPendingWidget, @Nullable Bitmap widgetPreviewBitmap) {
 
         if (appWidgetInfo == null) {
             appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(appWidgetId,
                     itemInfo.getTargetComponent());
         }
 
-        if (hostView == null) {
+        if (hostView == null && !showPendingWidget) {
             // Perform actual inflation because we're live
             hostView = mAppWidgetHolder.createView(appWidgetId, appWidgetInfo);
         }
@@ -1470,39 +1482,71 @@
         launcherInfo.minSpanX = itemInfo.minSpanX;
         launcherInfo.minSpanY = itemInfo.minSpanY;
         launcherInfo.user = appWidgetInfo.getProfile();
+        CellPos presenterPos = getCellPosMapper().mapModelToPresenter(itemInfo);
+        if (showPendingWidget) {
+            launcherInfo.restoreStatus = LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
+            PendingAppWidgetHostView pendingAppWidgetHostView =
+                    new PendingAppWidgetHostView(this, launcherInfo, appWidgetInfo);
+            pendingAppWidgetHostView.setPreviewBitmap(widgetPreviewBitmap);
+            hostView = pendingAppWidgetHostView;
+        } else if (hostView instanceof PendingAppWidgetHostView) {
+            ((PendingAppWidgetHostView) hostView).setPreviewBitmap(null);
+            // User has selected a widget config and exited the config activity, we can trigger
+            // re-inflation of PendingAppWidgetHostView to replace it with
+            // LauncherAppWidgetHostView in workspace.
+            completeRestoreAppWidget(appWidgetId, LauncherAppWidgetInfo.RESTORE_COMPLETED);
+
+            // Show resize frame on the newly inflated LauncherAppWidgetHostView.
+            LauncherAppWidgetHostView reInflatedHostView =
+                    getWorkspace().getWidgetForAppWidgetId(appWidgetId);
+            showWidgetResizeFrame(
+                    reInflatedHostView,
+                    (LauncherAppWidgetInfo) reInflatedHostView.getTag(),
+                    presenterPos);
+            return;
+        }
         if (itemInfo instanceof PendingAddWidgetInfo) {
             launcherInfo.sourceContainer = ((PendingAddWidgetInfo) itemInfo).sourceContainer;
         } else if (itemInfo instanceof PendingRequestArgs) {
             launcherInfo.sourceContainer =
                     ((PendingRequestArgs) itemInfo).getWidgetSourceContainer();
         }
-        CellPos presenterPos = getCellPosMapper().mapModelToPresenter(itemInfo);
         getModelWriter().addItemToDatabase(launcherInfo,
                 itemInfo.container, presenterPos.screenId, presenterPos.cellX, presenterPos.cellY);
 
         hostView.setVisibility(View.VISIBLE);
         mItemInflater.prepareAppWidget(hostView, launcherInfo);
-        mWorkspace.addInScreen(hostView, launcherInfo);
+        if (!enableAddAppWidgetViaConfigActivityV2() || hostView.getParent() == null) {
+            mWorkspace.addInScreen(hostView, launcherInfo);
+        }
         announceForAccessibility(R.string.item_added_to_workspace);
 
         // Show the widget resize frame.
         if (hostView instanceof LauncherAppWidgetHostView) {
             final LauncherAppWidgetHostView launcherHostView = (LauncherAppWidgetHostView) hostView;
-            CellLayout cellLayout = getCellLayout(launcherInfo.container, presenterPos.screenId);
-            if (mStateManager.getState() == NORMAL) {
-                AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
-            } else {
-                mStateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
-                    @Override
-                    public void onStateTransitionComplete(LauncherState finalState) {
-                        if ((mPrevLauncherState == SPRING_LOADED || mPrevLauncherState == EDIT_MODE)
-                                && finalState == NORMAL) {
-                            AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
-                            mStateManager.removeStateListener(this);
-                        }
+            showWidgetResizeFrame(launcherHostView, launcherInfo, presenterPos);
+        }
+    }
+
+    /** Show widget resize frame. */
+    private void showWidgetResizeFrame(
+            LauncherAppWidgetHostView launcherHostView,
+            LauncherAppWidgetInfo launcherInfo,
+            CellPos presenterPos) {
+        CellLayout cellLayout = getCellLayout(launcherInfo.container, presenterPos.screenId);
+        if (mStateManager.getState() == NORMAL) {
+            AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
+        } else {
+            mStateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    if ((mPrevLauncherState == SPRING_LOADED || mPrevLauncherState == EDIT_MODE)
+                            && finalState == NORMAL) {
+                        AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
+                        mStateManager.removeStateListener(this);
                     }
-                });
-            }
+                }
+            });
         }
     }
 
@@ -1579,7 +1623,7 @@
             }
 
             if (FeatureFlags.enableSplitContextually()) {
-                handleSplitAnimationGoingToHome();
+                handleSplitAnimationGoingToHome(LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
             }
             mOverlayManager.hideOverlay(isStarted() && !isForceInvisible());
             handleGestureContract(intent);
@@ -1595,7 +1639,7 @@
     }
 
     /** Handle animating away split placeholder view when user taps on home button */
-    protected void handleSplitAnimationGoingToHome() {
+    protected void handleSplitAnimationGoingToHome(EventEnum splitDismissReason) {
         // Overridden
     }
 
@@ -1759,19 +1803,39 @@
         addAppWidgetImpl(appWidgetId, info, boundWidget, addFlowHandler, 0);
     }
 
+    /**
+     * If FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2 is enabled, we always add widget
+     * host view to workspace, otherwise we only add widget to host view if config activity is
+     * not started.
+     */
     void addAppWidgetImpl(int appWidgetId, ItemInfo info,
             AppWidgetHostView boundWidget, WidgetAddFlowHandler addFlowHandler, int delay) {
-        if (!addFlowHandler.startConfigActivity(this, appWidgetId, info,
-                REQUEST_CREATE_APPWIDGET)) {
-            // If the configuration flow was not started, add the widget
+        final boolean isActivityStarted = addFlowHandler.startConfigActivity(
+                this, appWidgetId, info, REQUEST_CREATE_APPWIDGET);
 
-            // Exit spring loaded mode if necessary after adding the widget
-            Runnable onComplete = MULTI_SELECT_EDIT_MODE.get() ? null
-                    : () -> mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
-            completeAddAppWidget(appWidgetId, info, boundWidget,
-                    addFlowHandler.getProviderInfo(this));
-            mWorkspace.removeExtraEmptyScreenDelayed(delay, false, onComplete);
+        if (!enableAddAppWidgetViaConfigActivityV2() && isActivityStarted) {
+            return;
         }
+
+        // If FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2 is enabled and config activity is
+        // started, we should remove the dropped AppWidgetHostView from drag layer and extract the
+        // Bitmap that shows the preview. Then pass the Bitmap to completeAddAppWidget() to create
+        // a PendingWidgetHostView.
+        Bitmap widgetPreviewBitmap = null;
+        if (isActivityStarted) {
+            DragView dropView = getDragLayer().clearAnimatedView();
+            if (dropView != null && dropView.containsAppWidgetHostView()) {
+                widgetPreviewBitmap = getBitmapFromView(dropView.getContentView());
+            }
+        }
+
+        // Exit spring loaded mode if necessary after adding the widget
+        Runnable onComplete = MULTI_SELECT_EDIT_MODE.get() ? null
+                : () -> mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
+        completeAddAppWidget(appWidgetId, info, boundWidget,
+                addFlowHandler.getProviderInfo(this), addFlowHandler.needsConfigure(),
+                widgetPreviewBitmap);
+        mWorkspace.removeExtraEmptyScreenDelayed(delay, false, onComplete);
     }
 
     public void addPendingItem(PendingAddItemInfo info, int container, int screenId,
@@ -2366,6 +2430,18 @@
         return null;
     }
 
+    /** Convert a {@link View} to {@link Bitmap}. */
+    private static Bitmap getBitmapFromView(@Nullable View view) {
+        if (view == null) {
+            return null;
+        }
+        Bitmap returnedBitmap =
+                Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(returnedBitmap);
+        view.draw(canvas);
+        return returnedBitmap;
+    }
+
     /**
      * Returns the first view matching the operator in the given ViewGroups, or null if none.
      * Forward iteration matters.
@@ -2660,7 +2736,7 @@
     }
 
     /** Call to dismiss the intermediary split selection state. */
-    public void dismissSplitSelection() {
+    public void dismissSplitSelection(StatsLogManager.LauncherEvent splitDismissEvent) {
         // Overridden; move this into ActivityContext if necessary for Taskbar
     }
 
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index 3b62ae1..b0a644b 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -349,6 +349,8 @@
         @JvmField
         val PRIVATE_SPACE_APPS =
             nonRestorableItem("pref_private_space_apps", 0, EncryptionType.MOVE_TO_DEVICE_PROTECTED)
+        @JvmField val ENABLE_TWOLINE_ALLAPPS_TOGGLE =
+                backedUpItem("pref_enable_two_line_toggle", false)
         @JvmField
         val THEMED_ICONS =
             backedUpItem(Themes.KEY_THEMED_ICONS, false, EncryptionType.MOVE_TO_DEVICE_PROTECTED)
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index d2ea7cc..6e66c14 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -17,6 +17,7 @@
 
 import static com.android.app.animation.Interpolators.ACCELERATE_2;
 import static com.android.app.animation.Interpolators.DECELERATE_2;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
 import static com.android.launcher3.testing.shared.TestProtocol.ALL_APPS_STATE_ORDINAL;
@@ -427,11 +428,20 @@
         if (this != NORMAL) {
             StateManager<LauncherState> lsm = launcher.getStateManager();
             LauncherState lastState = lsm.getLastState();
-            lsm.goToState(lastState);
+            lsm.goToState(lastState, forEndCallback(this::onBackPressCompleted));
         }
     }
 
     /**
+     * To be called if back press is completed in a launcher state.
+     *
+     * @param success whether back press animation was successful or canceled.
+     */
+    protected void onBackPressCompleted(boolean success) {
+        // Do nothing. To be overridden by child class.
+    }
+
+    /**
      * Find {@link StateManager} and target {@link LauncherState} to handle back progress in
      * predictive back gesture.
      */
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 0e38007..9867556 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -132,7 +132,7 @@
         launcher.viewCache.setCacheSize(R.layout.folder_page, 2)
         TraceHelper.INSTANCE.endSection()
         launcher.workspace.removeExtraEmptyScreen(/* stripEmptyScreens= */ true)
-        launcher.workspace.pageIndicator.setAreScreensBinding(false, deviceProfile.isTwoPanels)
+        launcher.workspace.pageIndicator.setPauseScroll(/*pause=*/ false, deviceProfile.isTwoPanels)
     }
 
     /**
@@ -299,8 +299,8 @@
     }
 
     override fun bindScreens(orderedScreenIds: LIntArray) {
-        launcher.workspace.pageIndicator.setAreScreensBinding(
-            true,
+        launcher.workspace.pageIndicator.setPauseScroll(
+            /*pause=*/ true,
             launcher.deviceProfile.isTwoPanels
         )
         val firstScreenPosition = 0
diff --git a/src/com/android/launcher3/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java
index d460ba8..6168e41 100644
--- a/src/com/android/launcher3/SessionCommitReceiver.java
+++ b/src/com/android/launcher3/SessionCommitReceiver.java
@@ -30,6 +30,7 @@
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.ItemInstallQueue;
 import com.android.launcher3.pm.InstallSessionHelper;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.Executors;
 
 import java.util.Locale;
@@ -51,13 +52,13 @@
 
     @WorkerThread
     private static void processIntent(Context context, Intent intent) {
-        if (!isEnabled(context)) {
+        UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
+        if (!isEnabled(context, user)) {
             // User has decided to not add icons on homescreen.
             return;
         }
 
         SessionInfo info = intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION);
-        UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
         if (!PackageInstaller.ACTION_SESSION_COMMITTED.equals(intent.getAction())
                 || info == null || user == null) {
             // Invalid intent.
@@ -92,7 +93,17 @@
                 .queueItem(info.getAppPackageName(), user);
     }
 
-    public static boolean isEnabled(Context context) {
+    /**
+     * Returns whether adding Installed App Icons to home screen is allowed or not.
+     * Not allowed when:
+     * - User belongs to {@link com.android.launcher3.util.UserIconInfo.TYPE_PRIVATE} or
+     * - Home Settings preference to add App Icons on Home Screen is set as disabled
+     */
+    public static boolean isEnabled(Context context, UserHandle user) {
+        if (Flags.privateSpaceRestrictItemDrag() && user != null
+                && UserCache.getInstance(context).getUserInfo(user).isPrivate()) {
+            return false;
+        }
         return LauncherPrefs.getPrefs(context).getBoolean(ADD_ICON_PREFERENCE_KEY, true);
     }
 }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 86f31a1..ca34dd1 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -2226,8 +2226,7 @@
     private Runnable getWidgetResizeFrameRunnable(DragOptions options,
             LauncherAppWidgetHostView hostView, CellLayout cellLayout) {
         AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo();
-        if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE
-                && !options.isAccessibleDrag) {
+        if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) {
             return () -> {
                 if (!isPageInTransition()) {
                     AppWidgetResizeFrame.showForWidget(hostView, cellLayout);
@@ -2988,7 +2987,7 @@
     }
 
     public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, final DragView dragView,
-            final Runnable onCompleteRunnable, int animationType, final View finalView,
+            final Runnable onCompleteRunnable, int animationType, @Nullable final View finalView,
             boolean external) {
         int[] finalPos = new int[2];
         float scaleXY[] = new float[2];
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 28c87b6..ca587c1 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -36,6 +36,8 @@
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.Flags;
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.search.SearchAdapterProvider;
 import com.android.launcher3.config.FeatureFlags;
@@ -221,8 +223,11 @@
     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
         switch (viewType) {
             case VIEW_TYPE_ICON:
-                int layout = !FeatureFlags.enableTwolineAllapps() ? R.layout.all_apps_icon
-                                : R.layout.all_apps_icon_twoline;
+                int layout = (FeatureFlags.enableTwolineAllapps() &&
+                        (!Flags.enableTwolineToggle() || (Flags.enableTwolineToggle()
+                                && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(
+                                        mActivityContext.getApplicationContext()))))
+                        ? R.layout.all_apps_icon_twoline : R.layout.all_apps_icon;
                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
                         layout, parent, false);
                 icon.setLongPressTimeoutFactor(1f);
@@ -258,6 +263,7 @@
 
     @Override
     public void onBindViewHolder(ViewHolder holder, int position) {
+        holder.itemView.setVisibility(View.VISIBLE);
         switch (holder.getItemViewType()) {
             case VIEW_TYPE_ICON: {
                 AdapterItem adapterItem = mApps.getAdapterItems().get(position);
diff --git a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
index fcdfaa6..91fcf80 100644
--- a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
+++ b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
@@ -16,7 +16,12 @@
 
 package com.android.launcher3.allapps;
 
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN;
+import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER;
 import static com.android.launcher3.allapps.PrivateProfileManager.STATE_DISABLED;
 import static com.android.launcher3.allapps.PrivateProfileManager.STATE_ENABLED;
 import static com.android.launcher3.allapps.PrivateProfileManager.STATE_TRANSITION;
@@ -24,21 +29,37 @@
 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_TAP;
 
-import android.view.View;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.LayoutTransition;
+import android.animation.ValueAnimator;
+import android.view.ViewGroup;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.RelativeLayout;
+import android.widget.TextView;
 
+import androidx.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.Flags;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.UserProfileManager.UserProfileState;
+import com.android.launcher3.anim.AnimatedPropertySetter;
+import com.android.launcher3.anim.PropertySetter;
+
+import java.util.List;
 
 /**
  * Controller which returns views to be added to Private Space Header based upon
  * {@link UserProfileState}
  */
 public class PrivateSpaceHeaderViewController {
-    private static final int ANIMATION_DURATION = 2000;
+    private static final int EXPAND_SCROLL_DURATION = 2000;
+    private static final int EXPAND_COLLAPSE_DURATION = 800;
+    private static final int SETTINGS_OPACITY_DURATION = 160;
     private final ActivityAllAppsContainerView mAllApps;
     private final PrivateProfileManager mPrivateProfileManager;
 
@@ -50,10 +71,17 @@
 
     /** Add Private Space Header view elements based upon {@link UserProfileState} */
     public void addPrivateSpaceHeaderViewElements(RelativeLayout parent) {
+        // Set the transition duration for the settings and lock button to animate.
+        ViewGroup settingsAndLockGroup = parent.findViewById(R.id.settingsAndLockGroup);
+        LayoutTransition settingsAndLockTransition = settingsAndLockGroup.getLayoutTransition();
+        settingsAndLockTransition.enableTransitionType(LayoutTransition.CHANGING);
+        settingsAndLockTransition.setDuration(EXPAND_COLLAPSE_DURATION);
+
         //Add quietMode image and action for lock/unlock button
-        ImageButton quietModeButton = parent.findViewById(R.id.ps_lock_unlock_button);
-        assert quietModeButton != null;
-        addQuietModeButton(quietModeButton);
+        ViewGroup lockButton =
+                parent.findViewById(R.id.ps_lock_unlock_button);
+        assert lockButton != null;
+        addLockButton(parent, lockButton);
 
         //Trigger lock/unlock action from header.
         addHeaderOnClickListener(parent);
@@ -69,74 +97,181 @@
         addTransitionImage(transitionView);
     }
 
-    private void addQuietModeButton(ImageButton quietModeButton) {
+    /**
+     *  Adds the quietModeButton and attach onClickListener for the header to animate different
+     *  states when clicked.
+     */
+    private void addLockButton(ViewGroup psHeader, ViewGroup lockButton) {
+        TextView lockText = lockButton.findViewById(R.id.lock_text);
         switch (mPrivateProfileManager.getCurrentState()) {
             case STATE_ENABLED -> {
-                quietModeButton.setVisibility(View.VISIBLE);
-                quietModeButton.setImageResource(R.drawable.bg_ps_lock_button);
-                quietModeButton.setOnClickListener(view -> lockAction());
+                lockText.setVisibility(VISIBLE);
+                lockButton.setVisibility(VISIBLE);
+                lockButton.setOnClickListener(view -> lockAction(psHeader));
             }
             case STATE_DISABLED -> {
-                quietModeButton.setVisibility(View.VISIBLE);
-                quietModeButton.setImageResource(R.drawable.bg_ps_unlock_button);
-                quietModeButton.setOnClickListener(view -> unLockAction());
+                lockText.setVisibility(GONE);
+                lockButton.setVisibility(VISIBLE);
+                lockButton.setOnClickListener(view -> unlockAction(psHeader));
             }
-            default -> quietModeButton.setVisibility(View.GONE);
+            default -> lockButton.setVisibility(GONE);
         }
     }
 
     private void addHeaderOnClickListener(RelativeLayout header) {
         if (mPrivateProfileManager.getCurrentState() == STATE_DISABLED) {
-            header.setOnClickListener(view -> unLockAction());
+            header.setOnClickListener(view -> unlockAction(header));
         } else {
             header.setOnClickListener(null);
         }
     }
 
-    private void unLockAction() {
+    private void unlockAction(ViewGroup psHeader) {
         mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP);
-        mPrivateProfileManager.unlockPrivateProfile((this::onPrivateProfileUnlocked));
+        mPrivateProfileManager.unlockPrivateProfile((() -> onPrivateProfileUnlocked(psHeader)));
     }
 
-    private void lockAction() {
+    private void lockAction(ViewGroup psHeader) {
         mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_LOCK_TAP);
-        mPrivateProfileManager.lockPrivateProfile();
+        if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation()) {
+            updatePrivateStateAnimator(false, psHeader);
+        } else {
+            mPrivateProfileManager.lockPrivateProfile();
+        }
     }
 
     private void addPrivateSpaceSettingsButton(ImageButton settingsButton) {
         if (mPrivateProfileManager.getCurrentState() == STATE_ENABLED
                 && mPrivateProfileManager.isPrivateSpaceSettingsAvailable()) {
-            settingsButton.setVisibility(View.VISIBLE);
+            settingsButton.setVisibility(VISIBLE);
+            settingsButton.setAlpha(1f);
             settingsButton.setOnClickListener(
                     view -> {
                         mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP);
                         mPrivateProfileManager.openPrivateSpaceSettings();
                     });
         } else {
-            settingsButton.setVisibility(View.GONE);
+            settingsButton.setVisibility(GONE);
         }
     }
 
     private void addTransitionImage(ImageView transitionImage) {
         if (mPrivateProfileManager.getCurrentState() == STATE_TRANSITION) {
-            transitionImage.setVisibility(View.VISIBLE);
+            transitionImage.setVisibility(VISIBLE);
         } else {
-            transitionImage.setVisibility(View.GONE);
+            transitionImage.setVisibility(GONE);
         }
     }
 
-    private void onPrivateProfileUnlocked() {
+    private void onPrivateProfileUnlocked(ViewGroup header) {
         // If we are on main adapter view, we apply the PS Container expansion animation and
         // then scroll down to load the entire container, making animation visible.
         ActivityAllAppsContainerView<?>.AdapterHolder mainAdapterHolder =
                 (ActivityAllAppsContainerView<?>.AdapterHolder) mAllApps.mAH.get(MAIN);
         if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation()
                 && mAllApps.getActiveRecyclerView() == mainAdapterHolder.mRecyclerView) {
-            mAllApps.getActiveRecyclerView().scrollToBottomWithMotion(ANIMATION_DURATION);
+            // Animate the text and settings icon.
+            updatePrivateStateAnimator(true, header);
+            mAllApps.getActiveRecyclerView().scrollToBottomWithMotion(EXPAND_SCROLL_DURATION);
+        }
+    }
+
+    /** Finds the private space header to scroll to and set the private space icons to GONE. */
+    private void collapse() {
+        AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView();
+        for (int i = allAppsRecyclerView.getChildCount() - 1; i > 0; i--) {
+            int adapterPosition = allAppsRecyclerView.getChildAdapterPosition(
+                    allAppsRecyclerView.getChildAt(i));
+            List<BaseAllAppsAdapter.AdapterItem> allAppsAdapters = allAppsRecyclerView.getApps()
+                    .getAdapterItems();
+            if (adapterPosition < 0 || adapterPosition >= allAppsAdapters.size()) {
+                continue;
+            }
+            // Scroll to the private space header.
+            if (allAppsAdapters.get(adapterPosition).viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) {
+                // Note: SmoothScroller is meant to be used once.
+                RecyclerView.SmoothScroller smoothScroller =
+                        new LinearSmoothScroller(mAllApps.getContext()) {
+                            @Override protected int getVerticalSnapPreference() {
+                                return LinearSmoothScroller.SNAP_TO_END;
+                            }
+                        };
+                smoothScroller.setTargetPosition(adapterPosition);
+                RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager();
+                if (layoutManager != null) {
+                    layoutManager.startSmoothScroll(smoothScroller);
+                }
+                break;
+            }
+            // Make the private space apps gone to "collapse".
+            if (allAppsAdapters.get(adapterPosition).decorationInfo != null) {
+                allAppsRecyclerView.getChildAt(i).setVisibility(GONE);
+            }
         }
     }
 
     PrivateProfileManager getPrivateProfileManager() {
         return mPrivateProfileManager;
     }
+
+    /**
+     * Scrolls up to the private space header and animates the collapsing of the text.
+     */
+    private ValueAnimator animateCollapseAnimation(ViewGroup lockButton) {
+        float from = 1;
+        float to = 0;
+        ValueAnimator collapseAnim = ValueAnimator.ofFloat(from, to);
+        collapseAnim.setDuration(EXPAND_COLLAPSE_DURATION);
+        collapseAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                // scroll up
+                collapse();
+                // Animate the collapsing of the text.
+                lockButton.findViewById(R.id.lock_text).setVisibility(GONE);
+            }
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                mPrivateProfileManager.lockPrivateProfile();
+            }
+        });
+        return collapseAnim;
+    }
+
+    /**
+     * Using PropertySetter{@link PropertySetter}, we can update the view's attributes within an
+     * animation. At the moment, collapsing, setting alpha changes, and animating the text is done
+     * here.
+     */
+    private void updatePrivateStateAnimator(boolean expand, ViewGroup psHeader) {
+        PropertySetter setter = new AnimatedPropertySetter();
+        ViewGroup lockButton = psHeader.findViewById(R.id.ps_lock_unlock_button);
+        ImageButton settingsButton = psHeader.findViewById(R.id.ps_settings_button);
+        updateSettingsGearAlpha(settingsButton, expand, setter);
+        AnimatorSet animatorSet = setter.buildAnim();
+        animatorSet.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                // Animate the collapsing of the text at the same time while updating lock button.
+                lockButton.findViewById(R.id.lock_text).setVisibility(expand ? VISIBLE : GONE);
+            }
+        });
+        // Play the collapsing together of the stateAnimator to avoid being unable to scroll to the
+        // header. Otherwise the smooth scrolling will scroll higher when played with the state
+        // animator.
+        if (!expand) {
+            animatorSet.playTogether(animateCollapseAnimation(lockButton));
+        }
+        animatorSet.setDuration(EXPAND_COLLAPSE_DURATION);
+        animatorSet.start();
+     }
+
+    /** 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(0);
+    }
 }
diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
index 65c270a..ab5ba54 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
+++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
@@ -87,6 +87,14 @@
         appPairBackground = AppPairIconBackground(context, this)
         appPairBackground.setBounds(0, 0, backgroundSize.toInt(), backgroundSize.toInt())
         applyIcons(parentIcon.info.contents)
+
+        // Center the drawable area in the larger icon canvas
+        val lp: LayoutParams = layoutParams as LayoutParams
+        lp.gravity = Gravity.CENTER_HORIZONTAL
+        lp.topMargin = outerPadding.toInt()
+        lp.height = backgroundSize.toInt()
+        lp.width = backgroundSize.toInt()
+        layoutParams = lp
     }
 
     /** Sets up app pair member icons for drawing. */
@@ -125,15 +133,6 @@
 
     override fun dispatchDraw(canvas: Canvas) {
         super.dispatchDraw(canvas)
-
-        // Center the drawable area in the larger icon canvas
-        val lp: LayoutParams = layoutParams as LayoutParams
-        lp.gravity = Gravity.CENTER_HORIZONTAL
-        lp.topMargin = outerPadding.toInt()
-        lp.height = backgroundSize.toInt()
-        lp.width = backgroundSize.toInt()
-        layoutParams = lp
-
         // Draw background
         appPairBackground.draw(canvas)
 
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index f18f900..db693f0 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -42,6 +42,8 @@
 import android.view.accessibility.AccessibilityManager;
 import android.view.animation.Interpolator;
 
+import androidx.annotation.Nullable;
+
 import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DropTargetBar;
@@ -388,7 +390,13 @@
         mDropAnim.start();
     }
 
-    public void clearAnimatedView() {
+    /**
+     * Remove the drop view and end the drag animation.
+     *
+     * @return {@link DragView} that is removed.
+     */
+    @Nullable
+    public DragView clearAnimatedView() {
         if (mDropAnim != null) {
             mDropAnim.cancel();
         }
@@ -396,8 +404,10 @@
         if (mDropView != null) {
             mDragController.onDeferredEndDrag(mDropView);
         }
+        DragView ret = mDropView;
         mDropView = null;
         invalidate();
+        return ret;
     }
 
     public View getAnimatedView() {
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 1f07352..bcee442 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -30,6 +30,7 @@
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.annotation.TargetApi;
+import android.appwidget.AppWidgetHostView;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -563,6 +564,11 @@
         return mContentViewParent;
     }
 
+    /** Return true if {@link mContent} is a {@link AppWidgetHostView}. */
+    public boolean containsAppWidgetHostView() {
+        return mContent instanceof AppWidgetHostView;
+    }
+
     private static class SpringFloatValue {
 
         private static final FloatPropertyCompat<SpringFloatValue> VALUE =
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 871643e..9001a0c 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -119,7 +119,7 @@
             final Runnable onCompleteRunnable) {
         return reverse
                 ? new FolderPreviewItemAnim(this, mFirstPageParams.get(0), 0, 2, -1, -1,
-                        FINAL_ITEM_ANIMATION_DURATION, onCompleteRunnable)
+                FINAL_ITEM_ANIMATION_DURATION, onCompleteRunnable)
                 : new FolderPreviewItemAnim(this, mFirstPageParams.get(0), -1, -1, 0, 2,
                         INITIAL_ITEM_ANIMATION_DURATION, onCompleteRunnable);
     }
@@ -219,9 +219,9 @@
     /**
      * Draws each preview item.
      *
-     * @param offset The offset needed to draw the preview items.
+     * @param offset         The offset needed to draw the preview items.
      * @param shouldClipPath Iff true, clip path using {@param clipPath}.
-     * @param clipPath The clip path of the folder icon.
+     * @param clipPath       The clip path of the folder icon.
      */
     private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params, PointF offset,
             boolean shouldClipPath, Path clipPath) {
@@ -362,13 +362,13 @@
 
     /**
      * Handles the case where items in the preview are either:
-     *  - Moving into the preview
-     *  - Moving into a new position
-     *  - Moving out of the preview
+     * - Moving into the preview
+     * - Moving into a new position
+     * - Moving out of the preview
      *
      * @param oldItems The list of items in the old preview.
      * @param newItems The list of items in the new preview.
-     * @param dropped The item that was dropped onto the FolderIcon.
+     * @param dropped  The item that was dropped onto the FolderIcon.
      */
     public void onDrop(List<WorkspaceItemInfo> oldItems, List<WorkspaceItemInfo> newItems,
             WorkspaceItemInfo dropped) {
@@ -433,9 +433,8 @@
     @VisibleForTesting
     public void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) {
         if (item.hasPromiseIconUi() || (item.runtimeStatusFlags
-                    & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
+                & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
             PreloadIconDrawable drawable = newPendingIcon(mContext, item);
-            drawable.setLevel(item.getProgressLevel());
             p.drawable = drawable;
         } else {
             p.drawable = item.newIcon(mContext,
@@ -443,7 +442,6 @@
         }
         p.drawable.setBounds(0, 0, mIconSize, mIconSize);
         p.item = item;
-
         // Set the callback to FolderIcon as it is responsible to drawing the icon. The
         // callback will be released when the folder is opened.
         p.drawable.setCallback(mIcon);
diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index 3e77c78..9fffcc1 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -173,6 +173,8 @@
         mIconScaleMultiplier.updateValue(info.getProgressLevel() == 0 ? 0 : 1);
 
         setLevel(info.getProgressLevel());
+        // Set a disabled icon color if the app is suspended or if the app is pending download
+        setIsDisabled(info.isDisabled() || info.isPendingDownload());
     }
 
     @Override
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index d8fa90a..ee66a60 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -54,7 +54,6 @@
 import com.android.launcher3.icons.ComponentWithLabel.ComponentCachingLogic;
 import com.android.launcher3.icons.cache.BaseIconCache;
 import com.android.launcher3.icons.cache.CachingLogic;
-import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.IconRequestInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -63,6 +62,7 @@
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.InstantAppResolver;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
@@ -100,7 +100,7 @@
     private final UserCache mUserManager;
     private final InstantAppResolver mInstantAppResolver;
     private final IconProvider mIconProvider;
-    private final HandlerRunnable mCancelledRunnable;
+    private final CancellableTask mCancelledTask;
 
     private final SparseArray<BitmapInfo> mWidgetCategoryBitmapInfos;
 
@@ -119,9 +119,8 @@
         mIconProvider = iconProvider;
         mWidgetCategoryBitmapInfos = new SparseArray<>();
 
-        mCancelledRunnable = new HandlerRunnable(
-                mWorkerHandler, () -> null, MAIN_EXECUTOR, c -> { });
-        mCancelledRunnable.cancel();
+        mCancelledTask = new CancellableTask(() -> null, MAIN_EXECUTOR, c -> { });
+        mCancelledTask.cancel();
     }
 
     @Override
@@ -174,7 +173,7 @@
      *
      * @return a request ID that can be used to cancel the request.
      */
-    public HandlerRunnable updateIconInBackground(final ItemInfoUpdateReceiver caller,
+    public CancellableTask updateIconInBackground(final ItemInfoUpdateReceiver caller,
             final ItemInfoWithIcon info) {
         Preconditions.assertUIThread();
         Supplier<ItemInfoWithIcon> task;
@@ -191,7 +190,7 @@
         } else {
             Log.i(TAG, "Icon update not supported for "
                     + info == null ? "null" : info.getClass().getName());
-            return mCancelledRunnable;
+            return mCancelledTask;
         }
 
         if (mPendingIconRequestCount <= 0) {
@@ -199,7 +198,7 @@
         }
         mPendingIconRequestCount++;
 
-        HandlerRunnable<ItemInfoWithIcon> request = new HandlerRunnable<>(mWorkerHandler,
+        CancellableTask<ItemInfoWithIcon> request = new CancellableTask<>(
                 task, MAIN_EXECUTOR, caller::reapplyItemInfo, this::onIconRequestEnd);
         Utilities.postAsyncCallback(mWorkerHandler, request);
         return request;
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 2aabb7c..5cb1540 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -199,6 +199,11 @@
         @UiEvent(doc = "User tapped on app info system shortcut.")
         LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP(515),
 
+        /**
+         * @deprecated Use {@link #LAUNCHER_APP_ICON_MENU_SPLIT_LEFT_TOP} or
+         *             {@link #LAUNCHER_APP_ICON_MENU_SPLIT_RIGHT_BOTTOM}
+         */
+        @Deprecated
         @UiEvent(doc = "User tapped on split screen icon on a task menu.")
         LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP(518),
 
@@ -725,6 +730,27 @@
         @UiEvent(doc = "User tapped on private space uninstall system shortcut.")
         LAUNCHER_PRIVATE_SPACE_UNINSTALL_SYSTEM_SHORTCUT_TAP(1608),
 
+        @UiEvent(doc = "User initiated split selection")
+        LAUNCHER_SPLIT_SELECTION_INITIATED(1618),
+
+        @UiEvent(doc = "User finished a split selection session")
+        LAUNCHER_SPLIT_SELECTION_COMPLETE(1619),
+
+        @UiEvent(doc = "User selected both apps for split screen")
+        LAUNCHER_SPLIT_SELECTED_SECOND_APP(1609),
+
+        @UiEvent(doc = "User exited split selection by going home via swipe, button, or state "
+                + "transition")
+        LAUNCHER_SPLIT_SELECTION_EXIT_HOME(1610),
+
+        @UiEvent(doc = "User exited split selection by tapping cancel in split instructions view")
+        LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON(1611),
+
+        @UiEvent(doc = "User exited split selection when another activity/app came to foreground"
+                + " after first app had been selected OR if user long-pressed on home. Default exit"
+                + " metric.")
+        LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED(1612),
+
         // ADD MORE
         ;
 
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index a41b663..4d06c2e 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -240,8 +240,9 @@
                                 isTargetValid = context.getSystemService(LauncherApps.class)
                                         .isActivityEnabled(cn, mUser);
                             }
-                            if (!isTargetValid && si.hasStatusFlag(
-                                    FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)) {
+                            if (!isTargetValid && (si.hasStatusFlag(
+                                    FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)
+                                    || si.isArchived())) {
                                 if (updateWorkspaceItemIntent(context, si, packageName)) {
                                     infoUpdated = true;
                                 } else if (si.hasPromiseIconUi()) {
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index e46c502..352c363 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -139,7 +139,8 @@
      */
     private int mProgressLevel = 100;
 
-    protected ItemInfoWithIcon() { }
+    protected ItemInfoWithIcon() {
+    }
 
     protected ItemInfoWithIcon(ItemInfoWithIcon info) {
         super(info);
@@ -155,7 +156,20 @@
     }
 
     /**
-     * Returns true if the app corresponding to the item is archived. */
+     * @return {@code true} if the app is pending download (0 progress) or if the app is archived
+     * and its install session is active
+     */
+    public boolean isPendingDownload() {
+        if (isArchived()) {
+            return this.getProgressLevel() == 0
+                    && (this.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0;
+        }
+        return getProgressLevel() == 0;
+    }
+
+    /**
+     * Returns true if the app corresponding to the item is archived.
+     */
     public boolean isArchived() {
         if (!Utilities.enableSupportForArchiving()) {
             return false;
@@ -179,7 +193,7 @@
     public boolean isAppStartable() {
         return ((runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) == 0)
                 && (((runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0)
-                    || mProgressLevel == 100 || isArchived());
+                || mProgressLevel == 100 || isArchived());
     }
 
     /**
@@ -242,7 +256,7 @@
 
         return targetPackage != null
                 ? ApiWrapper.getAppMarketActivityIntent(
-                        context, targetPackage, Process.myUserHandle())
+                context, targetPackage, Process.myUserHandle())
                 : null;
     }
 
diff --git a/src/com/android/launcher3/pageindicators/PageIndicator.java b/src/com/android/launcher3/pageindicators/PageIndicator.java
index 30156c8..0640bf3 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicator.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicator.java
@@ -27,10 +27,12 @@
     void setMarkersCount(int numMarkers);
 
     /**
-     * Sets flag to indicate when the screens are in the process of binding so that we don't animate
-     * during that period.
+     * Sets a flag indicating whether to pause scroll.
+     * <p>Should be set to {@code true} while the screen is binding or new data is being applied,
+     * and to {@code false} once done. This prevents animation conflicts due to scrolling during
+     * those periods.</p>
      */
-    default void setAreScreensBinding(boolean areScreensBinding, boolean isTwoPanels) {
+    default void setPauseScroll(boolean pause, boolean isTwoPanels) {
         // No-op by default
     }
 
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index df369c6..77effca 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -130,7 +130,7 @@
      */
     private float mCurrentPosition;
     private float mFinalPosition;
-    private boolean mAreScreensBinding;
+    private boolean mIsScrollPaused;
     private boolean mIsTwoPanels;
     private ObjectAnimator mAnimator;
     private @Nullable ObjectAnimator mAlphaAnimator;
@@ -172,7 +172,7 @@
         }
 
         // Skip scroll update during binding. We will update it when binding completes.
-        if (mAreScreensBinding) {
+        if (mIsScrollPaused) {
             return;
         }
 
@@ -367,7 +367,7 @@
         mNumPages = numMarkers;
 
         // If the last page gets removed we want to go to the previous page.
-        if (mNumPages == mActivePage) {
+        if (mNumPages > 0 && mNumPages == mActivePage) {
             mActivePage--;
             CURRENT_POSITION.set(this, (float) mActivePage);
         }
@@ -376,15 +376,15 @@
     }
 
     @Override
-    public void setAreScreensBinding(boolean areScreensBinding, boolean isTwoPanels) {
+    public void setPauseScroll(boolean pause, boolean isTwoPanels) {
         mIsTwoPanels = isTwoPanels;
 
         // Reapply correct current position which was skipped during setScroll.
-        if (mAreScreensBinding && !areScreensBinding) {
+        if (mIsScrollPaused && !pause) {
             CURRENT_POSITION.set(this, (float) mActivePage);
         }
 
-        mAreScreensBinding = areScreensBinding;
+        mIsScrollPaused = pause;
     }
 
     @Override
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index 605ef16..2ec994e 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -211,7 +211,8 @@
      */
     @WorkerThread
     void tryQueuePromiseAppIcon(@Nullable final PackageInstaller.SessionInfo sessionInfo) {
-        if (SessionCommitReceiver.isEnabled(mAppContext)
+        if (sessionInfo != null
+                && SessionCommitReceiver.isEnabled(mAppContext, getUserHandle(sessionInfo))
                 && verifySessionInfo(sessionInfo)
                 && !promiseIconAddedForId(sessionInfo.getSessionId())) {
             FileLog.d(LOG, "Adding package name to install queue: "
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index f1d837c..fb463f7 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -222,6 +222,7 @@
     }
 
     /** Returns the recommended widgets mapped by their category. */
+    @NonNull
     public Map<WidgetRecommendationCategory, List<WidgetItem>> getCategorizedRecommendedWidgets() {
         Map<ComponentKey, WidgetItem> allWidgetItems = mAllWidgets.stream()
                 .filter(entry -> entry instanceof WidgetsListContentEntry)
@@ -232,7 +233,8 @@
                         Function.identity()
                 ));
         return mRecommendedWidgets.stream()
-                .filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo)
+                .filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo
+                        && ((PendingAddWidgetInfo) itemInfo).recommendationCategory != null)
                 .collect(Collectors.groupingBy(
                         it -> ((PendingAddWidgetInfo) it).recommendationCategory,
                         Collectors.collectingAndThen(
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 71957e1..43027da 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -23,7 +23,7 @@
 import com.android.launcher3.BubbleTextView
 import com.android.launcher3.allapps.BaseAllAppsAdapter
 import com.android.launcher3.config.FeatureFlags
-import com.android.launcher3.util.ExecutorRunnable
+import com.android.launcher3.util.CancellableTask
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR
 import com.android.launcher3.views.ActivityContext
@@ -39,7 +39,7 @@
 class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
 
     var hasWorkProfile = false
-    var executorRunnable: ExecutorRunnable<List<ViewHolder>>? = null
+    private var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
 
     /**
      * Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
@@ -62,14 +62,14 @@
                 override fun getLayoutManager(): RecyclerView.LayoutManager? = null
             }
 
-        executorRunnable?.cancel(/* interrupt= */ false)
-        executorRunnable =
-            ExecutorRunnable.createAndExecute(
-                VIEW_PREINFLATION_EXECUTOR,
+        mCancellableTask?.cancel()
+        var task: CancellableTask<List<ViewHolder>>? = null
+        task =
+            CancellableTask(
                 {
                     val list: ArrayList<ViewHolder> = ArrayList()
                     for (i in 0 until preInflateCount) {
-                        if (Thread.interrupted()) {
+                        if (task?.canceled == true) {
                             break
                         }
                         list.add(
@@ -85,6 +85,8 @@
                     }
                 }
             )
+        mCancellableTask = task
+        VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask)
     }
 
     /**
@@ -93,7 +95,7 @@
      */
     override fun clear() {
         super.clear()
-        executorRunnable?.cancel(/* interrupt= */ true)
+        mCancellableTask?.cancel()
     }
 
     /**
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index e6e7133..51bc339 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -159,6 +159,13 @@
     /**
      * @see #goToState(STATE_TYPE, boolean, AnimatorListener)
      */
+    public void goToState(STATE_TYPE state, AnimatorListener listener) {
+        goToState(state, shouldAnimateStateChange(), listener);
+    }
+
+    /**
+     * @see #goToState(STATE_TYPE, boolean, AnimatorListener)
+     */
     public void goToState(STATE_TYPE state, boolean animated) {
         goToState(state, animated, 0, null);
     }
@@ -168,7 +175,7 @@
      *
      * @param animated false if the state should change immediately without any animation,
      *                true otherwise
-     * @paras onCompleteRunnable any action to perform at the end of the transition, of null.
+     * @param listener any action to perform at the end of the transition, or null.
      */
     public void goToState(STATE_TYPE state, boolean animated, AnimatorListener listener) {
         goToState(state, animated, 0, listener);
diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index 8c43f75..0ff10c2 100644
--- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java
+++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_CLOSE_TAP_OUTSIDE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_LONGPRESS;
 
 import android.graphics.PointF;
@@ -207,7 +208,7 @@
                 mLauncher.getStatsLogManager().logger().log(LAUNCHER_WORKSPACE_LONGPRESS);
                 mLauncher.showDefaultOptions(mTouchDownPoint.x, mTouchDownPoint.y);
                 if (FeatureFlags.enableSplitContextually() && mLauncher.isSplitSelectionActive()) {
-                    mLauncher.dismissSplitSelection();
+                    mLauncher.dismissSplitSelection(LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
                 }
             } else {
                 cancelLongPress();
diff --git a/src/com/android/launcher3/util/CancellableTask.kt b/src/com/android/launcher3/util/CancellableTask.kt
new file mode 100644
index 0000000..49ef020
--- /dev/null
+++ b/src/com/android/launcher3/util/CancellableTask.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import java.util.function.Supplier
+
+/** A [Runnable] that can be posted to a [Executor] that can be cancelled. */
+class CancellableTask<T>
+@JvmOverloads
+constructor(
+    private val task: Supplier<T>,
+    // Executor where consumer needs to be executed on. Typically UI executor.
+    private val callbackExecutor: Executor,
+    // Consumer that needs to be accepted upon completion of the task. Typically work that needs to
+    // be done in UI thread after task completes.
+    private val callback: Consumer<T>,
+    // Callback to be executed on callbackExecutor at the end irrespective of the task being
+    // completed or cancelled
+    private val endRunnable: Runnable = Runnable {}
+) : Runnable {
+
+    // flag to cancel the callback
+    var canceled = false
+        private set
+
+    private var ended = false
+
+    override fun run() {
+        if (canceled) return
+        val value = task.get()
+        callbackExecutor.execute {
+            if (!canceled) {
+                callback.accept(value)
+            }
+            onEnd()
+        }
+    }
+
+    /**
+     * Cancel the [CancellableTask] if not scheduled. If [CancellableTask] has started execution at
+     * this time, we will try to cancel the callback if not executed yet.
+     */
+    fun cancel() {
+        canceled = true
+        callbackExecutor.execute(this::onEnd)
+    }
+
+    private fun onEnd() {
+        if (!ended) {
+            ended = true
+            endRunnable.run()
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/ExecutorRunnable.kt b/src/com/android/launcher3/util/ExecutorRunnable.kt
deleted file mode 100644
index 49cf592..0000000
--- a/src/com/android/launcher3/util/ExecutorRunnable.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util
-
-import java.util.concurrent.Executor
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Future
-import java.util.function.Consumer
-import java.util.function.Supplier
-
-/** A [Runnable] that can be posted to a [Executor] that can be cancelled. */
-class ExecutorRunnable<T>
-private constructor(
-    private val task: Supplier<T>,
-    // Executor where consumer needs to be executed on. Typically UI executor.
-    private val callbackExecutor: Executor,
-    // Consumer that needs to be accepted upon completion of the task. Typically work that needs to
-    // be done in UI thread after task completes.
-    private val callback: Consumer<T>
-) : Runnable {
-
-    // future of this runnable that will used for cancellation.
-    lateinit var future: Future<*>
-
-    // flag to cancel the callback
-    var canceled = false
-
-    override fun run() {
-        val value: T = task.get()
-        callbackExecutor.execute {
-            if (!canceled) {
-                callback.accept(value)
-            }
-        }
-    }
-
-    /**
-     * Cancel the [ExecutorRunnable] if not scheduled. If [ExecutorRunnable] has started execution
-     * at this time, we will try to cancel the callback if not executed yet.
-     */
-    fun cancel(interrupt: Boolean) {
-        future.cancel(interrupt)
-        canceled = true
-    }
-
-    companion object {
-        /**
-         * Create [ExecutorRunnable] and execute it on task [Executor]. It will also save the
-         * [Future] into this [ExecutorRunnable] to be used for cancellation.
-         */
-        fun <T> createAndExecute(
-            // Executor where task will be executed, typically an Executor running on background
-            // thread.
-            taskExecutor: ExecutorService,
-            task: Supplier<T>,
-            callbackExecutor: Executor,
-            callback: Consumer<T>
-        ): ExecutorRunnable<T> {
-            val executorRunnable = ExecutorRunnable(task, callbackExecutor, callback)
-            executorRunnable.future = taskExecutor.submit(executorRunnable)
-            return executorRunnable
-        }
-    }
-}
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index abc5ef8..5d2d3f4 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -104,7 +104,7 @@
     protected final Rect mHitRect = new Rect();
 
     @ViewDebug.ExportedProperty(category = "launcher")
-    private final RectF mSystemGestureRegion = new RectF();
+    protected final RectF mSystemGestureRegion = new RectF();
     private int mTouchDispatchState = 0;
 
     protected final T mActivity;
@@ -164,7 +164,7 @@
         return findActiveController(ev);
     }
 
-    private boolean isEventInLauncher(MotionEvent ev) {
+    protected boolean isEventWithinSystemGestureRegion(MotionEvent ev) {
         final float x = ev.getX();
         final float y = ev.getY();
 
@@ -175,7 +175,8 @@
     private TouchController findControllerToHandleTouch(MotionEvent ev) {
         AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
         if (topView != null
-                && (isEventInLauncher(ev) || topView.canInterceptEventsInSystemGestureRegion())
+                && (isEventWithinSystemGestureRegion(ev)
+                || topView.canInterceptEventsInSystemGestureRegion())
                 && topView.onControllerInterceptTouchEvent(ev)) {
             return topView;
         }
@@ -287,7 +288,7 @@
                 mTouchDispatchState |= TOUCH_DISPATCHING_FROM_VIEW
                         | TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS;
 
-                if (isEventInLauncher(ev)) {
+                if (isEventWithinSystemGestureRegion(ev)) {
                     mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION;
                 } else {
                     mTouchDispatchState |= TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION;
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 145ad80..54ce973 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.widget;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
 import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
 import static com.android.launcher3.LauncherPrefs.WIDGETS_EDUCATION_TIP_SEEN;
 
@@ -62,8 +63,10 @@
 
     protected final Rect mInsets = new Rect();
 
-    @Px protected int mContentHorizontalMargin;
-    @Px protected int mWidgetCellHorizontalPadding;
+    @Px
+    protected int mContentHorizontalMargin;
+    @Px
+    protected int mWidgetCellHorizontalPadding;
 
     protected int mNavBarScrimHeight;
     private final Paint mNavBarScrimPaint;
@@ -196,7 +199,7 @@
         DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
         int widthUsed;
         if (deviceProfile.isTablet) {
-            widthUsed = Math.max(2 * getTabletMargin(deviceProfile),
+            widthUsed = Math.max(2 * getTabletHorizontalMargin(deviceProfile),
                     2 * (mInsets.left + mInsets.right));
         } else if (mInsets.bottom > 0) {
             widthUsed = mInsets.left + mInsets.right;
@@ -212,7 +215,11 @@
                 MeasureSpec.getSize(heightMeasureSpec));
     }
 
-    private int getTabletMargin(DeviceProfile deviceProfile) {
+    private int getTabletHorizontalMargin(DeviceProfile deviceProfile) {
+        // All bottom-sheets showing widgets will be full-width across all devices.
+        if (enableCategorizedWidgetSuggestions()) {
+            return 0;
+        }
         if (deviceProfile.isLandscape && !deviceProfile.isTwoPanels) {
             return getResources().getDimensionPixelSize(
                     R.dimen.widget_picker_landscape_tablet_left_right_margin);
diff --git a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
index 99485be..aab78bd 100644
--- a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
+++ b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
@@ -41,9 +41,9 @@
 import com.android.launcher3.icons.BitmapRenderer;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.icons.ShadowGenerator;
-import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.widget.util.WidgetSizes;
@@ -74,12 +74,12 @@
      * @return a request id which can be used to cancel the request.
      */
     @NonNull
-    public HandlerRunnable loadPreview(
+    public CancellableTask loadPreview(
             @NonNull WidgetItem item,
             @NonNull Size previewSize,
             @NonNull Consumer<Bitmap> callback) {
         Handler handler = Executors.UI_HELPER_EXECUTOR.getHandler();
-        HandlerRunnable<Bitmap> request = new HandlerRunnable<>(handler,
+        CancellableTask<Bitmap> request = new CancellableTask<>(
                 () -> generatePreview(item, previewSize.getWidth(), previewSize.getHeight()),
                 MAIN_EXECUTOR,
                 callback);
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index 25979c2..adf85c7 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -16,13 +16,20 @@
 
 package com.android.launcher3.widget;
 
+import static android.graphics.Paint.ANTI_ALIAS_FLAG;
+import static android.graphics.Paint.DITHER_FLAG;
+import static android.graphics.Paint.FILTER_BITMAP_FLAG;
+
 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
+import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
+import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.Paint;
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
@@ -85,8 +92,12 @@
     private boolean mDrawableSizeChanged;
 
     private final TextPaint mPaint;
+
+    private final Paint mPreviewPaint;
     private Layout mSetupTextLayout;
 
+    @Nullable private Bitmap mPreviewBitmap;
+
     public PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info,
             @Nullable LauncherAppWidgetProviderInfo appWidget) {
         this(context, info, appWidget,
@@ -116,6 +127,15 @@
         mDrawableSizeChanged = true;
     }
 
+    /** Set {@link Bitmap} of widget preview. */
+    public void setPreviewBitmap(@Nullable Bitmap previewBitmap) {
+        if (this.mPreviewBitmap == previewBitmap) {
+            return;
+        }
+        this.mPreviewBitmap = previewBitmap;
+        invalidate();
+    }
+
     private PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info,
             LauncherAppWidgetProviderInfo appwidget, CharSequence label) {
         super(new ContextThemeWrapper(context, R.style.WidgetContainerTheme));
@@ -130,12 +150,17 @@
         mPaint.setColor(Themes.getAttrColor(getContext(), android.R.attr.textColorPrimary));
         mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
                 mLauncher.getDeviceProfile().iconTextSizePx, getResources().getDisplayMetrics()));
-
+        mPreviewPaint = new Paint(ANTI_ALIAS_FLAG | DITHER_FLAG | FILTER_BITMAP_FLAG);
         setWillNotDraw(false);
         setBackgroundResource(R.drawable.pending_widget_bg);
     }
 
     @Override
+    public AppWidgetProviderInfo getAppWidgetInfo() {
+        return mAppwidget;
+    }
+
+    @Override
     public void updateAppWidget(RemoteViews remoteViews) {
         checkIfRestored();
     }
@@ -393,6 +418,11 @@
 
     @Override
     protected void onDraw(Canvas canvas) {
+        if (mPreviewBitmap != null
+                && (mInfo.restoreStatus & LauncherAppWidgetInfo.FLAG_UI_NOT_READY) != 0) {
+            canvas.drawBitmap(mPreviewBitmap, 0, 0, mPreviewPaint);
+            return;
+        }
         if (mCenterDrawable == null) {
             // Nothing to draw
             return;
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index c75f9d1..7345511 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -51,8 +51,8 @@
 import com.android.launcher3.R;
 import com.android.launcher3.icons.FastBitmapDrawable;
 import com.android.launcher3.icons.RoundDrawableWrapper;
-import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.views.ActivityContext;
 
 import java.util.function.Consumer;
@@ -90,7 +90,7 @@
 
     private final DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
 
-    protected HandlerRunnable mActiveRequest;
+    protected CancellableTask mActiveRequest;
     private boolean mAnimatePreview = true;
 
     protected final ActivityContext mActivity;
@@ -367,6 +367,16 @@
         }
     }
 
+    /**
+     * Shows or hides the long description displayed below each widget.
+     *
+     * @param show a flag that shows the long description of the widget if {@code true}, hides it if
+     *             {@code false}.
+     */
+    public void showDescription(boolean show) {
+        mWidgetDescription.setVisibility(show ? VISIBLE : GONE);
+    }
+
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
         super.onTouchEvent(ev);
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index c347939..ceb0072 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.widget;
 
+import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY;
 
 import android.content.Context;
@@ -187,7 +188,13 @@
                 mWidgetCellHorizontalPadding)
                 .forEach(row -> {
                     TableRow tableRow = new TableRow(getContext());
-                    tableRow.setGravity(Gravity.TOP);
+                    if (enableCategorizedWidgetSuggestions()) {
+                        // Vertically center align items, so that even if they don't fill bounds,
+                        // they can look organized when placed together in a row.
+                        tableRow.setGravity(Gravity.CENTER_VERTICAL);
+                    } else {
+                        tableRow.setGravity(Gravity.TOP);
+                    }
                     row.forEach(widgetItem -> {
                         WidgetCell widget = addItemCell(tableRow);
                         widget.applyFromCellItem(widgetItem);
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
new file mode 100644
index 0000000..426a3ae
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker;
+
+import static com.android.launcher3.widget.util.WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.PagedView;
+import com.android.launcher3.R;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.pageindicators.PageIndicatorDots;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * A {@link PagedView} that displays widget recommendations in categories with dots as paged
+ * indicators.
+ */
+public final class WidgetRecommendationsView extends PagedView<PageIndicatorDots> {
+    private @Px float mAvailableHeight = Float.MAX_VALUE;
+
+    private static final int MAX_CATEGORIES = 3;
+    private TextView mRecommendationPageTitle;
+    private final List<String> mCategoryTitles = new ArrayList<>();
+
+    @Nullable
+    private OnLongClickListener mWidgetCellOnLongClickListener;
+    @Nullable
+    private OnClickListener mWidgetCellOnClickListener;
+
+    public WidgetRecommendationsView(Context context) {
+        this(context, /* attrs= */ null);
+    }
+
+    public WidgetRecommendationsView(Context context, AttributeSet attrs) {
+        this(context, attrs, /* defStyleAttr= */ 0);
+    }
+
+    public WidgetRecommendationsView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public void initParentViews(View parent) {
+        super.initParentViews(parent);
+        mRecommendationPageTitle = parent.findViewById(R.id.recommendations_page_title);
+    }
+
+    /** Sets a {@link android.view.View.OnLongClickListener} for all widget cells in this table. */
+    public void setWidgetCellLongClickListener(OnLongClickListener onLongClickListener) {
+        mWidgetCellOnLongClickListener = onLongClickListener;
+    }
+
+    /** Sets a {@link android.view.View.OnClickListener} for all widget cells in this table. */
+    public void setWidgetCellOnClickListener(OnClickListener widgetCellOnClickListener) {
+        mWidgetCellOnClickListener = widgetCellOnClickListener;
+    }
+
+    /**
+     * Displays all the provided recommendations in a single table if they fit.
+     *
+     * @param recommendedWidgets list of widgets to be displayed in recommendation section.
+     * @param deviceProfile      the current {@link DeviceProfile}
+     * @param availableHeight    height in px that can be used to display the recommendations;
+     *                           recommendations that don't fit in this height won't be shown
+     * @param availableWidth     width in px that the recommendations should display in
+     * @param cellPadding        padding in px that should be applied to each widget in the
+     *                           recommendations
+     * @return {@code false} if no recommendations could fit in the available space.
+     */
+    public boolean setRecommendations(
+            List<WidgetItem> recommendedWidgets, DeviceProfile deviceProfile,
+            final @Px float availableHeight, final @Px int availableWidth,
+            final @Px int cellPadding) {
+        this.mAvailableHeight = availableHeight;
+        removeAllViews();
+
+        maybeDisplayInTable(recommendedWidgets, deviceProfile, availableWidth, cellPadding);
+        updateTitleAndIndicator();
+        return getChildCount() > 0;
+    }
+
+    /**
+     * Displays the recommendations grouped by categories as pages.
+     * <p>In case of a single category, no title is displayed for it.</p>
+     *
+     * @param recommendations a map of widget items per recommendation category
+     * @param deviceProfile   the current {@link DeviceProfile}
+     * @param availableHeight height in px that can be used to display the recommendations;
+     *                        recommendations that don't fit in this height won't be shown
+     * @param availableWidth  width in px that the recommendations should display in
+     * @param cellPadding     padding in px that should be applied to each widget in the
+     *                        recommendations
+     * @return {@code false} if no recommendations could fit in the available space.
+     */
+    public boolean setRecommendations(
+            Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations,
+            DeviceProfile deviceProfile,
+            final @Px float availableHeight, final @Px int availableWidth,
+            final @Px int cellPadding) {
+        this.mAvailableHeight = availableHeight;
+        Context context = getContext();
+        mPageIndicator.setPauseScroll(true, deviceProfile.isTwoPanels);
+        removeAllViews();
+
+        int displayedCategories = 0;
+
+        // Render top MAX_CATEGORIES in separate tables. Each table becomes a page.
+        for (Map.Entry<WidgetRecommendationCategory, List<WidgetItem>> entry :
+                new TreeMap<>(recommendations).entrySet()) {
+            // If none of the recommendations for the category could fit in the mAvailableHeight, we
+            // don't want to add that category; and we look for the next one.
+            if (maybeDisplayInTable(entry.getValue(), deviceProfile, availableWidth, cellPadding)) {
+                mCategoryTitles.add(
+                        context.getResources().getString(entry.getKey().categoryTitleRes));
+                displayedCategories++;
+            }
+
+            if (displayedCategories == MAX_CATEGORIES) {
+                break;
+            }
+        }
+
+        updateTitleAndIndicator();
+        mPageIndicator.setPauseScroll(false, deviceProfile.isTwoPanels);
+        return getChildCount() > 0;
+    }
+
+    /** Displays the page title and paging indicator if there are multiple pages. */
+    private void updateTitleAndIndicator() {
+        boolean showPaginatedView = getPageCount() > 1;
+        int titleAndIndicatorVisibility = showPaginatedView ? View.VISIBLE : View.GONE;
+        mRecommendationPageTitle.setVisibility(titleAndIndicatorVisibility);
+        mPageIndicator.setVisibility(titleAndIndicatorVisibility);
+        if (showPaginatedView) {
+            mPageIndicator.setActiveMarker(0);
+            setCurrentPage(0);
+            mRecommendationPageTitle.setText(mCategoryTitles.get(0));
+        }
+    }
+
+    @Override
+    protected void notifyPageSwitchListener(int prevPage) {
+        if (getPageCount() > 1) {
+            // Since the title is outside the paging scroll, we update the title on page switch.
+            mRecommendationPageTitle.setText(mCategoryTitles.get(getNextPage()));
+            super.notifyPageSwitchListener(prevPage);
+            requestLayout();
+        }
+    }
+
+    @Override
+    protected boolean canScroll(float absVScroll, float absHScroll) {
+        // Allow only horizontal scroll.
+        return (absHScroll > absVScroll) && super.canScroll(absVScroll, absHScroll);
+    }
+
+    @Override
+    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+        super.onScrollChanged(l, t, oldl, oldt);
+        mPageIndicator.setScroll(l, mMaxScroll);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        boolean hasMultiplePages = getChildCount() > 0;
+
+        if (hasMultiplePages) {
+            int finalWidth = MeasureSpec.getSize(widthMeasureSpec);
+            int desiredHeight = 0;
+
+            for (int i = 0; i < getChildCount(); i++) {
+                View child = getChildAt(i);
+                measureChild(child, widthMeasureSpec, heightMeasureSpec);
+                if (mAvailableHeight == Float.MAX_VALUE) {
+                    // When we are not limited by height, use currentPage's height. This is the case
+                    // when the paged layout is placed in a scrollable container. We cannot use
+                    // height
+                    // of tallest child in such case, as it will display a scrollbar even for
+                    // smaller
+                    // pages that don't have more content.
+                    if (i == mCurrentPage) {
+                        int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
+                        desiredHeight = Math.max(parentHeight, child.getMeasuredHeight());
+                    }
+                } else {
+                    // Use height of tallest child when we are limited to a certain height.
+                    desiredHeight = Math.max(desiredHeight, child.getMeasuredHeight());
+                }
+            }
+
+            int finalHeight = resolveSizeAndState(desiredHeight, heightMeasureSpec, 0);
+            setMeasuredDimension(finalWidth, finalHeight);
+        } else {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+
+    /**
+     * Groups the provided recommendations into rows and displays them in a table if at least one
+     * fits.
+     * <p>Returns false if none of the recommendations could fit.</p>
+     */
+    private boolean maybeDisplayInTable(List<WidgetItem> recommendedWidgets,
+            DeviceProfile deviceProfile,
+            final @Px int availableWidth, final @Px int cellPadding) {
+        Context context = getContext();
+        LayoutInflater inflater = LayoutInflater.from(context);
+
+        List<ArrayList<WidgetItem>> rows = groupWidgetItemsUsingRowPxWithoutReordering(
+                recommendedWidgets,
+                context,
+                deviceProfile,
+                availableWidth,
+                cellPadding);
+
+        WidgetsRecommendationTableLayout recommendationsTable =
+                (WidgetsRecommendationTableLayout) inflater.inflate(
+                        R.layout.widget_recommendations_table,
+                        /* root=*/ this,
+                        /* attachToRoot=*/ false);
+        recommendationsTable.setWidgetCellOnClickListener(mWidgetCellOnClickListener);
+        recommendationsTable.setWidgetCellLongClickListener(mWidgetCellOnLongClickListener);
+
+        boolean displayedAtLeastOne = recommendationsTable.setRecommendedWidgets(rows,
+                deviceProfile, mAvailableHeight);
+        if (displayedAtLeastOne) {
+            addView(recommendationsTable);
+        }
+
+        return displayedAtLeastOne;
+    }
+
+    /** Returns location of a widget cell for displaying the "touch and hold" education tip. */
+    public View getViewForEducationTip() {
+        if (getChildCount() > 0) {
+            // first page (a table layout) -> first item (a widget cell).
+            return ((ViewGroup) getChildAt(0)).getChildAt(0);
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 4e704fd..f5742af 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -17,6 +17,7 @@
 
 import static android.view.View.MeasureSpec.makeMeasureSpec;
 
+import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
 import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.LauncherPrefs.WIDGETS_EDUCATION_DIALOG_SEEN;
@@ -45,6 +46,7 @@
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.Button;
+import android.widget.LinearLayout;
 import android.widget.TextView;
 import android.window.BackEvent;
 
@@ -65,7 +67,6 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.model.UserManagerState;
-import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.views.ArrowTipView;
 import com.android.launcher3.views.RecyclerViewFastScroller;
@@ -76,7 +77,6 @@
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.picker.search.SearchModeListener;
 import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
-import com.android.launcher3.widget.util.WidgetsTableUtils;
 import com.android.launcher3.workprofile.PersonalWorkPagedView;
 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
 
@@ -162,12 +162,13 @@
     @Nullable PersonalWorkPagedView mViewPager;
     private boolean mIsInSearchMode;
     private boolean mIsNoWidgetsViewNeeded;
-    @Px private int mMaxSpanPerRow;
+    @Px protected int mMaxSpanPerRow;
     protected DeviceProfile mDeviceProfile;
 
     protected TextView mNoWidgetsView;
     protected StickyHeaderLayout mSearchScrollView;
-    protected WidgetsRecommendationTableLayout mRecommendedWidgetsTable;
+    protected WidgetRecommendationsView mWidgetRecommendationsView;
+    protected LinearLayout mWidgetRecommendationsContainer;
     protected View mTabBar;
     protected View mSearchBarContainer;
     protected WidgetsSearchBar mSearchBar;
@@ -221,9 +222,14 @@
 
         setupViews();
 
-        mRecommendedWidgetsTable = mSearchScrollView.findViewById(R.id.recommended_widget_table);
-        mRecommendedWidgetsTable.setWidgetCellLongClickListener(this);
-        mRecommendedWidgetsTable.setWidgetCellOnClickListener(this);
+        mWidgetRecommendationsContainer = mSearchScrollView.findViewById(
+                R.id.widget_recommendations_container);
+        mWidgetRecommendationsView = mSearchScrollView.findViewById(
+                R.id.widget_recommendations_view);
+        mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
+        mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
+        mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
+
         mHeaderTitle = mSearchScrollView.findViewById(R.id.title);
 
         onRecommendedWidgetsBound();
@@ -551,7 +557,7 @@
     protected void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
         mIsInSearchMode = isInSearchMode;
         if (isInSearchMode) {
-            mRecommendedWidgetsTable.setVisibility(GONE);
+            mWidgetRecommendationsContainer.setVisibility(GONE);
             if (mHasWorkProfile) {
                 mViewPager.setVisibility(GONE);
                 mTabBar.setVisibility(GONE);
@@ -580,40 +586,46 @@
         if (mIsInSearchMode) {
             return;
         }
-        List<WidgetItem> recommendedWidgets =
-                mActivityContext.getPopupDataProvider().getRecommendedWidgets();
-        mHasRecommendedWidgets = recommendedWidgets.size() > 0;
-        if (mHasRecommendedWidgets) {
-            float noWidgetsViewHeight = 0;
-            if (mIsNoWidgetsViewNeeded) {
-                // Make sure recommended section leaves enough space for noWidgetsView.
-                Rect noWidgetsViewTextBounds = new Rect();
-                mNoWidgetsView.getPaint()
-                        .getTextBounds(mNoWidgetsView.getText().toString(), /* start= */ 0,
-                                mNoWidgetsView.getText().length(), noWidgetsViewTextBounds);
-                noWidgetsViewHeight = noWidgetsViewTextBounds.height();
-            }
-            if (!isTwoPane()) {
-                doMeasure(
-                        makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx,
-                                MeasureSpec.EXACTLY),
-                        makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx,
-                                MeasureSpec.EXACTLY));
-            }
-            float maxTableHeight = getMaxTableHeight(noWidgetsViewHeight);
 
-            List<ArrayList<WidgetItem>> recommendedWidgetsInTable =
-                    WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering(
-                            recommendedWidgets,
-                            mActivityContext,
-                            mActivityContext.getDeviceProfile(),
-                            mMaxSpanPerRow,
-                            mWidgetCellHorizontalPadding);
-            mRecommendedWidgetsTable.setRecommendedWidgets(
-                    recommendedWidgetsInTable, maxTableHeight);
+        if (enableCategorizedWidgetSuggestions()) {
+            mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations(
+                    mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets(),
+                    mDeviceProfile,
+                    /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
+                    /* availableWidth= */ mMaxSpanPerRow,
+                    /* cellPadding= */ mWidgetCellHorizontalPadding
+            );
         } else {
-            mRecommendedWidgetsTable.setVisibility(GONE);
+            mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations(
+                    mActivityContext.getPopupDataProvider().getRecommendedWidgets(),
+                    mDeviceProfile,
+                    /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
+                    /* availableWidth= */ mMaxSpanPerRow,
+                    /* cellPadding= */ mWidgetCellHorizontalPadding
+            );
         }
+        mWidgetRecommendationsContainer.setVisibility(mHasRecommendedWidgets ? VISIBLE : GONE);
+    }
+
+    @Px
+    private float getMaxAvailableHeightForRecommendations() {
+        float noWidgetsViewHeight = 0;
+        if (mIsNoWidgetsViewNeeded) {
+            // Make sure recommended section leaves enough space for noWidgetsView.
+            Rect noWidgetsViewTextBounds = new Rect();
+            mNoWidgetsView.getPaint()
+                    .getTextBounds(mNoWidgetsView.getText().toString(), /* start= */ 0,
+                            mNoWidgetsView.getText().length(), noWidgetsViewTextBounds);
+            noWidgetsViewHeight = noWidgetsViewTextBounds.height();
+        }
+        if (!isTwoPane()) {
+            doMeasure(
+                    makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx,
+                            MeasureSpec.EXACTLY),
+                    makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx,
+                            MeasureSpec.EXACTLY));
+        }
+        return getMaxTableHeight(noWidgetsViewHeight);
     }
 
     /** b/209579563: "Widgets" header should be focused first. */
@@ -622,7 +634,8 @@
         return mHeaderTitle;
     }
 
-    protected float getMaxTableHeight(float noWidgetsViewHeight) {
+    @Px
+    protected float getMaxTableHeight(@Px float noWidgetsViewHeight) {
         return (mContent.getMeasuredHeight()
                 - mTabsHeight - getHeaderViewHeight()
                 - noWidgetsViewHeight)
@@ -699,7 +712,9 @@
 
     private static int getWidgetSheetId(BaseActivity activity) {
         boolean isTwoPane = (activity.getDeviceProfile().isTablet
-                && activity.getDeviceProfile().isLandscape
+                // Enables two pane picker for tablets in all orientations when the
+                // enableCategorizedWidgetSuggestions flag is on.
+                && (activity.getDeviceProfile().isLandscape || enableCategorizedWidgetSuggestions())
                 && !activity.getDeviceProfile().isTwoPanels)
                 // Enables two pane picker for unfolded foldables if the flag is on.
                 || (activity.getDeviceProfile().isTwoPanels && enableUnfoldedTwoPanePicker());
@@ -813,28 +828,40 @@
     public void onDeviceProfileChanged(DeviceProfile dp) {
         super.onDeviceProfileChanged(dp);
 
-        if (mDeviceProfile.isLandscape != dp.isLandscape && dp.isTablet && !dp.isTwoPanels) {
-            handleClose(false);
-            show(BaseActivity.fromContext(getContext()), false);
-        } else if (!isTwoPane()) {
-            reset();
-            resetExpandedHeaders();
-        }
-
-        // When folding/unfolding the foldables, we need to switch between the regular widget picker
-        // and the two pane picker, so we rebuild the picker with the correct layout.
-        if (mDeviceProfile.isTwoPanels != dp.isTwoPanels && enableUnfoldedTwoPanePicker()) {
+        if (shouldRecreateLayout(/*oldDp=*/ mDeviceProfile, /*newDp=*/ dp)) {
             SparseArray<Parcelable> widgetsState = new SparseArray<>();
             saveHierarchyState(widgetsState);
             handleClose(false);
             WidgetsFullSheet sheet = show(BaseActivity.fromContext(getContext()), false);
             sheet.restoreHierarchyState(widgetsState);
             sheet.restorePreviousAdapterHolderType(getCurrentAdapterHolderType());
+        } else if (!isTwoPane()) {
+            reset();
+            resetExpandedHeaders();
         }
 
         mDeviceProfile = dp;
     }
 
+    /**
+     * Indicates if layout should be re-created on device profile change - so that a different
+     * layout can be displayed.
+     */
+    private static boolean shouldRecreateLayout(DeviceProfile oldDp, DeviceProfile newDp) {
+        // When folding/unfolding the foldables, we need to switch between the regular widget picker
+        // and the two pane picker, so we rebuild the picker with the correct layout.
+        boolean isFoldUnFold =
+                oldDp.isTwoPanels != newDp.isTwoPanels && enableUnfoldedTwoPanePicker();
+        // In tablets, on orientation change we switch between single and two pane picker unless the
+        // categorized suggestions flag was on. With the categorized suggestions feature, we use a
+        // two pane picker across all orientations.
+        boolean useDifferentLayoutOnOrientationChange =
+                (!enableCategorizedWidgetSuggestions() && (newDp.isTablet && !newDp.isTwoPanels
+                        && oldDp.isLandscape != newDp.isLandscape));
+
+        return isFoldUnFold || useDifferentLayoutOnOrientationChange;
+    }
+
     @Override
     public void onBackInvoked() {
         if (mIsInSearchMode) {
@@ -855,9 +882,8 @@
     }
 
     @Nullable private View getViewToShowEducationTip() {
-        if (mRecommendedWidgetsTable.getVisibility() == VISIBLE
-                && mRecommendedWidgetsTable.getChildCount() > 0) {
-            return ((ViewGroup) mRecommendedWidgetsTable.getChildAt(0)).getChildAt(0);
+        if (mWidgetRecommendationsContainer.getVisibility() == VISIBLE) {
+            return mWidgetRecommendationsView.getViewForEducationTip();
         }
 
         AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
index 3383299..d373a3b 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -35,9 +35,9 @@
 import com.android.launcher3.R;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.icons.PlaceHolderIconDrawable;
-import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
 
@@ -57,7 +57,7 @@
      * widget apps aren't collapsable.
     */
     private final boolean mIsCollapsable;
-    @Nullable private HandlerRunnable mIconLoadRequest;
+    @Nullable private CancellableTask mIconLoadRequest;
     @Nullable private Drawable mIconDrawable;
     @Nullable private WidgetsListDrawableState mListDrawableState;
     private ImageView mAppIcon;
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
index c7d2aa3..f10ab48 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.widget.picker;
 
+import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
+
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.util.Log;
@@ -147,7 +149,13 @@
                 tableRow = (TableRow) table.getChildAt(i);
             } else {
                 tableRow = new TableRow(table.getContext());
-                tableRow.setGravity(Gravity.TOP);
+                if (enableCategorizedWidgetSuggestions()) {
+                    // Vertically center align items, so that even if they don't fill bounds, they
+                    // can look organized when placed together in a row.
+                    tableRow.setGravity(Gravity.CENTER_VERTICAL);
+                } else {
+                    tableRow.setGravity(Gravity.TOP);
+                }
                 table.addView(tableRow);
             }
             if (tableRow.getChildCount() > widgetItems.size()) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
index 06cc65e..47750a5 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.widget.picker;
 
+import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
 
 import android.content.Context;
@@ -31,7 +32,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.widget.WidgetCell;
@@ -61,7 +61,7 @@
         super(context, attrs);
         // There are 1 row for title, 1 row for dimension and 2 rows for description.
         mWidgetsRecommendationTableVerticalPadding = 2 * getResources()
-                .getDimensionPixelSize(R.dimen.recommended_widgets_table_vertical_padding);
+                .getDimensionPixelSize(R.dimen.widget_recommendations_table_vertical_padding);
         mWidgetCellVerticalPadding = 2 * getResources()
                 .getDimensionPixelSize(R.dimen.widget_cell_vertical_padding);
         mWidgetCellTextViewsHeight = 4 * getResources().getDimension(R.dimen.widget_cell_font_size);
@@ -84,17 +84,22 @@
      * <p>If the content can't fit {@code recommendationTableMaxHeight}, this view will remove a
      * last row from the {@code recommendedWidgets} until it fits or only one row left. If the only
      * row still doesn't fit, we scale down the preview image.
+     *
+     * <p>Returns {@code false} if none of the widgets could fit</p>
      */
-    public void setRecommendedWidgets(List<ArrayList<WidgetItem>> recommendedWidgets,
+    public boolean setRecommendedWidgets(List<ArrayList<WidgetItem>> recommendedWidgets,
+            DeviceProfile deviceProfile,
             float recommendationTableMaxHeight) {
         mRecommendationTableMaxHeight = recommendationTableMaxHeight;
         RecommendationTableData data = fitRecommendedWidgetsToTableSpace(/* previewScale= */ 1f,
+                deviceProfile,
                 recommendedWidgets);
         bindData(data);
+        return !data.mRecommendationTable.isEmpty();
     }
 
     private void bindData(RecommendationTableData data) {
-        if (data.mRecommendationTable.size() == 0) {
+        if (data.mRecommendationTable.isEmpty()) {
             setVisibility(GONE);
             return;
         }
@@ -104,12 +109,20 @@
         for (int i = 0; i < data.mRecommendationTable.size(); i++) {
             List<WidgetItem> widgetItems = data.mRecommendationTable.get(i);
             TableRow tableRow = new TableRow(getContext());
-            tableRow.setGravity(Gravity.TOP);
-
+            if (enableCategorizedWidgetSuggestions()) {
+                // Vertically center align items, so that even if they don't fill bounds, they can
+                // look organized when placed together in a row.
+                tableRow.setGravity(Gravity.CENTER_VERTICAL);
+            } else {
+                tableRow.setGravity(Gravity.TOP);
+            }
             for (WidgetItem widgetItem : widgetItems) {
                 WidgetCell widgetCell = addItemCell(tableRow);
                 widgetCell.applyFromCellItem(widgetItem, data.mPreviewScale);
                 widgetCell.showBadge();
+                if (enableCategorizedWidgetSuggestions()) {
+                    widgetCell.showDescription(false);
+                }
             }
             addView(tableRow);
         }
@@ -132,6 +145,7 @@
 
     private RecommendationTableData fitRecommendedWidgetsToTableSpace(
             float previewScale,
+            DeviceProfile deviceProfile,
             List<ArrayList<WidgetItem>> recommendedWidgetsInTable) {
         if (previewScale < MAX_DOWN_SCALE_RATIO) {
             Log.w(TAG, "Hide recommended widgets. Can't down scale previews to " + previewScale);
@@ -139,7 +153,6 @@
         }
         // A naive estimation of the widgets recommendation table height without inflation.
         float totalHeight = mWidgetsRecommendationTableVerticalPadding;
-        DeviceProfile deviceProfile = Launcher.getLauncher(getContext()).getDeviceProfile();
         for (int i = 0; i < recommendedWidgetsInTable.size(); i++) {
             List<WidgetItem> widgetItems = recommendedWidgetsInTable.get(i);
             float rowHeight = 0;
@@ -163,12 +176,14 @@
             // num of row by 1 to see if it fits.
             return fitRecommendedWidgetsToTableSpace(
                     previewScale,
+                    deviceProfile,
                     recommendedWidgetsInTable.subList(/* fromIndex= */0,
                             /* toIndex= */recommendedWidgetsInTable.size() - 1));
         }
 
         float nextPreviewScale = previewScale * DOWN_SCALE_RATIO;
-        return fitRecommendedWidgetsToTableSpace(nextPreviewScale, recommendedWidgetsInTable);
+        return fitRecommendedWidgetsToTableSpace(nextPreviewScale, deviceProfile,
+                recommendedWidgetsInTable);
     }
 
     /** Data class for the widgets recommendation table and widgets preview scaling. */
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index 744c45b..165b2fe 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.widget.picker;
 
+import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
 import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
 
 import android.content.Context;
@@ -109,9 +110,15 @@
 
         mWidgetsListTableViewHolderBinder =
                 new WidgetsListTableViewHolderBinder(mActivityContext, layoutInflater, this, this);
-        mRecommendedWidgetsTable = mContent.findViewById(R.id.recommended_widget_table);
-        mRecommendedWidgetsTable.setWidgetCellLongClickListener(this);
-        mRecommendedWidgetsTable.setWidgetCellOnClickListener(this);
+
+        mWidgetRecommendationsContainer = mContent.findViewById(
+                R.id.widget_recommendations_container);
+        mWidgetRecommendationsView = mContent.findViewById(
+                R.id.widget_recommendations_view);
+        mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
+        mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
+        mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
+
         mHeaderTitle = mContent.findViewById(R.id.title);
         mRightPane = mContent.findViewById(R.id.right_pane);
         mRightPane.setOutlineProvider(mViewOutlineProviderRightPane);
@@ -213,7 +220,7 @@
             mSuggestedWidgetsHeader.setExpanded(true);
             resetExpandedHeaders();
             mRightPane.removeAllViews();
-            mRightPane.addView(mRecommendedWidgetsTable);
+            mRightPane.addView(mWidgetRecommendationsContainer);
             mRightPaneScrollView.setScrollY(0);
             mRightPane.setAccessibilityPaneTitle(suggestionsRightPaneTitle);
             mSuggestedWidgetsPackageUserKey = PackageUserKey.fromPackageItemInfo(packageItemInfo);
@@ -224,7 +231,8 @@
     }
 
     @Override
-    protected float getMaxTableHeight(float noWidgetsViewHeight) {
+    @Px
+    protected float getMaxTableHeight(@Px float noWidgetsViewHeight) {
         return Float.MAX_VALUE;
     }
 
@@ -308,15 +316,25 @@
                 if (mSuggestedWidgetsHeader != null) {
                     mSuggestedWidgetsHeader.setExpanded(false);
                 }
+
+                WidgetsListContentEntry contentEntryToBind;
+                if (enableCategorizedWidgetSuggestions()) {
+                    // Setting max span size enables row to understand how to fit more than one item
+                    // in a row.
+                    contentEntryToBind = contentEntry.withMaxSpanSize(mMaxSpanPerRow);
+                } else {
+                    contentEntryToBind = contentEntry;
+                }
+
                 WidgetsRowViewHolder widgetsRowViewHolder =
                         mWidgetsListTableViewHolderBinder.newViewHolder(mRightPane);
                 mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder,
-                        contentEntry,
+                        contentEntryToBind,
                         ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST,
                         Collections.EMPTY_LIST);
                 widgetsRowViewHolder.mDataCallback = data -> {
                     mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder,
-                            contentEntry,
+                            contentEntryToBind,
                             ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST,
                             Collections.singletonList(data));
                 };
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
index 6ed6879..59da1be 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
@@ -50,7 +50,7 @@
 	folderContentPaddingLeftRight: 0.0px (0.0dp)
 	folderTopPadding: 48.0px (24.0dp)
 	folderFooterHeight: 112.0px (56.0dp)
-	bottomSheetTopPadding: 704.0px (352.0dp)
+	bottomSheetTopPadding: 530.0px (265.0dp)
 	bottomSheetOpenDuration: 500
 	bottomSheetCloseDuration: 500
 	bottomSheetWorkspaceScale: 0.97
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
index a51669b..ad1f095 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
@@ -50,7 +50,7 @@
 	folderContentPaddingLeftRight: 0.0px (0.0dp)
 	folderTopPadding: 48.0px (24.0dp)
 	folderFooterHeight: 112.0px (56.0dp)
-	bottomSheetTopPadding: 704.0px (352.0dp)
+	bottomSheetTopPadding: 530.0px (265.0dp)
 	bottomSheetOpenDuration: 500
 	bottomSheetCloseDuration: 500
 	bottomSheetWorkspaceScale: 0.97
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ExecutorRunnableTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/CancellableTaskTest.kt
similarity index 92%
rename from tests/multivalentTests/src/com/android/launcher3/util/ExecutorRunnableTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/util/CancellableTaskTest.kt
index 6634577..eb58096 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/ExecutorRunnableTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/CancellableTaskTest.kt
@@ -29,12 +29,12 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-/** Unit test for [ExecutorRunnable] */
+/** Unit test for [CancellableTask] */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class ExecutorRunnableTest {
+class CancellableTaskTest {
 
-    private lateinit var underTest: ExecutorRunnable<Int>
+    private lateinit var underTest: CancellableTask<Int>
 
     private val lock = ReentrantLock()
     private var result: Int = -1
@@ -51,8 +51,7 @@
 
     private fun submitJob() {
         underTest =
-            ExecutorRunnable.createAndExecute(
-                Executors.UI_HELPER_EXECUTOR,
+            CancellableTask(
                 {
                     isTaskExecuted = true
                     1
@@ -63,6 +62,7 @@
                     result = it + 1
                 }
             )
+        Executors.UI_HELPER_EXECUTOR.execute(underTest)
     }
 
     @Test
@@ -82,7 +82,7 @@
         Executors.UI_HELPER_EXECUTOR.submit { lock.lock() }
         submitJob()
 
-        underTest.cancel(false)
+        underTest.cancel()
 
         lock.unlock() // unblock task on UI_HELPER_EXECUTOR
         awaitAllExecutorCompleted()
@@ -101,7 +101,7 @@
         awaitExecutorCompleted(Executors.UI_HELPER_EXECUTOR)
         assertTrue("task should be executed.", isTaskExecuted)
 
-        underTest.cancel(false)
+        underTest.cancel()
 
         lock.unlock() // unblock callback on VIEW_PREINFLATION_EXECUTOR
         awaitExecutorCompleted(Executors.VIEW_PREINFLATION_EXECUTOR)
@@ -113,7 +113,7 @@
     fun run_and_cancelAfterCompletion_executeAll() {
         awaitAllExecutorCompleted()
 
-        underTest.cancel(false)
+        underTest.cancel()
 
         assertTrue("task should be executed", isTaskExecuted)
         assertTrue("callback should be executed", isCallbackExecuted)
diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java
index 92fff49..490cb47 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java
+++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
@@ -33,8 +34,11 @@
 import android.graphics.drawable.Drawable;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.widget.ImageButton;
 import android.widget.ImageView;
+import android.widget.LinearLayout;
 import android.widget.RelativeLayout;
+import android.widget.TextView;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -59,7 +63,6 @@
     private static final int PS_TRANSITION_IMAGE_COUNT = 1;
 
     private Context mContext;
-    private LayoutInflater mLayoutInflater;
     private PrivateSpaceHeaderViewController mPsHeaderViewController;
     private RelativeLayout mPsHeaderLayout;
     @Mock
@@ -71,16 +74,15 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mContext = new ActivityContextWrapper(getApplicationContext());
-        mLayoutInflater = LayoutInflater.from(getApplicationContext());
         mPsHeaderViewController = new PrivateSpaceHeaderViewController(mAllApps,
                 mPrivateProfileManager);
-        mPsHeaderLayout = (RelativeLayout) mLayoutInflater.inflate(R.layout.private_space_header,
-                null);
+        mPsHeaderLayout = (RelativeLayout) LayoutInflater.from(mContext).inflate(
+                R.layout.private_space_header, null);
     }
 
     @Test
     public void privateProfileDisabled_psHeaderContainsLockedView() throws Exception {
-        Bitmap unlockButton = getBitmap(mContext.getDrawable(R.drawable.bg_ps_unlock_button));
+        Bitmap unlockButton = getBitmap(mContext.getDrawable(R.drawable.ic_lock));
         when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_DISABLED);
 
         mPsHeaderViewController.addPrivateSpaceHeaderViewElements(mPsHeaderLayout);
@@ -93,11 +95,15 @@
             if (view.getId() == R.id.ps_container_header) {
                 totalContainerHeaderView += 1;
                 assertEquals(View.VISIBLE, view.getVisibility());
-            } else if (view.getId() == R.id.ps_lock_unlock_button
-                    && view instanceof ImageView imageView) {
+            } else if (view.getId() == R.id.settingsAndLockGroup) {
+                ImageView lockIcon = view.findViewById(R.id.lock_icon);
+                assertTrue(getBitmap(lockIcon.getDrawable()).sameAs(unlockButton));
+                assertEquals(View.VISIBLE, lockIcon.getVisibility());
+
+                // Verify textView shouldn't be showing when disabled.
+                TextView lockText = view.findViewById(R.id.lock_text);
+                assertEquals(View.GONE, lockText.getVisibility());
                 totalLockUnlockButtonView += 1;
-                assertEquals(View.VISIBLE, view.getVisibility());
-                getBitmap(imageView.getDrawable()).sameAs(unlockButton);
             } else {
                 assertEquals(View.GONE, view.getVisibility());
             }
@@ -108,8 +114,8 @@
 
     @Test
     public void privateProfileEnabled_psHeaderContainsUnlockedView() throws Exception {
-        Bitmap lockImage = getBitmap(mContext.getDrawable(R.drawable.bg_ps_lock_button));
-        Bitmap settingsImage = getBitmap(mContext.getDrawable(R.drawable.bg_ps_settings_button));
+        Bitmap lockImage = getBitmap(mContext.getDrawable(R.drawable.ic_lock));
+        Bitmap settingsImage = getBitmap(mContext.getDrawable(R.drawable.ic_ps_settings));
         when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED);
         when(mPrivateProfileManager.isPrivateSpaceSettingsAvailable()).thenReturn(true);
 
@@ -124,16 +130,20 @@
             if (view.getId() == R.id.ps_container_header) {
                 totalContainerHeaderView += 1;
                 assertEquals(View.VISIBLE, view.getVisibility());
-            } else if (view.getId() == R.id.ps_lock_unlock_button
-                    && view instanceof ImageView imageView) {
-                totalLockUnlockButtonView += 1;
-                assertEquals(View.VISIBLE, view.getVisibility());
-                getBitmap(imageView.getDrawable()).sameAs(lockImage);
-            } else if (view.getId() == R.id.ps_settings_button
-                    && view instanceof ImageView imageView) {
+            } else if (view.getId() == R.id.settingsAndLockGroup) {
+                // Look for settings button.
+                ImageButton settingsButton = view.findViewById(R.id.ps_settings_button);
+                assertEquals(View.VISIBLE, settingsButton.getVisibility());
                 totalSettingsImageView += 1;
-                assertEquals(View.VISIBLE, view.getVisibility());
-                getBitmap(imageView.getDrawable()).sameAs(settingsImage);
+                assertTrue(getBitmap(settingsButton.getDrawable()).sameAs(settingsImage));
+
+                // Look for lock_icon and lock_text.
+                ImageView lockIcon = view.findViewById(R.id.lock_icon);
+                assertTrue(getBitmap(lockIcon.getDrawable()).sameAs(lockImage));
+                assertEquals(View.VISIBLE, lockIcon.getVisibility());
+                TextView lockText = view.findViewById(R.id.lock_text);
+                assertEquals(View.VISIBLE, lockText.getVisibility());
+                totalLockUnlockButtonView += 1;
             } else {
                 assertEquals(View.GONE, view.getVisibility());
             }
@@ -146,7 +156,7 @@
     @Test
     public void privateProfileEnabledAndNoSettingsIntent_psHeaderContainsUnlockedView()
             throws Exception {
-        Bitmap lockImage = getBitmap(mContext.getDrawable(R.drawable.bg_ps_lock_button));
+        Bitmap lockImage = getBitmap(mContext.getDrawable(R.drawable.ic_lock));
         when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED);
         when(mPrivateProfileManager.isPrivateSpaceSettingsAvailable()).thenReturn(false);
 
@@ -161,11 +171,18 @@
             if (view.getId() == R.id.ps_container_header) {
                 totalContainerHeaderView += 1;
                 assertEquals(View.VISIBLE, view.getVisibility());
-            } else if (view.getId() == R.id.ps_lock_unlock_button
-                    && view instanceof ImageView imageView) {
+            } else if (view.getId() == R.id.settingsAndLockGroup) {
+                // Ensure there is no settings button.
+                ImageButton settingsImage = view.findViewById(R.id.ps_settings_button);
+                assertEquals(View.GONE, settingsImage.getVisibility());
+
+                // Check lock icon and lock text is there.
+                ImageView lockIcon = view.findViewById(R.id.lock_icon);
+                assertTrue(getBitmap(lockIcon.getDrawable()).sameAs(lockImage));
+                assertEquals(View.VISIBLE, lockIcon.getVisibility());
+                TextView lockText = view.findViewById(R.id.lock_text);
+                assertEquals(View.VISIBLE, lockText.getVisibility());
                 totalLockUnlockButtonView += 1;
-                assertEquals(View.VISIBLE, view.getVisibility());
-                getBitmap(imageView.getDrawable()).sameAs(lockImage);
             } else {
                 assertEquals(View.GONE, view.getVisibility());
             }
@@ -194,7 +211,10 @@
                     && view instanceof ImageView imageView) {
                 totalLockUnlockButtonView += 1;
                 assertEquals(View.VISIBLE, view.getVisibility());
-                getBitmap(imageView.getDrawable()).sameAs(transitionImage);
+                assertTrue(getBitmap(imageView.getDrawable()).sameAs(transitionImage));
+            } else if (view.getId() == R.id.settingsAndLockGroup) {
+                LinearLayout lockUnlockButton = view.findViewById(R.id.ps_lock_unlock_button);
+                assertEquals(View.GONE, lockUnlockButton.getVisibility());
             } else {
                 assertEquals(View.GONE, view.getVisibility());
             }
diff --git a/tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java b/tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java
index ef5af0f..10e9f8a 100644
--- a/tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java
+++ b/tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java
@@ -24,7 +24,6 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 
-import org.junit.Before;
 import org.junit.Test;
 
 /**
@@ -33,12 +32,6 @@
  */
 public class TaplAllAppsIconsWorkingTest extends AbstractLauncherUiTest {
 
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        initialize(this);
-    }
-
     /**
      * Makes sure we can launch an icon from All apps
      */
diff --git a/tests/src/com/android/launcher3/allapps/TaplKeyboardFocusTest.java b/tests/src/com/android/launcher3/allapps/TaplKeyboardFocusTest.java
index db574a1..0360470 100644
--- a/tests/src/com/android/launcher3/allapps/TaplKeyboardFocusTest.java
+++ b/tests/src/com/android/launcher3/allapps/TaplKeyboardFocusTest.java
@@ -32,7 +32,6 @@
 import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.launcher3.views.ActivityContext;
 
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -40,12 +39,6 @@
 @RunWith(AndroidJUnit4.class)
 public class TaplKeyboardFocusTest extends AbstractLauncherUiTest {
 
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        initialize(this);
-    }
-
     @Test
     public void testAllAppsFocusApp() {
         final HomeAllApps allApps = mLauncher.goHome().switchToAllApps();
diff --git a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
index 848ae82..4d73f7a 100644
--- a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
+++ b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
@@ -36,7 +36,6 @@
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.util.rule.ScreenRecordRule;
 
-import org.junit.Before;
 import org.junit.Test;
 
 /**
@@ -49,15 +48,6 @@
             "android.permission.READ_DEVICE_CONFIG";
 
     /**
-     * Calls static method initialize
-     */
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        initialize(this);
-    }
-
-    /**
      * Make sure we can go home after pressing the context menu on an Icon on the AllApps.
      */
     @Test
diff --git a/tests/src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java b/tests/src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java
index ec24397..c69d1b9 100644
--- a/tests/src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java
+++ b/tests/src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java
@@ -31,7 +31,6 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 
-import org.junit.Before;
 import org.junit.Test;
 
 /**
@@ -41,12 +40,6 @@
  */
 public class TaplAppIconMenuTest extends AbstractLauncherUiTest {
 
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        initialize(this);
-    }
-
     private boolean isOptionsPopupVisible(Launcher launcher) {
         final ArrowPopup<?> popup = launcher.getOptionsPopup();
         return popup != null && popup.isShown();
diff --git a/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java b/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java
index 8bdcd03..aca9765 100644
--- a/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java
+++ b/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java
@@ -72,7 +72,7 @@
     @Before
     public void setup() throws Throwable {
         mWorkspaceBuilder = new TestWorkspaceBuilder(mTargetContext);
-        AbstractLauncherUiTest.initialize(this);
+        super.setUp();
     }
 
     @After
diff --git a/tests/src/com/android/launcher3/dragging/TaplDragTest.java b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
index 9942602..d57461f 100644
--- a/tests/src/com/android/launcher3/dragging/TaplDragTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
@@ -16,8 +16,8 @@
 package com.android.launcher3.dragging;
 
 import static com.android.launcher3.util.TestConstants.AppNames.GMAIL_APP_NAME;
-import static com.android.launcher3.util.TestConstants.AppNames.PHOTOS_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.MAPS_APP_NAME;
+import static com.android.launcher3.util.TestConstants.AppNames.PHOTOS_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.STORE_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME;
 import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
@@ -44,7 +44,6 @@
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 import com.android.launcher3.util.rule.TestStabilityRule;
 
-import org.junit.Before;
 import org.junit.Test;
 
 /**
@@ -59,12 +58,6 @@
  */
 public class TaplDragTest extends AbstractLauncherUiTest {
 
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        initialize(this);
-    }
-
     /**
      * Adds two icons to the Workspace and combines them into a folder, then makes sure the icons
      * are no longer in the Workspace then adds a third one to test adding an icon to an existing
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 20ba04b..e21309f 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -41,7 +41,6 @@
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.TestStabilityRule;
 
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -54,12 +53,6 @@
  */
 public class TaplUninstallRemoveTest extends AbstractLauncherUiTest {
 
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        initialize(this);
-    }
-
     /**
      * Deletes app both built-in and user-installed from the Workspace and makes sure it's no longer
      * in the Workspace.
@@ -121,6 +114,7 @@
     @Test
     @PortraitLandscape
     @PlatinumTest(focusArea = "launcher")
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/326130648
     public void testUninstallFromAllApps() throws Exception {
         installDummyAppAndWaitForUIUpdate();
         try {
diff --git a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index 5283c6b..8631f03 100644
--- a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -11,11 +11,11 @@
 import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY2;
 import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY3;
 import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 import static com.android.launcher3.util.TestUtil.DUMMY_CLASS_NAME;
 import static com.android.launcher3.util.TestUtil.DUMMY_PACKAGE;
 import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -40,13 +40,14 @@
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.LauncherModelHelper;
 import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.launcher3.util.TestUtil;
+import com.android.launcher3.util.rule.TestStabilityRule;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 
 import java.io.IOException;
@@ -61,6 +62,9 @@
 @RunWith(AndroidJUnit4.class)
 public class CacheDataUpdatedTaskTest {
 
+    @Rule(order = 0)
+    public TestRule testStabilityRule = new TestStabilityRule();
+
     @Rule
     public final CheckFlagsRule mCheckFlagsRule =
             DeviceFlagsValueProvider.createCheckFlagsRule();
@@ -157,6 +161,7 @@
     }
 
     @Test
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/325283522
     public void testSessionUpdate_updates_pending_apps() {
         // Run on model executor so that no other task runs in the middle.
         runOnExecutorSync(MODEL_EXECUTOR, () -> {
diff --git a/tests/src/com/android/launcher3/model/data/ItemInfoWithIconTest.kt b/tests/src/com/android/launcher3/model/data/ItemInfoWithIconTest.kt
new file mode 100644
index 0000000..cfbde98
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/data/ItemInfoWithIconTest.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model.data
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.pm.PackageInstallInfo
+import com.android.launcher3.util.LauncherModelHelper
+import com.google.common.truth.Truth
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ItemInfoWithIconTest {
+
+    private var context = LauncherModelHelper.SandboxModelContext()
+    private lateinit var itemInfoWithIcon: ItemInfoWithIcon
+
+    @Before
+    fun setup() {
+        itemInfoWithIcon =
+            object : ItemInfoWithIcon() {
+                override fun clone(): ItemInfoWithIcon? {
+                    return null
+                }
+            }
+    }
+
+    @After
+    fun tearDown() {
+        context.destroy()
+    }
+
+    @Test
+    fun itemInfoWithIconDefaultParamsTest() {
+        Truth.assertThat(itemInfoWithIcon.isDisabled).isFalse()
+        Truth.assertThat(itemInfoWithIcon.isPendingDownload).isFalse()
+        Truth.assertThat(itemInfoWithIcon.isArchived).isFalse()
+    }
+
+    @Test
+    fun isDisabledOrPendingTest() {
+        itemInfoWithIcon.setProgressLevel(0, PackageInstallInfo.STATUS_INSTALLING)
+        Truth.assertThat(itemInfoWithIcon.isDisabled).isFalse()
+        Truth.assertThat(itemInfoWithIcon.isPendingDownload).isTrue()
+
+        itemInfoWithIcon.setProgressLevel(1, PackageInstallInfo.STATUS_INSTALLING)
+        Truth.assertThat(itemInfoWithIcon.isDisabled).isFalse()
+        Truth.assertThat(itemInfoWithIcon.isPendingDownload).isFalse()
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java
index d26a9db..4eae7e1 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java
@@ -21,7 +21,6 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -29,12 +28,6 @@
 @RunWith(AndroidJUnit4.class)
 public class TaplTestsLauncher3Test extends AbstractLauncherUiTest {
 
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        initialize(this);
-    }
-
     @Test
     public void testDevicePressMenu() throws Exception {
         mDevice.pressMenu();
diff --git a/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java b/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
index f8f5dde..802e564 100644
--- a/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
+++ b/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
@@ -28,7 +28,6 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
-import android.platform.test.rule.ScreenRecordRule;
 import android.util.Log;
 import android.view.View;
 
@@ -45,7 +44,8 @@
 import com.android.launcher3.allapps.WorkProfileManager;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.util.TestUtil;
-import com.android.launcher3.util.rule.TestStabilityRule;
+import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
+import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 
 import org.junit.After;
 import org.junit.Before;
@@ -145,8 +145,8 @@
     }
 
     // Staging; will be promoted to presubmit if stable
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
-    @ScreenRecordRule.ScreenRecord
+    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
+    @ScreenRecord
     @Test
     public void toggleWorks() {
         assumeTrue(mWorkProfileSetupSuccessful);
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
index ec226af..5ca5ba5 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
@@ -37,7 +37,6 @@
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import org.junit.Assume;
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,13 +51,6 @@
     @Rule
     public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
 
-    @Override
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        AbstractLauncherUiTest.initialize(this);
-    }
-
     @Test
     @PortraitLandscape
     @ScreenRecord // b/316910614
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
index f12f961..7dba728 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
@@ -74,7 +74,6 @@
         super.setUp();
         mCallbackAction = UUID.randomUUID().toString();
         mShortcutId = UUID.randomUUID().toString();
-        AbstractLauncherUiTest.initialize(this);
     }
 
     @Test
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java b/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
index bc73683..1cd8c88 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
@@ -30,7 +30,6 @@
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 import com.android.launcher3.widget.picker.WidgetsRecyclerView;
 
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -42,12 +41,6 @@
 @RunWith(AndroidJUnit4.class)
 public class TaplWidgetPickerTest extends AbstractLauncherUiTest {
 
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        initialize(this);
-    }
-
     private WidgetsRecyclerView getWidgetsView(Launcher launcher) {
         return WidgetsFullSheet.getWidgetsView(launcher);
     }
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
index a3d3344..c60ee8c 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
@@ -57,7 +57,7 @@
     @Test
     public void testIconWithoutTheme() throws Exception {
         setThemeEnabled(false);
-        AbstractLauncherUiTest.initialize(this);
+        initialize(this);
 
         HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
         allApps.freeze();
@@ -75,7 +75,7 @@
     @Test
     public void testShortcutIconWithoutTheme() throws Exception {
         setThemeEnabled(false);
-        AbstractLauncherUiTest.initialize(this);
+        initialize(this);
 
         HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
         allApps.freeze();
@@ -94,7 +94,7 @@
     @Test
     public void testIconWithTheme() throws Exception {
         setThemeEnabled(true);
-        AbstractLauncherUiTest.initialize(this);
+        initialize(this);
 
         HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
         allApps.freeze();
@@ -112,7 +112,7 @@
     @Test
     public void testShortcutIconWithTheme() throws Exception {
         setThemeEnabled(true);
-        AbstractLauncherUiTest.initialize(this);
+        initialize(this);
 
         HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
         allApps.freeze();
diff --git a/tests/src/com/android/launcher3/util/CellContentDimensionsTest.kt b/tests/src/com/android/launcher3/util/CellContentDimensionsTest.kt
index 4770546..2294f5f 100644
--- a/tests/src/com/android/launcher3/util/CellContentDimensionsTest.kt
+++ b/tests/src/com/android/launcher3/util/CellContentDimensionsTest.kt
@@ -39,6 +39,7 @@
         val config =
             Configuration(runningContext.resources.configuration).apply {
                 this.densityDpi = DisplayMetrics.DENSITY_DEFAULT
+                fontScale = 1.0f
             }
         context = runningContext.createConfigurationContext(config)
         iconSizeSteps = IconSizeSteps(context!!.resources)