Merge "Add CUJ Jank interactions for App Pair saving and launching" into main
diff --git a/quickstep/Android.bp b/quickstep/Android.bp
index a290e84..f14cebd 100644
--- a/quickstep/Android.bp
+++ b/quickstep/Android.bp
@@ -52,5 +52,6 @@
         "tests/src/com/android/quickstep/TaplOverviewIconTest.java",
         "tests/src/com/android/quickstep/TaplTestsQuickstep.java",
         "tests/src/com/android/quickstep/TaplTestsSplitscreen.java",
+        "tests/src/com/android/launcher3/testcomponent/ExcludeFromRecentsTestActivity.java"
     ],
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 36ce049..2710bd9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -414,6 +414,13 @@
     }
 
     @Override
+    protected boolean canToggleHomeAllApps() {
+        return mLauncher.isResumed()
+                && !mTaskbarLauncherStateController.isInOverview()
+                && !mLauncher.areFreeformTasksVisible();
+    }
+
+    @Override
     public RecentsView getRecentsView() {
         return mLauncher.getOverviewPanel();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 37f1363..aedbe6c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -17,6 +17,7 @@
 
 import static android.content.pm.PackageManager.FEATURE_PC;
 import static android.os.Trace.TRACE_TAG_APP;
+import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
@@ -1214,7 +1215,7 @@
                     .handleAppPairLaunchInApp((AppPairIcon) launchingIconView, itemInfos);
         } else {
             // Tapped a single app, nothing complicated here.
-            startItemInfoActivity(itemInfos.get(0));
+            startItemInfoActivity(itemInfos.get(0), null /*foundTask*/);
         }
     }
 
@@ -1255,19 +1256,37 @@
                         recents.getSplitSelectController().getAppPairsController().launchAppPair(
                                 (AppPairIcon) launchingIconView, -1 /*cuj*/);
                     } else {
-                        startItemInfoActivity(itemInfos.get(0));
+                        startItemInfoActivity(itemInfos.get(0), foundTask);
                     }
                 }
         );
     }
 
-    private void startItemInfoActivity(ItemInfo info) {
+    /**
+     * Starts an activity with the information provided by the "info" param. However, if
+     * taskInRecents is present, it will prioritize re-launching an existing instance via
+     * {@link ActivityManagerWrapper#startActivityFromRecents(int, ActivityOptions)}
+     */
+    private void startItemInfoActivity(ItemInfo info, @Nullable Task taskInRecents) {
         Intent intent = new Intent(info.getIntent())
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         try {
             TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon");
             if (info.user.equals(Process.myUserHandle())) {
                 // TODO(b/216683257): Use startActivityForResult for search results that require it.
+                if (taskInRecents != null) {
+                    // Re launch instance from recents
+                    ActivityOptionsWrapper opts = getActivityLaunchOptions(null, info);
+                    opts.options.setLaunchDisplayId(
+                            getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId());
+                    if (ActivityManagerWrapper.getInstance()
+                            .startActivityFromRecents(taskInRecents.key, opts.options)) {
+                        mControllers.uiController.getRecentsView()
+                                .addSideTaskLaunchCallback(opts.onEndCallback);
+                        return;
+                    }
+                }
+
                 startActivity(intent);
             } else {
                 getSystemService(LauncherApps.class).startMainActivity(
@@ -1566,4 +1585,8 @@
     public void closeKeyboardQuickSwitchView() {
         mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false);
     }
+
+    boolean canToggleHomeAllApps() {
+        return mControllers.uiController.canToggleHomeAllApps();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index a14e3fd..8d48154 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -205,13 +205,7 @@
                     mLauncherState = finalState;
                     updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, false);
                     applyState();
-                    boolean disallowLongClick =
-                            FeatureFlags.enableSplitContextually()
-                                    ? mLauncher.isSplitSelectionActive()
-                                    : finalState == LauncherState.OVERVIEW_SPLIT_SELECT;
-                    com.android.launcher3.taskbar.Utilities.setOverviewDragState(
-                            mControllers, finalState.disallowTaskbarGlobalDrag(),
-                            disallowLongClick, finalState.allowTaskbarInitialSplitSelection());
+                    updateOverviewDragState(finalState);
                 }
             };
 
@@ -256,6 +250,7 @@
 
         mCanSyncViews = true;
         mLauncher.addOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
+        updateOverviewDragState(mLauncherState);
     }
 
     public void onDestroy() {
@@ -328,7 +323,7 @@
         updateStateForSysuiFlags(systemUiStateFlags, /* applyState */ true);
     }
 
-    private  void updateStateForSysuiFlags(int systemUiStateFlags, boolean applyState) {
+    private void updateStateForSysuiFlags(int systemUiStateFlags, boolean applyState) {
         final boolean prevIsAwake = hasAnyFlag(FLAG_AWAKE);
         final boolean currIsAwake = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_AWAKE);
 
@@ -358,6 +353,21 @@
     }
 
     /**
+     * Updates overview drag state on various controllers based on {@link #mLauncherState}.
+     *
+     * @param launcherState The current state launcher is in
+     */
+    private void updateOverviewDragState(LauncherState launcherState) {
+        boolean disallowLongClick =
+                FeatureFlags.enableSplitContextually()
+                        ? mLauncher.isSplitSelectionActive()
+                        : launcherState == LauncherState.OVERVIEW_SPLIT_SELECT;
+        com.android.launcher3.taskbar.Utilities.setOverviewDragState(
+                mControllers, launcherState.disallowTaskbarGlobalDrag(),
+                disallowLongClick, launcherState.allowTaskbarInitialSplitSelection());
+    }
+
+    /**
      * Updates the proper flag to change the state of the task bar.
      *
      * Note that this only updates the flag. {@link #applyState()} needs to be called separately.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index ff33ca9..ecbc7e7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -23,7 +23,6 @@
 
 import static com.android.launcher3.BaseActivity.EVENT_DESTROYED;
 import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
-import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate;
 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
@@ -69,6 +68,7 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
+import com.android.quickstep.AllAppsActionManager;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TouchInteractionService;
@@ -158,6 +158,8 @@
     private final SimpleBroadcastReceiver mTaskbarBroadcastReceiver =
             new SimpleBroadcastReceiver(this::showTaskbarFromBroadcast);
 
+    private final AllAppsActionManager mAllAppsActionManager;
+
     private final Runnable mActivityOnDestroyCallback = new Runnable() {
         @Override
         public void run() {
@@ -212,12 +214,14 @@
     private Boolean mFolded;
 
     @SuppressLint("WrongConstant")
-    public TaskbarManager(TouchInteractionService service) {
+    public TaskbarManager(
+            TouchInteractionService service, AllAppsActionManager allAppsActionManager) {
         Display display =
                 service.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY);
         mContext = service.createWindowContext(display,
                 ENABLE_TASKBAR_NAVBAR_UNIFICATION ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL,
                 null);
+        mAllAppsActionManager = allAppsActionManager;
         mNavigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
                 ? service.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
                 : null;
@@ -291,10 +295,10 @@
                     recreateTaskbar();
                 } else {
                     // Config change might be handled without re-creating the taskbar
-                    if (dp != null && !isTaskbarPresent(dp)) {
+                    if (dp != null && !isTaskbarEnabled(dp)) {
                         destroyExistingTaskbar();
                     } else {
-                        if (dp != null && isTaskbarPresent(dp)) {
+                        if (dp != null && isTaskbarEnabled(dp)) {
                             if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
                                 // Re-initialize for screen size change? Should this be done
                                 // by looking at screen-size change flag in configDiff in the
@@ -349,7 +353,7 @@
         }
         DeviceProfile dp = mUserUnlocked ?
                 LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null;
-        if (dp == null || !isTaskbarPresent(dp)) {
+        if (dp == null || !isTaskbarEnabled(dp)) {
             removeTaskbarRootViewFromWindow();
         }
     }
@@ -369,20 +373,11 @@
      * @param homeAllAppsIntent Intent used if Taskbar is not enabled or Launcher is resumed.
      */
     public void toggleAllApps(Intent homeAllAppsIntent) {
-        if (mTaskbarActivityContext == null) {
+        if (mTaskbarActivityContext == null || mTaskbarActivityContext.canToggleHomeAllApps()) {
             mContext.startActivity(homeAllAppsIntent);
-            return;
+        } else {
+            mTaskbarActivityContext.toggleAllAppsSearch();
         }
-
-        if (mActivity != null
-                && mActivity.isResumed()
-                && !mActivity.isInState(OVERVIEW)
-                && !(mActivity instanceof QuickstepLauncher l && l.areFreeformTasksVisible())) {
-            mContext.startActivity(homeAllAppsIntent);
-            return;
-        }
-
-        mTaskbarActivityContext.toggleAllAppsSearch();
     }
 
     /**
@@ -477,9 +472,12 @@
             DeviceProfile dp = mUserUnlocked ?
                 LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null;
 
+            // All Apps action is unrelated to navbar unification, so we only need to check DP.
+            mAllAppsActionManager.setTaskbarPresent(dp != null && dp.isTaskbarPresent);
+
             destroyExistingTaskbar();
 
-            boolean isTaskbarEnabled = dp != null && isTaskbarPresent(dp);
+            boolean isTaskbarEnabled = dp != null && isTaskbarEnabled(dp);
             debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled
                 + " [dp != null (i.e. mUserUnlocked)]=" + (dp != null)
                 + " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION
@@ -544,7 +542,7 @@
         }
     }
 
-    private static boolean isTaskbarPresent(DeviceProfile deviceProfile) {
+    private static boolean isTaskbarEnabled(DeviceProfile deviceProfile) {
         return ENABLE_TASKBAR_NAVBAR_UNIFICATION || deviceProfile.isTaskbarPresent;
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index efe1e39..109400e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -197,6 +197,11 @@
         return false;
     }
 
+    /** Returns {@code true} if Home All Apps available instead of Taskbar All Apps. */
+    protected boolean canToggleHomeAllApps() {
+        return false;
+    }
+
     @CallSuper
     protected void dumpLogs(String prefix, PrintWriter pw) {
         pw.println(String.format(
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index a92e77a..8ef35c0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -49,7 +49,6 @@
 import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
-import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
 import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
@@ -178,10 +177,6 @@
         if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
             return false;
         }
-        if (isDesktopModeSupported()) {
-            // TODO(b/268075592): add support for quickswitch to/from desktop
-            return false;
-        }
         if (isTrackpadMultiFingerSwipe(ev)) {
             return isTrackpadFourFingerSwipe(ev);
         }
@@ -324,7 +319,6 @@
 
     @Override
     public void onDragEnd(PointF velocity) {
-        cancelAnimations();
         boolean horizontalFling = mSwipeDetector.isFling(velocity.x);
         boolean verticalFling = mSwipeDetector.isFling(velocity.y);
         boolean noFling = !horizontalFling && !verticalFling;
@@ -353,6 +347,7 @@
             return;
         }
         InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS);
+        cancelAnimations();
 
         final LauncherState targetState;
         if (horizontalFling && verticalFling) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index ff142fe..de73630 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -30,7 +30,6 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
-import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
@@ -79,10 +78,6 @@
         if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) {
             return false;
         }
-        if (isDesktopModeSupported()) {
-            // TODO(b/268075592): add support for quickswitch to/from desktop
-            return false;
-        }
         return true;
     }
 
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 4752225..181bda9 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -1255,7 +1255,11 @@
             return LAST_TASK;
         }
 
-        if (isDesktopModeSupported() && endTarget == NEW_TASK) {
+        if (((mRecentsView.getNextPageTaskView() != null
+                && mRecentsView.getNextPageTaskView().isDesktopTask())
+                || (mRecentsView.getCurrentPageTaskView() != null
+                && mRecentsView.getCurrentPageTaskView().isDesktopTask()))
+                && endTarget == NEW_TASK) {
             // TODO(b/268075592): add support for quickswitch to/from desktop
             return LAST_TASK;
         }
@@ -1416,9 +1420,11 @@
             mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED);
             setClampScrollOffset(false);
         };
-        if (mRecentsView != null) {
+        if (mRecentsView != null && (mRecentsView.getCurrentPageTaskView() != null
+                && !mRecentsView.getCurrentPageTaskView().isDesktopTask())) {
             ActiveGestureLog.INSTANCE.trackEvent(ActiveGestureErrorDetector.GestureEvent
                     .SET_ON_PAGE_TRANSITION_END_CALLBACK);
+            // TODO(b/268075592): add support for quickswitch to/from desktop
             mRecentsView.setOnPageTransitionEndCallback(onPageTransitionEnd);
         } else {
             onPageTransitionEnd.run();
@@ -2232,6 +2238,15 @@
                     mRecentsAnimationController, mRecentsAnimationTargets);
         });
 
+        if ((mRecentsView.getNextPageTaskView() != null
+                && mRecentsView.getNextPageTaskView().isDesktopTask())
+                || (mRecentsView.getCurrentPageTaskView() != null
+                && mRecentsView.getCurrentPageTaskView().isDesktopTask())) {
+            // TODO(b/268075592): add support for quickswitch to/from desktop
+            mRecentsViewScrollLinked = false;
+            return;
+        }
+
         // Disable scrolling in RecentsView for trackpad 3-finger swipe up gesture.
         if (!mGestureState.isThreeFingerTrackpadGesture()) {
             mRecentsViewScrollLinked = true;
diff --git a/quickstep/src/com/android/quickstep/AllAppsActionManager.kt b/quickstep/src/com/android/quickstep/AllAppsActionManager.kt
new file mode 100644
index 0000000..fd2ed3a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/AllAppsActionManager.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep
+
+import android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS
+import android.app.PendingIntent
+import android.app.RemoteAction
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.view.accessibility.AccessibilityManager
+import com.android.launcher3.R
+import java.util.concurrent.Executor
+
+/**
+ * Registers a [RemoteAction] for toggling All Apps if needed.
+ *
+ * We need this action when either [isHomeAndOverviewSame] or [isTaskbarPresent] is `true`. When
+ * home and overview are the same, we can control Launcher's or Taskbar's All Apps tray. If they are
+ * not the same, but Taskbar is present, we can only control Taskbar's tray.
+ */
+class AllAppsActionManager(
+    private val context: Context,
+    private val bgExecutor: Executor,
+    private val createAllAppsPendingIntent: () -> PendingIntent,
+) {
+
+    /** `true` if home and overview are the same Activity. */
+    var isHomeAndOverviewSame = false
+        set(value) {
+            field = value
+            updateSystemAction()
+        }
+
+    /** `true` if Taskbar is enabled. */
+    var isTaskbarPresent = false
+        set(value) {
+            field = value
+            updateSystemAction()
+        }
+
+    /** `true` if the action should be registered. */
+    var isActionRegistered = false
+        private set
+
+    private fun updateSystemAction() {
+        val shouldRegisterAction = isHomeAndOverviewSame || isTaskbarPresent
+        if (isActionRegistered == shouldRegisterAction) return
+        isActionRegistered = shouldRegisterAction
+
+        bgExecutor.execute {
+            val accessibilityManager =
+                context.getSystemService(AccessibilityManager::class.java) ?: return@execute
+            if (shouldRegisterAction) {
+                accessibilityManager.registerSystemAction(
+                    RemoteAction(
+                        Icon.createWithResource(context, R.drawable.ic_apps),
+                        context.getString(R.string.all_apps_label),
+                        context.getString(R.string.all_apps_label),
+                        createAllAppsPendingIntent(),
+                    ),
+                    GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS,
+                )
+            } else {
+                accessibilityManager.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS)
+            }
+        }
+    }
+
+    fun onDestroy() {
+        context
+            .getSystemService(AccessibilityManager::class.java)
+            ?.unregisterSystemAction(
+                GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS,
+            )
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 719c4f7..b43c520 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -31,6 +31,7 @@
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TRACKPAD_GESTURE;
 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.OnboardingPrefs.HOME_BOUNCE_SEEN;
 import static com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH;
 import static com.android.quickstep.GestureState.DEFAULT_STATE;
@@ -59,14 +60,12 @@
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW;
 
 import android.app.PendingIntent;
-import android.app.RemoteAction;
 import android.app.Service;
 import android.content.IIntentReceiver;
 import android.content.IIntentSender;
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.graphics.Region;
-import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Looper;
@@ -77,7 +76,6 @@
 import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.MotionEvent;
-import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.BinderThread;
 import androidx.annotation.NonNull;
@@ -88,7 +86,6 @@
 import com.android.launcher3.ConstantItem;
 import com.android.launcher3.EncryptionType;
 import com.android.launcher3.LauncherPrefs;
-import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.provider.RestoreDbTask;
@@ -101,7 +98,6 @@
 import com.android.launcher3.uioverrides.flags.FlagsFactory;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.DisplayController;
-import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.ScreenOnTracker;
@@ -488,6 +484,7 @@
 
     private TaskbarManager mTaskbarManager;
     private Function<GestureState, AnimatedFloat> mSwipeUpProxyProvider = i -> null;
+    private AllAppsActionManager mAllAppsActionManager;
 
     @Override
     public void onCreate() {
@@ -497,7 +494,9 @@
         mMainChoreographer = Choreographer.getInstance();
         mAM = ActivityManagerWrapper.getInstance();
         mDeviceState = new RecentsAnimationDeviceState(this, true);
-        mTaskbarManager = new TaskbarManager(this);
+        mAllAppsActionManager = new AllAppsActionManager(
+                this, UI_HELPER_EXECUTOR, this::createAllAppsPendingIntent);
+        mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager);
         mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
         mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer();
         BootAwarePreloader.start(this);
@@ -590,16 +589,7 @@
     }
 
     private void onOverviewTargetChange(boolean isHomeAndOverviewSame) {
-        Executors.UI_HELPER_EXECUTOR.execute(() -> {
-            AccessibilityManager am = getSystemService(AccessibilityManager.class);
-
-            if (isHomeAndOverviewSame) {
-                am.registerSystemAction(
-                        createAllAppsAction(), GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
-            } else {
-                am.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
-            }
-        });
+        mAllAppsActionManager.setHomeAndOverviewSame(isHomeAndOverviewSame);
 
         StatefulActivity newOverviewActivity = mOverviewComponentObserver.getActivityInterface()
                 .getCreatedActivity();
@@ -609,13 +599,12 @@
         mTISBinder.onOverviewTargetChange();
     }
 
-    private RemoteAction createAllAppsAction() {
+    private PendingIntent createAllAppsPendingIntent() {
         final Intent homeIntent = new Intent(mOverviewComponentObserver.getHomeIntent())
                 .setAction(INTENT_ACTION_ALL_APPS_TOGGLE);
-        final PendingIntent actionPendingIntent;
 
         if (FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) {
-            actionPendingIntent = new PendingIntent(new IIntentSender.Stub() {
+            return new PendingIntent(new IIntentSender.Stub() {
                 @Override
                 public void send(int code, Intent intent, String resolvedType,
                         IBinder allowlistToken, IIntentReceiver finishedReceiver,
@@ -624,18 +613,12 @@
                 }
             });
         } else {
-            actionPendingIntent = PendingIntent.getActivity(
+            return PendingIntent.getActivity(
                     this,
                     GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS,
                     homeIntent,
                     PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
         }
-
-        return new RemoteAction(
-                Icon.createWithResource(this, R.drawable.ic_apps),
-                getString(R.string.all_apps_label),
-                getString(R.string.all_apps_label),
-                actionPendingIntent);
     }
 
     @UiThread
@@ -678,8 +661,7 @@
         mDeviceState.destroy();
         SystemUiProxy.INSTANCE.get(this).clearProxy();
 
-        getSystemService(AccessibilityManager.class)
-                .unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
+        mAllAppsActionManager.onDestroy();
 
         mTaskbarManager.destroy();
         sConnected = false;
diff --git a/quickstep/src/com/android/quickstep/util/AssistStateManager.java b/quickstep/src/com/android/quickstep/util/AssistStateManager.java
index 660fc22..a854656 100644
--- a/quickstep/src/com/android/quickstep/util/AssistStateManager.java
+++ b/quickstep/src/com/android/quickstep/util/AssistStateManager.java
@@ -53,12 +53,7 @@
     }
 
     /** Return {@code true} if the Settings toggle is enabled. */
-    public boolean isSettingsNavHandleEnabled() {
-        return false;
-    }
-
-    /** Return {@code true} if the Settings toggle is enabled. */
-    public boolean isSettingsHomeButtonEnabled() {
+    public boolean isSettingsAllEntrypointsEnabled() {
         return false;
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 2ead7a6..af1dd3a 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1270,6 +1270,8 @@
                     final SurfaceTransaction showTransaction = new SurfaceTransaction();
                     for (int i = apps.length - 1; i >= 0; --i) {
                         showTransaction.getTransaction().show(apps[i].leash);
+                        showTransaction.forSurface(apps[i].leash).setLayer(
+                                Integer.MAX_VALUE - 1000 + apps[i].prefixOrderIndex);
                     }
                     surfaceApplier.scheduleApply(showTransaction);
                 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt
new file mode 100644
index 0000000..73b35e8
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep
+
+import android.app.PendingIntent
+import android.content.IIntentSender
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+import com.android.launcher3.util.TestUtil
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit.SECONDS
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TIMEOUT = 5L
+
+@RunWith(AndroidJUnit4::class)
+class AllAppsActionManagerTest {
+    private val callbackSemaphore = Semaphore(0)
+    private val bgExecutor = UI_HELPER_EXECUTOR
+
+    private val allAppsActionManager =
+        AllAppsActionManager(
+            InstrumentationRegistry.getInstrumentation().targetContext,
+            bgExecutor,
+        ) {
+            callbackSemaphore.release()
+            PendingIntent(IIntentSender.Default())
+        }
+
+    @Test
+    fun taskbarPresent_actionRegistered() {
+        allAppsActionManager.isTaskbarPresent = true
+        assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
+        assertThat(allAppsActionManager.isActionRegistered).isTrue()
+    }
+
+    @Test
+    fun homeAndOverviewSame_actionRegistered() {
+        allAppsActionManager.isHomeAndOverviewSame = true
+        assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
+        assertThat(allAppsActionManager.isActionRegistered).isTrue()
+    }
+
+    @Test
+    fun toggleTaskbar_destroyedAfterActionRegistered_actionUnregistered() {
+        allAppsActionManager.isTaskbarPresent = true
+        assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
+
+        allAppsActionManager.isTaskbarPresent = false
+        TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to unregister.
+        assertThat(allAppsActionManager.isActionRegistered).isFalse()
+    }
+
+    @Test
+    fun toggleTaskbar_destroyedBeforeActionRegistered_pendingActionUnregistered() {
+        allAppsActionManager.isTaskbarPresent = true
+        allAppsActionManager.isTaskbarPresent = false
+
+        TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to unregister.
+        assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
+        assertThat(allAppsActionManager.isActionRegistered).isFalse()
+    }
+
+    @Test
+    fun changeHome_sameAsOverviewBeforeActionUnregistered_actionRegisteredAgain() {
+        allAppsActionManager.isHomeAndOverviewSame = true // Initialize to same.
+        assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
+
+        allAppsActionManager.isHomeAndOverviewSame = false
+        allAppsActionManager.isHomeAndOverviewSame = true
+        assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
+        assertThat(allAppsActionManager.isActionRegistered).isTrue()
+    }
+}
diff --git a/quickstep/tests/src/com/android/launcher3/testcomponent/ExcludeFromRecentsTestActivity.java b/quickstep/tests/src/com/android/launcher3/testcomponent/ExcludeFromRecentsTestActivity.java
new file mode 100644
index 0000000..68ac3d5
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/testcomponent/ExcludeFromRecentsTestActivity.java
@@ -0,0 +1,22 @@
+/*
+ * 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.testcomponent;
+
+/**
+ * Extension of BaseTestingActivity to help test excludeFromRecents="true".
+ */
+public class ExcludeFromRecentsTestActivity extends BaseTestingActivity {}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index a53bb4e..e37e5cc 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -21,6 +21,7 @@
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
@@ -37,6 +38,7 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.tapl.BaseOverview;
 import com.android.launcher3.tapl.LaunchedAppState;
 import com.android.launcher3.tapl.LauncherInstrumentation.NavigationModel;
 import com.android.launcher3.tapl.Overview;
@@ -54,6 +56,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -432,6 +435,7 @@
     @PortraitLandscape
     @TaskbarModeSwitch()
     @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/309820115
+    @Ignore("b/315376057")
     @ScreenRecord // b/309820115
     public void testOverviewForTablet() throws Exception {
         assumeTrue(mLauncher.isTablet());
@@ -581,4 +585,25 @@
             mLauncher.getDevice().setOrientationNatural();
         }
     }
+
+    @Test
+    public void testExcludeFromRecents() throws Exception {
+        startExcludeFromRecentsTestActivity();
+        OverviewTask currentTask = getAndAssertLaunchedApp().switchToOverview().getCurrentTask();
+        // TODO(b/326565120): the expected content description shouldn't be null but for now there
+        // is a bug that causes it to sometimes be for excludeForRecents tasks.
+        assertTrue("Can't find ExcludeFromRecentsTestActivity after entering Overview from it",
+                currentTask.containsContentDescription("ExcludeFromRecents")
+                        || currentTask.containsContentDescription(null));
+        // Going home should clear out the excludeFromRecents task.
+        BaseOverview overview = mLauncher.goHome().switchToOverview();
+        if (overview.hasTasks()) {
+            currentTask = overview.getCurrentTask();
+            assertFalse("Found ExcludeFromRecentsTestActivity after entering Overview from Home",
+                    currentTask.containsContentDescription("ExcludeFromRecents")
+                            || currentTask.containsContentDescription(null));
+        } else {
+            // Presumably the test started with 0 tasks and remains that way after going home.
+        }
+    }
 }
diff --git a/res/layout/widget_recommendations.xml b/res/layout/widget_recommendations.xml
index 89821ac..531db2e 100644
--- a/res/layout/widget_recommendations.xml
+++ b/res/layout/widget_recommendations.xml
@@ -28,6 +28,7 @@
         android:layout_marginTop="16dp"
         android:accessibilityLiveRegion="polite"
         android:gravity="center_horizontal"
+        android:layout_gravity="top"
         android:lineHeight="20sp"
         android:textColor="?attr/widgetPickerTitleColor"
         android:textFontWeight="500"
@@ -38,7 +39,7 @@
         android:id="@+id/widget_recommendations_page_indicator"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_gravity="center_horizontal"
+        android:layout_gravity="center_horizontal|top"
         android:elevation="1dp"
         android:visibility="gone" />
     <!--
@@ -50,8 +51,9 @@
     <com.android.launcher3.widget.picker.WidgetRecommendationsView
         android:id="@+id/widget_recommendations_view"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="0dp"
         android:layout_gravity="center"
+        android:layout_weight="1"
         android:background="@drawable/widgets_surface_background"
         android:importantForAccessibility="yes"
         launcher:pageIndicator="@+id/widget_recommendations_page_indicator" />
diff --git a/res/layout/widgets_two_pane_sheet.xml b/res/layout/widgets_two_pane_sheet.xml
index 8e45740f..6c4810c 100644
--- a/res/layout/widgets_two_pane_sheet.xml
+++ b/res/layout/widgets_two_pane_sheet.xml
@@ -122,7 +122,7 @@
                         <LinearLayout
                             android:id="@+id/widget_recommendations_container"
                             android:layout_width="match_parent"
-                            android:layout_height="wrap_content"
+                            android:layout_height="match_parent"
                             android:background="@drawable/widgets_surface_background"
                             android:orientation="vertical"
                             android:visibility="gone">
diff --git a/res/values/id.xml b/res/values/id.xml
index 198496f..59813ad 100644
--- a/res/values/id.xml
+++ b/res/values/id.xml
@@ -40,6 +40,7 @@
     <item type="id" name="cache_entry_tag_id" />
 
     <item type="id" name="saved_clip_children_tag_id" />
+    <item type="id" name="saved_clip_to_padding_tag_id" />
 
     <item type="id" name="saved_floating_widget_foreground" />
     <item type="id" name="saved_floating_widget_background" />
diff --git a/src/com/android/launcher3/UtilitiesKt.kt b/src/com/android/launcher3/UtilitiesKt.kt
new file mode 100644
index 0000000..a207d57
--- /dev/null
+++ b/src/com/android/launcher3/UtilitiesKt.kt
@@ -0,0 +1,159 @@
+/*
+ * 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
+
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewParent
+
+object UtilitiesKt {
+
+    /**
+     * Modify [ViewGroup]'s attribute with type [T]. The overridden attribute is saved by calling
+     * [View.setTag] and can be later restored by [View.getTag].
+     *
+     * @param <T> type of [ViewGroup] attribute. For example, [T] is [Boolean] if modifying
+     *   [ViewGroup.setClipChildren]
+     */
+    abstract class ViewGroupAttrModifier<T>(
+        private val targetAttrValue: T,
+        private val tagKey: Int
+    ) {
+        /**
+         * If [targetAttrValue] is different from existing view attribute returned from
+         * [getAttribute], this method will save existing attribute by calling [ViewGroup.setTag].
+         * Then call [setAttribute] to set attribute with [targetAttrValue].
+         */
+        fun saveAndChangeAttribute(viewGroup: ViewGroup) {
+            val oldAttrValue = getAttribute(viewGroup)
+            if (oldAttrValue !== targetAttrValue) {
+                viewGroup.setTag(tagKey, oldAttrValue)
+                setAttribute(viewGroup, targetAttrValue)
+            }
+        }
+
+        /** Restore saved attribute in [saveAndChangeAttribute] by calling [ViewGroup.getTag]. */
+        @Suppress("UNCHECKED_CAST")
+        fun restoreAttribute(viewGroup: ViewGroup) {
+            val oldAttrValue: T = viewGroup.getTag(tagKey) as T ?: return
+            setAttribute(viewGroup, oldAttrValue)
+            viewGroup.setTag(tagKey, null)
+        }
+
+        /** Subclass will override this method to decide how to get [ViewGroup] attribute. */
+        abstract fun getAttribute(viewGroup: ViewGroup): T
+
+        /** Subclass will override this method to decide how to set [ViewGroup] attribute. */
+        abstract fun setAttribute(viewGroup: ViewGroup, attr: T)
+    }
+
+    /** [ViewGroupAttrModifier] to call [ViewGroup.setClipChildren] to false. */
+    @JvmField
+    val CLIP_CHILDREN_FALSE_MODIFIER: ViewGroupAttrModifier<Boolean> =
+        object : ViewGroupAttrModifier<Boolean>(false, R.id.saved_clip_children_tag_id) {
+            override fun getAttribute(viewGroup: ViewGroup): Boolean {
+                return viewGroup.clipChildren
+            }
+
+            override fun setAttribute(viewGroup: ViewGroup, clipChildren: Boolean) {
+                viewGroup.clipChildren = clipChildren
+            }
+        }
+
+    /** [ViewGroupAttrModifier] to call [ViewGroup.setClipToPadding] to false. */
+    @JvmField
+    val CLIP_TO_PADDING_FALSE_MODIFIER: ViewGroupAttrModifier<Boolean> =
+        object : ViewGroupAttrModifier<Boolean>(false, R.id.saved_clip_to_padding_tag_id) {
+            override fun getAttribute(viewGroup: ViewGroup): Boolean {
+                return viewGroup.clipToPadding
+            }
+
+            override fun setAttribute(viewGroup: ViewGroup, clipToPadding: Boolean) {
+                viewGroup.clipToPadding = clipToPadding
+            }
+        }
+
+    /**
+     * Recursively call [ViewGroupAttrModifier.saveAndChangeAttribute] from [View] to its parent
+     * (direct or indirect) inclusive.
+     *
+     * [ViewGroupAttrModifier.saveAndChangeAttribute] will save the existing attribute value on each
+     * view with [View.setTag], which can be restored in [restoreAttributesOnViewTree].
+     *
+     * Note that if parent is null or not a parent of the view, this method will be applied all the
+     * way to root view.
+     *
+     * @param v child view
+     * @param parent direct or indirect parent of child view
+     * @param modifiers list of [ViewGroupAttrModifier] to modify view attribute
+     */
+    @JvmStatic
+    fun modifyAttributesOnViewTree(
+        v: View?,
+        parent: ViewParent?,
+        vararg modifiers: ViewGroupAttrModifier<*>
+    ) {
+        if (v == null) {
+            return
+        }
+        if (v is ViewGroup) {
+            for (modifier in modifiers) {
+                modifier.saveAndChangeAttribute(v)
+            }
+        }
+        if (v === parent) {
+            return
+        }
+        if (v.parent is View) {
+            modifyAttributesOnViewTree(v.parent as View, parent, *modifiers)
+        }
+    }
+
+    /**
+     * Recursively call [ViewGroupAttrModifier.restoreAttribute]} to restore view attributes
+     * previously saved in [ViewGroupAttrModifier.saveAndChangeAttribute] on view to its parent
+     * (direct or indirect) inclusive.
+     *
+     * Note that if parent is null or not a parent of the view, this method will be applied all the
+     * way to root view.
+     *
+     * @param v child view
+     * @param parent direct or indirect parent of child view
+     * @param modifiers list of [ViewGroupAttrModifier] to restore view attributes
+     */
+    @JvmStatic
+    fun restoreAttributesOnViewTree(
+        v: View?,
+        parent: ViewParent?,
+        vararg modifiers: ViewGroupAttrModifier<*>
+    ) {
+        if (v == null) {
+            return
+        }
+        if (v is ViewGroup) {
+            for (modifier in modifiers) {
+                modifier.restoreAttribute(v)
+            }
+        }
+        if (v === parent) {
+            return
+        }
+        if (v.parent is View) {
+            restoreAttributesOnViewTree(v.parent as View, parent, *modifiers)
+        }
+    }
+}
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 31bbe01..66b8216 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -55,6 +55,7 @@
 import com.android.launcher3.views.OptionsPopupView;
 import com.android.launcher3.views.OptionsPopupView.OptionItem;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.util.WidgetSizes;
 
 import java.util.ArrayList;
@@ -413,6 +414,10 @@
                 announceConfirmation(R.string.item_added_to_workspace);
             } else if (item instanceof PendingAddItemInfo) {
                 PendingAddItemInfo info = (PendingAddItemInfo) item;
+                if (info instanceof PendingAddWidgetInfo widgetInfo
+                        && widgetInfo.bindOptions == null) {
+                    widgetInfo.bindOptions = widgetInfo.getDefaultSizeOptions(mContext);
+                }
                 Workspace<?> workspace = mContext.getWorkspace();
                 workspace.post(
                         () -> workspace.snapToPage(workspace.getPageIndexForScreenId(screenId))
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index e2c5795..3678109 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -24,6 +24,9 @@
 import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER;
+import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree;
+import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree;
 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_BOTTOM_SHEET_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
@@ -38,12 +41,9 @@
 import android.util.FloatProperty;
 import android.view.HapticFeedbackConstants;
 import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
 import android.view.animation.Interpolator;
 
 import androidx.annotation.FloatRange;
-import androidx.annotation.Nullable;
 
 import com.android.app.animation.Interpolators;
 import com.android.launcher3.DeviceProfile;
@@ -306,9 +306,11 @@
         if (hasScaleEffect != mHasScaleEffect) {
             mHasScaleEffect = hasScaleEffect;
             if (mHasScaleEffect) {
-                setClipChildrenOnViewTree(rv, mLauncher.getAppsView(), false);
+                modifyAttributesOnViewTree(rv, mLauncher.getAppsView(),
+                        CLIP_CHILDREN_FALSE_MODIFIER);
             } else {
-                restoreClipChildrenOnViewTree(rv, mLauncher.getAppsView());
+                restoreAttributesOnViewTree(rv, mLauncher.getAppsView(),
+                        CLIP_CHILDREN_FALSE_MODIFIER);
             }
         }
     }
@@ -423,79 +425,6 @@
     }
 
     /**
-     * Recursively call {@link ViewGroup#setClipChildren(boolean)} from {@link View} to ts parent
-     * (direct or indirect) inclusive. This method will also save the old clipChildren value on each
-     * view with {@link View#setTag(int, Object)}, which can be restored in
-     * {@link #restoreClipChildrenOnViewTree(View, ViewParent)}.
-     *
-     * Note that if parent is null or not a parent of the view, this method will be applied all the
-     * way to root view.
-     *
-     * @param v child view
-     * @param parent direct or indirect parent of child view
-     * @param clipChildren whether we should clip children
-     */
-    private static void setClipChildrenOnViewTree(
-            @Nullable View v,
-            @Nullable ViewParent parent,
-            boolean clipChildren) {
-        if (v == null) {
-            return;
-        }
-
-        if (v instanceof ViewGroup) {
-            ViewGroup viewGroup = (ViewGroup) v;
-            boolean oldClipChildren = viewGroup.getClipChildren();
-            if (oldClipChildren != clipChildren) {
-                v.setTag(R.id.saved_clip_children_tag_id, oldClipChildren);
-                viewGroup.setClipChildren(clipChildren);
-            }
-        }
-
-        if (v == parent) {
-            return;
-        }
-
-        if (v.getParent() instanceof View) {
-            setClipChildrenOnViewTree((View) v.getParent(), parent, clipChildren);
-        }
-    }
-
-    /**
-     * Recursively call {@link ViewGroup#setClipChildren(boolean)} to restore clip children value
-     * set in {@link #setClipChildrenOnViewTree(View, ViewParent, boolean)} on view to its parent
-     * (direct or indirect) inclusive.
-     *
-     * Note that if parent is null or not a parent of the view, this method will be applied all the
-     * way to root view.
-     *
-     * @param v child view
-     * @param parent direct or indirect parent of child view
-     */
-    private static void restoreClipChildrenOnViewTree(
-            @Nullable View v, @Nullable ViewParent parent) {
-        if (v == null) {
-            return;
-        }
-        if (v instanceof ViewGroup) {
-            ViewGroup viewGroup = (ViewGroup) v;
-            Object viewTag = viewGroup.getTag(R.id.saved_clip_children_tag_id);
-            if (viewTag instanceof Boolean) {
-                viewGroup.setClipChildren((boolean) viewTag);
-                viewGroup.setTag(R.id.saved_clip_children_tag_id, null);
-            }
-        }
-
-        if (v == parent) {
-            return;
-        }
-
-        if (v.getParent() instanceof View) {
-            restoreClipChildrenOnViewTree((View) v.getParent(), parent);
-        }
-    }
-
-    /**
      * Updates the total scroll range but does not update the UI.
      */
     public void setShiftRange(float shiftRange) {
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index fba7537..e07408a 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -18,6 +18,8 @@
 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_LEFT;
 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_RIGHT;
 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_PREINSTALLED_APPS_COUNT;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_USER_INSTALLED_APPS_COUNT;
 
 import android.content.Context;
 
@@ -342,6 +344,20 @@
         Map<Boolean, List<AppInfo>> split = mPrivateApps.stream()
                 .collect(Collectors.partitioningBy(mPrivateProviderManager
                                 .splitIntoUserInstalledAndSystemApps()));
+
+        // TODO(b/329688630): switch to the pulled LayoutStaticSnapshot atom
+        mActivityContext
+                .getStatsLogManager()
+                .logger()
+                .withCardinality(split.get(true).size())
+                .log(LAUNCHER_PRIVATE_SPACE_USER_INSTALLED_APPS_COUNT);
+
+        mActivityContext
+                .getStatsLogManager()
+                .logger()
+                .withCardinality(split.get(false).size())
+                .log(LAUNCHER_PRIVATE_SPACE_PREINSTALLED_APPS_COUNT);
+
         // Add user installed apps
         position = addAppsWithSections(split.get(true), position);
         // Add system apps separator.
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 3ede267..52fb122 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -754,6 +754,12 @@
         @UiEvent(doc = "User tapped add widget button in widget sheet.")
         LAUNCHER_WIDGET_ADD_BUTTON_TAP(1622),
 
+        @UiEvent(doc = "Number of user installed Private profile apps, shown above separator line")
+        LAUNCHER_PRIVATE_SPACE_USER_INSTALLED_APPS_COUNT(1672),
+
+        @UiEvent(doc = "Number of preinstalled Private profile apps, shown under separator line")
+        LAUNCHER_PRIVATE_SPACE_PREINSTALLED_APPS_COUNT(1673)
+
         // ADD MORE
         ;
 
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index fa2a1b0..966b6a6 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -29,6 +29,8 @@
 import android.util.Pair;
 import android.view.View;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel.CallbackTask;
@@ -309,9 +311,16 @@
             // Bind workspace screens
             executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
 
+            ItemInflater inflater = mCallbacks.getItemInflater();
+
             // Load items on the current page.
-            bindItemsInChunks(currentWorkspaceItems, ITEMS_CHUNK, mUiExecutor);
-            bindItemsInChunks(currentAppWidgets, 1, mUiExecutor);
+            if (enableWorkspaceInflation() && inflater != null) {
+                inflateAsyncAndBind(currentWorkspaceItems, inflater, mUiExecutor);
+                inflateAsyncAndBind(currentAppWidgets, inflater, mUiExecutor);
+            } else {
+                bindItemsInChunks(currentWorkspaceItems, ITEMS_CHUNK, mUiExecutor);
+                bindItemsInChunks(currentAppWidgets, 1, mUiExecutor);
+            }
             if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
                 mExtraItems.forEach(item ->
                         executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
@@ -322,18 +331,20 @@
 
             RunnableList onCompleteSignal = new RunnableList();
 
-            if (enableWorkspaceInflation()) {
+            if (enableWorkspaceInflation() && inflater != null) {
                 MODEL_EXECUTOR.execute(() ->  {
-                    setupPendingBind(otherWorkspaceItems, otherAppWidgets, currentScreenIds,
-                            pendingExecutor);
+                    inflateAsyncAndBind(otherWorkspaceItems, inflater, pendingExecutor);
+                    inflateAsyncAndBind(otherAppWidgets, inflater, pendingExecutor);
+                    setupPendingBind(currentScreenIds, pendingExecutor);
 
                     // Wait for the async inflation to complete and then notify the completion
                     // signal on UI thread.
                     MAIN_EXECUTOR.execute(onCompleteSignal::executeAllAndDestroy);
                 });
             } else {
-                setupPendingBind(
-                        otherWorkspaceItems, otherAppWidgets, currentScreenIds, pendingExecutor);
+                bindItemsInChunks(otherWorkspaceItems, ITEMS_CHUNK, pendingExecutor);
+                bindItemsInChunks(otherAppWidgets, 1, pendingExecutor);
+                setupPendingBind(currentScreenIds, pendingExecutor);
                 onCompleteSignal.executeAllAndDestroy();
             }
 
@@ -348,13 +359,8 @@
         }
 
         private void setupPendingBind(
-                List<ItemInfo> otherWorkspaceItems,
-                List<ItemInfo> otherAppWidgets,
                 IntSet currentScreenIds,
                 Executor pendingExecutor) {
-            bindItemsInChunks(otherWorkspaceItems, ITEMS_CHUNK, pendingExecutor);
-            bindItemsInChunks(otherAppWidgets, 1, pendingExecutor);
-
             StringCache cacheClone = mBgDataModel.stringCache.clone();
             executeCallbacksTask(c -> c.bindStringCache(cacheClone), pendingExecutor);
 
@@ -371,18 +377,11 @@
          * Tries to inflate the items asynchronously and bind. Returns true on success or false if
          * async-binding is not supported in this case.
          */
-        private boolean inflateAsyncAndBind(List<ItemInfo> items, Executor executor) {
-            if (!enableWorkspaceInflation()) {
-                return false;
-            }
-            ItemInflater inflater = mCallbacks.getItemInflater();
-            if (inflater == null) {
-                return false;
-            }
-
+        private void inflateAsyncAndBind(
+                List<ItemInfo> items, @NonNull ItemInflater inflater, Executor executor) {
             if (mMyBindingId != mBgDataModel.lastBindId) {
                 Log.d(TAG, "Too many consecutive reloads, skipping obsolete view inflation");
-                return true;
+                return;
             }
 
             ModelWriter writer = mApp.getModel()
@@ -390,15 +389,10 @@
             List<Pair<ItemInfo, View>> bindItems = items.stream().map(i ->
                     Pair.create(i, inflater.inflateItem(i, writer, null))).toList();
             executeCallbacksTask(c -> c.bindInflatedItems(bindItems), executor);
-            return true;
         }
 
-        private void bindItemsInChunks(List<ItemInfo> workspaceItems, int chunkCount,
-                Executor executor) {
-            if (inflateAsyncAndBind(workspaceItems, executor)) {
-                return;
-            }
-
+        private void bindItemsInChunks(
+                List<ItemInfo> workspaceItems, int chunkCount, Executor executor) {
             // Bind the workspace items
             int count = workspaceItems.size();
             for (int i = 0; i < count; i += chunkCount) {
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index 2ec994e..f3769d5 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -215,11 +215,15 @@
                 && SessionCommitReceiver.isEnabled(mAppContext, getUserHandle(sessionInfo))
                 && verifySessionInfo(sessionInfo)
                 && !promiseIconAddedForId(sessionInfo.getSessionId())) {
-            FileLog.d(LOG, "Adding package name to install queue: "
-                    + sessionInfo.getAppPackageName());
+            // In case of unarchival, we do not want to add a workspace promise icon if one is
+            // not already present. For general app installations however, we do support it.
+            if (!Utilities.enableSupportForArchiving() || !sessionInfo.isUnarchival()) {
+                FileLog.d(LOG, "Adding package name to install queue: "
+                        + sessionInfo.getAppPackageName());
 
-            ItemInstallQueue.INSTANCE.get(mAppContext)
-                    .queueItem(sessionInfo.getAppPackageName(), getUserHandle(sessionInfo));
+                ItemInstallQueue.INSTANCE.get(mAppContext)
+                        .queueItem(sessionInfo.getAppPackageName(), getUserHandle(sessionInfo));
+            }
 
             getPromiseIconIds().add(sessionInfo.getSessionId());
             updatePromiseIconPrefs();
diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index ddc3cbb..4c9371d 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -347,8 +347,7 @@
     /** Return extra space revealed during predictive back animation. */
     @Px
     protected int getBottomOffsetPx() {
-        final int height = getMeasuredHeight();
-        return (int) ((height / PREDICTIVE_BACK_MIN_SCALE - height) / 2);
+        return (int) (getMeasuredHeight() * (1 - PREDICTIVE_BACK_MIN_SCALE));
     }
 
     /**
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
index 3b661d0..255a6d2 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
@@ -38,6 +38,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.function.Consumer;
 
 /**
  * A {@link PagedView} that displays widget recommendations in categories with dots as paged
@@ -45,11 +46,13 @@
  */
 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<>();
 
+    /** Callbacks to run when page changes */
+    private final List<Consumer<Integer>> mPageSwitchListeners = new ArrayList<>();
+
     @Nullable
     private OnLongClickListener mWidgetCellOnLongClickListener;
     @Nullable
@@ -84,6 +87,13 @@
     }
 
     /**
+     * Add a callback to run when the current displayed page changes.
+     */
+    public void addPageSwitchListener(Consumer<Integer> pageChangeListener) {
+        mPageSwitchListeners.add(pageChangeListener);
+    }
+
+    /**
      * Displays all the provided recommendations in a single table if they fit.
      *
      * @param recommendedWidgets list of widgets to be displayed in recommendation section.
@@ -104,7 +114,7 @@
 
         int displayedWidgets = maybeDisplayInTable(recommendedWidgets, deviceProfile,
                 availableWidth, cellPadding);
-        updateTitleAndIndicator();
+        updateTitleAndIndicator(/* requestedPage= */ 0);
         return displayedWidgets;
     }
 
@@ -119,16 +129,18 @@
      * @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
+     * @param requestedPage   page number to display initially.
      * @return number of recommendations that could fit in the available space.
      */
     public int setRecommendations(
             Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations,
-            DeviceProfile deviceProfile,
-            final @Px float availableHeight, final @Px int availableWidth,
-            final @Px int cellPadding) {
+            DeviceProfile deviceProfile, final @Px float availableHeight,
+            final @Px int availableWidth, final @Px int cellPadding, final int requestedPage) {
         this.mAvailableHeight = availableHeight;
         Context context = getContext();
-        mPageIndicator.setPauseScroll(true, deviceProfile.isTwoPanels);
+        // For purpose of recommendations section, we don't want paging dots to be halved in two
+        // pane display, so, we always provide isTwoPanels = "false".
+        mPageIndicator.setPauseScroll(/*pause=*/true, /*isTwoPanels=*/ false);
         clear();
 
         int displayedCategories = 0;
@@ -153,26 +165,33 @@
             }
         }
 
-        updateTitleAndIndicator();
-        mPageIndicator.setPauseScroll(false, deviceProfile.isTwoPanels);
+        updateTitleAndIndicator(requestedPage);
+        // For purpose of recommendations section, we don't want paging dots to be halved in two
+        // pane display, so, we always provide isTwoPanels = "false".
+        mPageIndicator.setPauseScroll(/*pause=*/false, /*isTwoPanels=*/false);
         return totalDisplayedWidgets;
     }
 
     private void clear() {
         mCategoryTitles.clear();
         removeAllViews();
+        setCurrentPage(0);
+        mPageIndicator.setActiveMarker(0);
     }
 
     /** Displays the page title and paging indicator if there are multiple pages. */
-    private void updateTitleAndIndicator() {
+    private void updateTitleAndIndicator(int requestedPage) {
         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));
+            if (requestedPage <= 0 || requestedPage >= getPageCount()) {
+                requestedPage = 0;
+            }
+            setCurrentPage(requestedPage);
+            mPageIndicator.setActiveMarker(requestedPage);
+            mRecommendationPageTitle.setText(mCategoryTitles.get(requestedPage));
         }
     }
 
@@ -180,7 +199,9 @@
     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()));
+            int nextPage = getNextPage();
+            mRecommendationPageTitle.setText(mCategoryTitles.get(nextPage));
+            mPageSwitchListeners.forEach(listener -> listener.accept(nextPage));
             super.notifyPageSwitchListener(prevPage);
         }
     }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 848f6fa..1629ca7 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -15,8 +15,6 @@
  */
 package com.android.launcher3.widget.picker;
 
-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;
@@ -30,6 +28,7 @@
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.Parcelable;
 import android.os.Process;
 import android.os.UserHandle;
@@ -67,6 +66,7 @@
 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;
@@ -81,7 +81,9 @@
 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Predicate;
 import java.util.stream.IntStream;
 
@@ -98,8 +100,11 @@
 
     // The widget recommendation table can easily take over the entire screen on devices with small
     // resolution or landscape on phone. This ratio defines the max percentage of content area that
-    // the table can display.
-    private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.75f;
+    // the table can display with respect to bottom sheet's height.
+    private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.45f;
+    private static final String RECOMMENDATIONS_SAVED_STATE_KEY =
+            "widgetsFullSheet:mRecommendationsCurrentPage";
+    private static final String SUPER_SAVED_STATE_KEY = "widgetsFullSheet:superHierarchyState";
     private final UserCache mUserCache;
     private final UserManagerState mUserManagerState = new UserManagerState();
     private final UserHandle mCurrentUser = Process.myUserHandle();
@@ -109,8 +114,13 @@
     protected final boolean mHasWorkProfile;
     // Number of recommendations displayed
     protected int mRecommendedWidgetsCount;
+    private List<WidgetItem> mRecommendedWidgets = new ArrayList<>();
+    private Map<WidgetRecommendationCategory, List<WidgetItem>> mRecommendedWidgetsMap =
+            new HashMap<>();
+    protected int mRecommendationsCurrentPage = 0;
     protected final SparseArray<AdapterHolder> mAdapters = new SparseArray();
-    @Nullable private ArrowTipView mLatestEducationalTip;
+    @Nullable
+    private ArrowTipView mLatestEducationalTip;
     private final OnLayoutChangeListener mLayoutChangeListenerToShowTips =
             new OnLayoutChangeListener() {
                 @Override
@@ -156,14 +166,19 @@
                 }
             };
 
-    @Px private final int mTabsHeight;
+    @Px
+    private final int mTabsHeight;
 
-    @Nullable private WidgetsRecyclerView mCurrentWidgetsRecyclerView;
-    @Nullable private WidgetsRecyclerView mCurrentTouchEventRecyclerView;
-    @Nullable PersonalWorkPagedView mViewPager;
+    @Nullable
+    private WidgetsRecyclerView mCurrentWidgetsRecyclerView;
+    @Nullable
+    private WidgetsRecyclerView mCurrentTouchEventRecyclerView;
+    @Nullable
+    PersonalWorkPagedView mViewPager;
     private boolean mIsInSearchMode;
     private boolean mIsNoWidgetsViewNeeded;
-    @Px protected int mMaxSpanPerRow;
+    @Px
+    protected int mMaxSpanPerRow;
     protected DeviceProfile mDeviceProfile;
 
     protected TextView mNoWidgetsView;
@@ -227,13 +242,16 @@
                 R.id.widget_recommendations_container);
         mWidgetRecommendationsView = mSearchScrollView.findViewById(
                 R.id.widget_recommendations_view);
+        // To save the currently displayed page, so that, it can be requested when rebinding
+        // recommendations with different size constraints.
+        mWidgetRecommendationsView.addPageSwitchListener(
+                newPage -> mRecommendationsCurrentPage = newPage);
         mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
         mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
         mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
 
         mHeaderTitle = mSearchScrollView.findViewById(R.id.title);
 
-        onRecommendedWidgetsBound();
         onWidgetsBound();
         setUpEducationViewsIfNeeded();
     }
@@ -354,7 +372,6 @@
         super.onAttachedToWindow();
         LauncherAppState.getInstance(mActivityContext).getModel()
                 .refreshAndBindWidgetsAndShortcuts(null);
-        onRecommendedWidgetsBound();
     }
 
     @Override
@@ -582,16 +599,26 @@
         }
 
         if (enableCategorizedWidgetSuggestions()) {
+            // We avoid applying new recommendations when some are already displayed.
+            if (mRecommendedWidgetsMap.isEmpty()) {
+                mRecommendedWidgetsMap =
+                        mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets();
+            }
             mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
-                    mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets(),
+                    mRecommendedWidgetsMap,
                     mDeviceProfile,
                     /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
                     /* availableWidth= */ mMaxSpanPerRow,
-                    /* cellPadding= */ mWidgetCellHorizontalPadding
+                    /* cellPadding= */ mWidgetCellHorizontalPadding,
+                    /* requestedPage= */ mRecommendationsCurrentPage
             );
         } else {
+            if (mRecommendedWidgets.isEmpty()) {
+                mRecommendedWidgets =
+                        mActivityContext.getPopupDataProvider().getRecommendedWidgets();
+            }
             mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
-                    mActivityContext.getPopupDataProvider().getRecommendedWidgets(),
+                    mRecommendedWidgets,
                     mDeviceProfile,
                     /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
                     /* availableWidth= */ mMaxSpanPerRow,
@@ -604,23 +631,8 @@
 
     @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);
+        return (mDeviceProfile.heightPx - mDeviceProfile.bottomSheetTopPadding)
+                * getRecommendationSectionHeightRatio();
     }
 
     /** b/209579563: "Widgets" header should be focused first. */
@@ -629,12 +641,12 @@
         return mHeaderTitle;
     }
 
+    /**
+     * Ratio of recommendations section with respect to bottom sheet's height on scale of 0 to 1.
+     */
     @Px
-    protected float getMaxTableHeight(@Px float noWidgetsViewHeight) {
-        return (mContent.getMeasuredHeight()
-                - mTabsHeight - getHeaderViewHeight()
-                - noWidgetsViewHeight)
-                * RECOMMENDATION_TABLE_HEIGHT_RATIO;
+    protected float getRecommendationSectionHeightRatio() {
+        return RECOMMENDATION_TABLE_HEIGHT_RATIO;
     }
 
     private void open(boolean animate) {
@@ -705,6 +717,27 @@
         return sheet;
     }
 
+    @Override
+    public void saveHierarchyState(SparseArray<Parcelable> sparseArray) {
+        Bundle bundle = new Bundle();
+        // With widget picker open, when we open shade to switch theme, Launcher re-creates the
+        // picker and calls save/restore hierarchy state. We save the state of recommendations
+        // across those updates.
+        bundle.putInt(RECOMMENDATIONS_SAVED_STATE_KEY, mRecommendationsCurrentPage);
+        SparseArray<Parcelable> superState = new SparseArray<>();
+        super.saveHierarchyState(superState);
+        bundle.putSparseParcelableArray(SUPER_SAVED_STATE_KEY, superState);
+        sparseArray.put(0, bundle);
+    }
+
+    @Override
+    public void restoreHierarchyState(SparseArray<Parcelable> sparseArray) {
+        Bundle state = (Bundle) sparseArray.get(0);
+        mRecommendationsCurrentPage = state.getInt(
+                RECOMMENDATIONS_SAVED_STATE_KEY, /*defaultValue=*/0);
+        super.restoreHierarchyState(state.getSparseParcelableArray(SUPER_SAVED_STATE_KEY));
+    }
+
     private static int getWidgetSheetId(BaseActivity activity) {
         boolean isTwoPane = (activity.getDeviceProfile().isTablet
                 // Enables two pane picker for tablets in all orientations when the
@@ -793,7 +826,7 @@
 
     /** private the height, in pixel, + the vertical margins of a given view. */
     protected static int measureHeightWithVerticalMargins(View view) {
-        if (view.getVisibility() != VISIBLE) {
+        if (view == null || view.getVisibility() != VISIBLE) {
             return 0;
         }
         MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
@@ -828,6 +861,7 @@
             saveHierarchyState(widgetsState);
             handleClose(false);
             WidgetsFullSheet sheet = show(BaseActivity.fromContext(getContext()), false);
+            sheet.restoreRecommendations(mRecommendedWidgets, mRecommendedWidgetsMap);
             sheet.restoreHierarchyState(widgetsState);
             sheet.restorePreviousAdapterHolderType(getCurrentAdapterHolderType());
         } else if (!isTwoPane()) {
@@ -838,6 +872,12 @@
         mDeviceProfile = dp;
     }
 
+    private void restoreRecommendations(List<WidgetItem> recommendedWidgets,
+            Map<WidgetRecommendationCategory, List<WidgetItem>> recommendedWidgetsMap) {
+        mRecommendedWidgets = recommendedWidgets;
+        mRecommendedWidgetsMap = recommendedWidgetsMap;
+    }
+
     /**
      * Indicates if layout should be re-created on device profile change - so that a different
      * layout can be displayed.
@@ -887,7 +927,8 @@
         }
     }
 
-    @Nullable private View getViewToShowEducationTip() {
+    @Nullable
+    private View getViewToShowEducationTip() {
         if (mWidgetRecommendationsContainer.getVisibility() == VISIBLE) {
             return mWidgetRecommendationsView.getViewForEducationTip();
         }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index c3bb993..c60bca0 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -17,6 +17,10 @@
 
 import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
 import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
+import static com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER;
+import static com.android.launcher3.UtilitiesKt.CLIP_TO_PADDING_FALSE_MODIFIER;
+import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree;
+import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree;
 
 import android.content.Context;
 import android.graphics.Outline;
@@ -27,6 +31,7 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewOutlineProvider;
+import android.view.ViewParent;
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 import android.widget.ScrollView;
@@ -57,13 +62,19 @@
     private static final int MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 395;
     private static final String SUGGESTIONS_PACKAGE_NAME = "widgets_list_suggestions_entry";
 
+    // This ratio defines the max percentage of content area that the recommendations can display
+    // with respect to the bottom sheet's height.
+    private static final float RECOMMENDATION_SECTION_HEIGHT_RATIO_TWO_PANE = 0.75f;
     private FrameLayout mSuggestedWidgetsContainer;
     private WidgetsListHeader mSuggestedWidgetsHeader;
     private PackageUserKey mSuggestedWidgetsPackageUserKey;
+    private View mPrimaryWidgetListView;
     private LinearLayout mRightPane;
 
     private ScrollView mRightPaneScrollView;
     private WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder;
+
+    private boolean mOldIsBackSwipeProgressing;
     private int mActivePage = -1;
     private PackageUserKey mSelectedHeader;
 
@@ -118,14 +129,23 @@
         mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
         mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
         mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
+        // To save the currently displayed page, so that, it can be requested when rebinding
+        // recommendations with different size constraints.
+        mWidgetRecommendationsView.addPageSwitchListener(
+                newPage -> mRecommendationsCurrentPage = newPage);
 
         mHeaderTitle = mContent.findViewById(R.id.title);
         mRightPane = mContent.findViewById(R.id.right_pane);
         mRightPane.setOutlineProvider(mViewOutlineProviderRightPane);
         mRightPaneScrollView = mContent.findViewById(R.id.right_pane_scroll_view);
         mRightPaneScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
+        mRightPaneScrollView.setOutlineProvider(mViewOutlineProvider);
+        mRightPaneScrollView.setClipToOutline(true);
 
-        onRecommendedWidgetsBound();
+        mPrimaryWidgetListView = findViewById(R.id.primary_widgets_list_view);
+        mPrimaryWidgetListView.setOutlineProvider(mViewOutlineProvider);
+        mPrimaryWidgetListView.setClipToOutline(true);
+
         onWidgetsBound();
         setUpEducationViewsIfNeeded();
 
@@ -134,6 +154,27 @@
     }
 
     @Override
+    protected void onScaleProgressChanged() {
+        super.onScaleProgressChanged();
+        boolean isBackSwipeProgressing = mSlideInViewScale.value > 0;
+        if (isBackSwipeProgressing == mOldIsBackSwipeProgressing) {
+            return;
+        }
+        mOldIsBackSwipeProgressing = isBackSwipeProgressing;
+        if (isBackSwipeProgressing) {
+            modifyAttributesOnViewTree(mPrimaryWidgetListView, (ViewParent) mContent,
+                    CLIP_CHILDREN_FALSE_MODIFIER);
+            modifyAttributesOnViewTree(mRightPaneScrollView,  (ViewParent) mContent,
+                    CLIP_CHILDREN_FALSE_MODIFIER, CLIP_TO_PADDING_FALSE_MODIFIER);
+        } else {
+            restoreAttributesOnViewTree(mPrimaryWidgetListView, mContent,
+                    CLIP_CHILDREN_FALSE_MODIFIER);
+            restoreAttributesOnViewTree(mRightPaneScrollView, mContent,
+                    CLIP_CHILDREN_FALSE_MODIFIER, CLIP_TO_PADDING_FALSE_MODIFIER);
+        }
+    }
+
+    @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         super.onLayout(changed, l, t, r, b);
         if (changed && mDeviceProfile.isTwoPanels && enableUnfoldedTwoPanePicker()) {
@@ -207,11 +248,13 @@
         String suggestionsRightPaneTitle = getContext().getString(
                 R.string.widget_picker_right_pane_accessibility_title, suggestionsHeaderTitle);
         packageItemInfo.title = suggestionsHeaderTitle;
+        // Suggestions may update at run time. The widgets count on suggestions doesn't add any
+        // value, so, we don't show the count.
         WidgetsListHeaderEntry widgetsListHeaderEntry = WidgetsListHeaderEntry.create(
                         packageItemInfo,
                         /*titleSectionName=*/ suggestionsHeaderTitle,
                         /*items=*/ mActivityContext.getPopupDataProvider().getRecommendedWidgets(),
-                        /*visibleWidgetsCount=*/ mRecommendedWidgetsCount)
+                        /*visibleWidgetsCount=*/ 0)
                 .withWidgetListShown();
 
         mSuggestedWidgetsHeader.applyFromItemInfoWithIcon(widgetsListHeaderEntry);
@@ -233,8 +276,8 @@
 
     @Override
     @Px
-    protected float getMaxTableHeight(@Px float noWidgetsViewHeight) {
-        return mContent.getMeasuredHeight() - measureHeightWithVerticalMargins(mHeaderTitle);
+    protected float getRecommendationSectionHeightRatio() {
+        return RECOMMENDATION_SECTION_HEIGHT_RATIO_TWO_PANE;
     }
 
     @Override
diff --git a/tests/Android.bp b/tests/Android.bp
index 24ae158..12cea1f 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -23,14 +23,23 @@
     srcs: [
         "src/**/*.java",
         "src/**/*.kt",
-        "multivalentTests/src/**/*.java",
-        "multivalentTests/src/**/*.kt",
+        ":launcher3-robo-src",
     ],
     exclude_srcs: [
         ":launcher-non-quickstep-tests-src",
     ],
 }
 
+filegroup {
+    name: "launcher3-robo-src",
+    // multivalentTests directory is a shared folder for not only robolectric converted test
+    // classes but also shared helper classes.
+    srcs: [
+        "multivalentTests/src/**/*.java",
+        "multivalentTests/src/**/*.kt",
+    ],
+}
+
 // Source code used for screenshot tests
 filegroup {
     name: "launcher-image-tests-helpers",
@@ -222,11 +231,8 @@
 android_robolectric_test {
     enabled: true,
     name: "Launcher3RoboTests",
-    // multivalentTests directory is a shared folder for not only robolectric converted test
-    // classes but also shared helper classes.
     srcs: [
-        "multivalentTests/src/**/*.java",
-        "multivalentTests/src/**/*.kt",
+        ":launcher3-robo-src",
 
         // Test util classes
         ":launcher-testing-helpers",
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 7cb7964..27dd2a9 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -388,6 +388,15 @@
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity-alias>
+        <activity android:name="com.android.launcher3.testcomponent.ExcludeFromRecentsTestActivity"
+            android:label="ExcludeFromRecentsTestActivity"
+            android:exported="true"
+            android:excludeFromRecents="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
 
         <!-- [b/197780098] Disable eager initialization of Jetpack libraries. -->
         <provider
diff --git a/tests/src/com/android/launcher3/UtilitiesKtTest.kt b/tests/src/com/android/launcher3/UtilitiesKtTest.kt
new file mode 100644
index 0000000..dd83871
--- /dev/null
+++ b/tests/src/com/android/launcher3/UtilitiesKtTest.kt
@@ -0,0 +1,118 @@
+/*
+ * 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
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER
+import com.android.launcher3.UtilitiesKt.CLIP_TO_PADDING_FALSE_MODIFIER
+import com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree
+import com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.views.WidgetsEduView
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UtilitiesKtTest {
+
+    val context: Context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+
+    private lateinit var rootView: WidgetsEduView
+    private lateinit var midView: ViewGroup
+    private lateinit var childView: View
+    @Before
+    fun setup() {
+        rootView =
+            LayoutInflater.from(context).inflate(R.layout.widgets_edu, null) as WidgetsEduView
+        midView = rootView.requireViewById(R.id.edu_view)
+        childView = rootView.requireViewById(R.id.edu_header)
+    }
+
+    @Test
+    fun set_clipChildren_false() {
+        assertThat(rootView.clipChildren).isTrue()
+        assertThat(midView.clipChildren).isTrue()
+
+        modifyAttributesOnViewTree(childView, rootView, CLIP_CHILDREN_FALSE_MODIFIER)
+
+        assertThat(rootView.clipChildren).isFalse()
+        assertThat(midView.clipChildren).isFalse()
+    }
+
+    @Test
+    fun restore_clipChildren_true() {
+        assertThat(rootView.clipChildren).isTrue()
+        assertThat(midView.clipChildren).isTrue()
+        modifyAttributesOnViewTree(childView, rootView, CLIP_CHILDREN_FALSE_MODIFIER)
+        assertThat(rootView.clipChildren).isFalse()
+        assertThat(midView.clipChildren).isFalse()
+
+        restoreAttributesOnViewTree(childView, rootView, CLIP_CHILDREN_FALSE_MODIFIER)
+
+        assertThat(rootView.clipChildren).isTrue()
+        assertThat(midView.clipChildren).isTrue()
+    }
+
+    @Test
+    fun restore_clipChildren_skipRestoreMidView() {
+        assertThat(rootView.clipChildren).isTrue()
+        assertThat(midView.clipChildren).isTrue()
+        rootView.clipChildren = false
+        modifyAttributesOnViewTree(childView, rootView, CLIP_CHILDREN_FALSE_MODIFIER)
+        assertThat(rootView.clipChildren).isFalse()
+        assertThat(midView.clipChildren).isFalse()
+
+        restoreAttributesOnViewTree(childView, rootView, CLIP_CHILDREN_FALSE_MODIFIER)
+
+        assertThat(rootView.clipChildren).isFalse()
+        assertThat(midView.clipChildren).isTrue()
+    }
+
+    @Test
+    fun set_clipToPadding_false() {
+        assertThat(rootView.clipToPadding).isTrue()
+        assertThat(midView.clipToPadding).isTrue()
+
+        modifyAttributesOnViewTree(childView, rootView, CLIP_TO_PADDING_FALSE_MODIFIER)
+
+        assertThat(rootView.clipToPadding).isFalse()
+        assertThat(midView.clipToPadding).isFalse()
+    }
+
+    @Test
+    fun restore_clipToPadding_true() {
+        assertThat(rootView.clipToPadding).isTrue()
+        assertThat(midView.clipToPadding).isTrue()
+        modifyAttributesOnViewTree(childView, rootView, CLIP_TO_PADDING_FALSE_MODIFIER)
+        assertThat(rootView.clipToPadding).isFalse()
+        assertThat(midView.clipToPadding).isFalse()
+
+        restoreAttributesOnViewTree(childView, rootView, CLIP_TO_PADDING_FALSE_MODIFIER)
+
+        assertThat(rootView.clipToPadding).isTrue()
+        assertThat(midView.clipToPadding).isTrue()
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index a1ff346..4e38847 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -589,6 +589,17 @@
                 false /* newTask */);
     }
 
+    /** Starts ExcludeFromRecentsTestActivity, which has excludeFromRecents="true". */
+    public static void startExcludeFromRecentsTestActivity() {
+        final String packageName = getAppPackageName();
+        final Intent intent = getInstrumentation().getContext().getPackageManager()
+                .getLaunchIntentForPackage(packageName);
+        intent.setComponent(new ComponentName(packageName,
+                "com.android.launcher3.testcomponent.ExcludeFromRecentsTestActivity"));
+        startIntent(intent, By.pkg(packageName).text("ExcludeFromRecentsTestActivity"),
+                false /* newTask */);
+    }
+
     private static void startIntent(Intent intent, BySelector selector, boolean newTask) {
         intent.addCategory(Intent.CATEGORY_LAUNCHER);
         if (newTask) {
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index bfff541..0e523c3 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -2452,7 +2452,7 @@
             int bottomBound = Math.min(
                     containerBounds.bottom,
                     getRealDisplaySize().y - systemGestureRegion.bottom);
-            int y = (bottomBound - containerBounds.top) / 2;
+            int y = (bottomBound + containerBounds.top) / 2;
             // Do not tap in the status bar.
             y = Math.max(y, systemGestureRegion.top);
 
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 1cfbf09..99da5c3 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -25,6 +25,7 @@
 import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.UiObject2;
@@ -259,6 +260,23 @@
     }
 
     /**
+     * Returns whether the given String is contained in this Task's contentDescription. Also returns
+     * true if both Strings are null.
+     *
+     * TODO(b/326565120): remove Nullable support once the bug causing it to be null is fixed.
+     */
+    public boolean containsContentDescription(@Nullable String expected) {
+        String actual = mTask.getContentDescription();
+        if (actual == null && expected == null) {
+            return true;
+        }
+        if (actual == null || expected == null) {
+            return false;
+        }
+        return actual.contains(expected);
+    }
+
+    /**
      * Enum used to specify  which task is retrieved when it is a split task.
      */
     public enum OverviewSplitTask {