Merge "Don't crop bubble flyout in overview" into main
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index 93d8d54..b299edf 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -61,4 +61,14 @@
     namespace: "launcher_overview"
     description: "Enables the non-overlapping layout for desktop windows in Overview mode."
     bug: "378011776"
+}
+
+flag {
+    name: "enable_use_top_visible_activity_for_exclude_from_recent_task"
+    namespace: "launcher_overview"
+    description: "Enables using the top visible activity for exclude from recent task instead of the activity indicies."
+    bug: "342627272"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
 }
\ No newline at end of file
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index c6e2d8c..80d8154 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -48,7 +48,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/quickstep/res/layout/overview_add_desktop_button.xml b/quickstep/res/layout/overview_add_desktop_button.xml
new file mode 100644
index 0000000..2333dd1
--- /dev/null
+++ b/quickstep/res/layout/overview_add_desktop_button.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.quickstep.views.AddDesktopButton
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:launcher="http://schemas.android.com/apgk/res-auto"
+    android:id="@+id/add_desktop_button"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:src="@drawable/ic_desktop_add"
+    android:padding="10dp" />
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index fd0243a..2ff9b18 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -111,10 +111,9 @@
     public boolean areDesktopTasksVisible() {
         boolean desktopTasksVisible = mVisibleDesktopTasksCount > 0;
         if (DEBUG) {
-            Log.d(TAG, "areDesktopTasksVisible: desktopVisible=" + desktopTasksVisible
-                    + " overview=" + mInOverviewState);
+            Log.d(TAG, "areDesktopTasksVisible: desktopVisible=" + desktopTasksVisible);
         }
-        return desktopTasksVisible && !mInOverviewState;
+        return desktopTasksVisible;
     }
 
     /**
@@ -219,12 +218,8 @@
                     + " currentValue=" + mInOverviewState);
         }
         if (overviewStateEnabled != mInOverviewState) {
-            final boolean wereDesktopTasksVisibleBefore = areDesktopTasksVisible();
             mInOverviewState = overviewStateEnabled;
             final boolean areDesktopTasksVisibleNow = areDesktopTasksVisible();
-            if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
-                notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
-            }
 
             if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
                 return;
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 1967dfd..306443e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -35,6 +35,8 @@
 import android.view.animation.Interpolator;
 import android.widget.HorizontalScrollView;
 import android.widget.TextView;
+import android.window.OnBackInvokedDispatcher;
+import android.window.WindowOnBackInvokedDispatcher;
 
 import androidx.annotation.LayoutRes;
 import androidx.annotation.NonNull;
@@ -109,6 +111,8 @@
 
     @Nullable private AnimatorSet mOpenAnimation;
 
+    private boolean mIsBackCallbackRegistered = false;
+
     @Nullable private KeyboardQuickSwitchViewController.ViewCallbacks mViewCallbacks;
 
     public KeyboardQuickSwitchView(@NonNull Context context) {
@@ -158,6 +162,34 @@
         mIsRtl = Utilities.isRtl(resources);
     }
 
+    private void registerOnBackInvokedCallback() {
+        OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
+
+        if (isOnBackInvokedCallbackEnabled(dispatcher)
+                && !mIsBackCallbackRegistered) {
+            dispatcher.registerOnBackInvokedCallback(
+                    OnBackInvokedDispatcher.PRIORITY_OVERLAY, mViewCallbacks.onBackInvokedCallback);
+            mIsBackCallbackRegistered = true;
+        }
+    }
+
+    private void unregisterOnBackInvokedCallback() {
+        OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
+
+        if (isOnBackInvokedCallbackEnabled(dispatcher)
+                && mIsBackCallbackRegistered) {
+            dispatcher.unregisterOnBackInvokedCallback(
+                    mViewCallbacks.onBackInvokedCallback);
+            mIsBackCallbackRegistered = false;
+        }
+    }
+
+    private boolean isOnBackInvokedCallbackEnabled(OnBackInvokedDispatcher dispatcher) {
+        return dispatcher instanceof WindowOnBackInvokedDispatcher
+                && ((WindowOnBackInvokedDispatcher) dispatcher).isOnBackInvokedCallbackEnabled()
+                && mViewCallbacks != null;
+    }
+
     private KeyboardQuickSwitchTaskView createAndAddTaskView(
             int index,
             boolean isFinalView,
@@ -277,6 +309,7 @@
                 new ViewTreeObserver.OnGlobalLayoutListener() {
                     @Override
                     public void onGlobalLayout() {
+                        registerOnBackInvokedCallback();
                         animateOpen(currentFocusIndexOverride);
 
                         getViewTreeObserver().removeOnGlobalLayoutListener(this);
@@ -293,6 +326,9 @@
     }
 
     void resetViewCallbacks() {
+        // Unregister the back invoked callback after the view is closed and before the
+        // mViewCallbacks is reset.
+        unregisterOnBackInvokedCallback();
         mViewCallbacks = null;
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index e623b21..3114bc8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -25,6 +25,7 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.animation.AnimationUtils;
+import android.window.OnBackInvokedCallback;
 import android.window.RemoteTransition;
 
 import androidx.annotation.NonNull;
@@ -331,6 +332,7 @@
     }
 
     class ViewCallbacks {
+        public final OnBackInvokedCallback onBackInvokedCallback = () -> closeQuickSwitchView(true);
 
         boolean onKeyUp(int keyCode, KeyEvent event, boolean isRTL, boolean allowTraversal) {
             if (keyCode != KeyEvent.KEYCODE_TAB
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index f0129b4..7d75286 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -69,14 +69,17 @@
     public static final int ALL_APPS_PAGE_PROGRESS_INDEX = 1;
     public static final int WIDGETS_PAGE_PROGRESS_INDEX = 2;
     public static final int SYSUI_SURFACE_PROGRESS_INDEX = 3;
+    public static final int LAUNCHER_PAUSE_PROGRESS_INDEX = 4;
 
-    public static final int DISPLAY_PROGRESS_COUNT = 4;
+    public static final int DISPLAY_PROGRESS_COUNT = 5;
 
     private final AnimatedFloat mTaskbarInAppDisplayProgress = new AnimatedFloat(
             this::onInAppDisplayProgressChanged);
     private final MultiPropertyFactory<AnimatedFloat> mTaskbarInAppDisplayProgressMultiProp =
             new MultiPropertyFactory<>(mTaskbarInAppDisplayProgress,
                     AnimatedFloat.VALUE, DISPLAY_PROGRESS_COUNT, Float::max);
+    private final AnimatedFloat mLauncherPauseProgress = new AnimatedFloat(
+            this::onLauncherPauseProgressUpdate);
 
     private final QuickstepLauncher mLauncher;
     private final HomeVisibilityState mHomeState;
@@ -499,7 +502,8 @@
                 "MINUS_ONE_PAGE_PROGRESS_INDEX",
                 "ALL_APPS_PAGE_PROGRESS_INDEX",
                 "WIDGETS_PAGE_PROGRESS_INDEX",
-                "SYSUI_SURFACE_PROGRESS_INDEX");
+                "SYSUI_SURFACE_PROGRESS_INDEX",
+                "LAUNCHER_PAUSE_PROGRESS_INDEX");
 
         mTaskbarLauncherStateController.dumpLogs(prefix + "\t", pw);
     }
@@ -529,4 +533,39 @@
             mLauncher.getWorkspace().onOverlayScrollChanged(0);
         }
     }
+
+    /**
+     * Called when Launcher Activity resumed while staying at home.
+     * <p>
+     * Shift nav buttons up to at-home position.
+     */
+    public void onLauncherResume() {
+        mLauncherPauseProgress.animateToValue(0.0f).start();
+    }
+
+    /**
+     * Called when Launcher Activity paused while staying at home.
+     * <p>
+     * To avoid UI clash between taskbar & bottom sheet, shift nav buttons down to in-app position.
+     */
+    public void onLauncherPause() {
+        mLauncherPauseProgress.animateToValue(1.0f).start();
+    }
+
+    /**
+     * On launcher stop, avoid animating taskbar & overriding pre-existing animations.
+     */
+    public void onLauncherStop() {
+        mLauncherPauseProgress.cancelAnimation();
+        mLauncherPauseProgress.updateValue(0.0f);
+    }
+
+    private void onLauncherPauseProgressUpdate() {
+        // If we are not aligned with hotseat, setting this will clobber the 3 button nav position.
+        // So in that case, treat the progress as 0 instead.
+        float pauseProgress = isIconAlignedWithHotseat() ? mLauncherPauseProgress.value : 0;
+        onTaskbarInAppDisplayProgressUpdate(pauseProgress, LAUNCHER_PAUSE_PROGRESS_INDEX);
+    }
+
+
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt b/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt
index 8d1f4f5..032eb51 100644
--- a/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt
@@ -32,7 +32,8 @@
 import com.android.launcher3.views.ActivityContext
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.SystemUiProxy
-import com.android.quickstep.util.GroupTask
+import com.android.quickstep.util.DesktopTask
+import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.android.wm.shell.shared.multiinstance.ManageWindowsViewContainer
 import java.util.Collections
@@ -60,20 +61,29 @@
     private val recentsModel = RecentsModel.INSTANCE[controllers.taskbarActivityContext]
 
     override fun onClick(v: View?) {
-        val filter =
-            Predicate<GroupTask> { task: GroupTask? ->
-                task != null && task.task1.key.packageName == itemInfo?.getTargetPackage()
-            }
-        recentsModel.getTasks(
-            { tasks: List<GroupTask> ->
-                // Since fetching thumbnails is asynchronous, use this set to gate until the tasks
-                // are ready to display
-                val pendingTaskIds =
-                    Collections.synchronizedSet(tasks.map { it.task1.key.id }.toMutableSet())
-                createAndShowTaskShortcutView(tasks, pendingTaskIds)
-            },
-            filter,
-        )
+        val targetPackage = itemInfo?.getTargetPackage()
+        val targetUserId = itemInfo?.user?.identifier
+        val isTargetPackageTask: (Task) -> Boolean = { task ->
+            task.key?.packageName == targetPackage && task.key.userId == targetUserId
+        }
+
+        recentsModel.getTasks { tasks ->
+            val desktopTask = tasks.filterIsInstance<DesktopTask>().firstOrNull()
+            val packageDesktopTasks =
+                (desktopTask?.tasks ?: emptyList()).filter(isTargetPackageTask)
+            val nonDesktopPackageTasks =
+                tasks.filter { isTargetPackageTask(it.task1) }.map { it.task1 }
+
+            // Add tasks from the fetched tasks, deduplicating by task ID
+            val packageTasks =
+                (packageDesktopTasks + nonDesktopPackageTasks).distinctBy { it.key.id }
+
+            // Since fetching thumbnails is asynchronous, use `awaitedTaskIds` to gate until the
+            // tasks are ready to display
+            val awaitedTaskIds = packageTasks.map { it.key.id }.toMutableSet()
+
+            createAndShowTaskShortcutView(packageTasks, awaitedTaskIds)
+        }
     }
 
     /**
@@ -83,25 +93,20 @@
      * thumbnails are processed, it creates a [TaskbarShortcutManageWindowsView] with the collected
      * thumbnails and positions it appropriately.
      */
-    private fun createAndShowTaskShortcutView(
-        tasks: List<GroupTask?>,
-        pendingTaskIds: MutableSet<Int>,
-    ) {
+    private fun createAndShowTaskShortcutView(tasks: List<Task>, pendingTaskIds: MutableSet<Int>) {
         val taskList = arrayListOf<Pair<Int, Bitmap?>>()
-        tasks.forEach { groupTask ->
-            groupTask?.task1?.let { task ->
-                recentsModel.thumbnailCache.getThumbnailInBackground(task) {
-                    thumbnailData: ThumbnailData ->
-                    pendingTaskIds.remove(task.key.id)
-                    // Add the current pair of task id and ThumbnailData to the list of all tasks
-                    if (thumbnailData.thumbnail != null) {
-                        taskList.add(task.key.id to thumbnailData.thumbnail)
-                    }
 
-                    // If the set is empty, all thumbnails have been fetched
-                    if (pendingTaskIds.isEmpty() && taskList.isNotEmpty()) {
-                        createAndPositionTaskbarShortcut(taskList)
-                    }
+        tasks.forEach { task ->
+            recentsModel.thumbnailCache.getThumbnailInBackground(task) {
+                thumbnailData: ThumbnailData ->
+                pendingTaskIds.remove(task.key.id)
+                // Add the current pair of task id and ThumbnailData to the list of all tasks
+                if (thumbnailData.thumbnail != null) {
+                    taskList.add(task.key.id to thumbnailData.thumbnail)
+                }
+                // If the set is empty, all thumbnails have been fetched
+                if (pendingTaskIds.isEmpty() && taskList.isNotEmpty()) {
+                    createAndPositionTaskbarShortcut(taskList)
                 }
             }
         }
@@ -113,13 +118,13 @@
     private fun createAndPositionTaskbarShortcut(taskList: ArrayList<Pair<Int, Bitmap?>>) {
         val onIconClickListener =
             ({ taskId: Int? ->
-                taskbarShortcutAllWindowsView.removeFromContainer()
+                taskbarShortcutAllWindowsView.animateClose()
                 if (taskId != null) {
                     SystemUiProxy.INSTANCE.get(target).showDesktopApp(taskId, null)
                 }
             })
 
-        val onOutsideClickListener = { taskbarShortcutAllWindowsView.removeFromContainer() }
+        val onOutsideClickListener = { taskbarShortcutAllWindowsView.animateClose() }
 
         taskbarShortcutAllWindowsView =
             TaskbarShortcutManageWindowsView(
@@ -172,6 +177,7 @@
         init {
             createAndShowMenuView(snapshotList, onIconClickListener, onOutsideClickListener)
             taskbarOverlayContext.dragLayer.addTouchController(this)
+            animateOpen()
         }
 
         /** Adds the carousel menu to the taskbar overlay drag layer */
@@ -245,7 +251,7 @@
                     it.action == MotionEvent.ACTION_DOWN &&
                         !taskbarOverlayContext.dragLayer.isEventOverView(menuView.rootView, it)
                 ) {
-                    removeFromContainer()
+                    animateClose()
                 }
             }
             return false
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 0613980..70cb7ce 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -448,6 +448,8 @@
         onNavButtonsDarkIntensityChanged(sharedState.navButtonsDarkIntensity);
         onNavigationBarLumaSamplingEnabled(sharedState.mLumaSamplingDisplayId,
                 sharedState.mIsLumaSamplingEnabled);
+        setWallpaperVisible(sharedState.wallpaperVisible);
+        onTransitionModeUpdated(sharedState.barMode, true /* checkBarModes */);
 
         if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
             // W/ the flag not set this entire class gets re-created, which resets the value of
@@ -1319,9 +1321,25 @@
         } else if (tag instanceof TaskItemInfo info) {
             RemoteTransition remoteTransition = canUnminimizeDesktopTask(info.getTaskId())
                     ? createUnminimizeRemoteTransition() : null;
-            UI_HELPER_EXECUTOR.execute(() ->
-                    SystemUiProxy.INSTANCE.get(this).showDesktopApp(
-                            info.getTaskId(), remoteTransition));
+
+            if (areDesktopTasksVisible() && recents != null) {
+                TaskView taskView = recents.getTaskViewByTaskId(info.getTaskId());
+                if (taskView == null) return;
+                RunnableList runnableList = taskView.launchWithAnimation();
+                if (runnableList != null) {
+                    runnableList.add(() ->
+                            // wrapped it in runnable here since we need the post for DW to be
+                            // ready. if we don't other DW will be gone and only the launched task
+                            // will show.
+                            UI_HELPER_EXECUTOR.execute(() ->
+                                    SystemUiProxy.INSTANCE.get(this).showDesktopApp(
+                                            info.getTaskId(), remoteTransition)));
+                }
+            } else {
+                UI_HELPER_EXECUTOR.execute(() ->
+                        SystemUiProxy.INSTANCE.get(this).showDesktopApp(
+                                info.getTaskId(), remoteTransition));
+            }
             mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(
                     /* stash= */ true);
         } else if (tag instanceof WorkspaceItemInfo) {
@@ -1565,7 +1583,17 @@
                                                 .launchAppPair((AppPairIcon) launchingIconView,
                                                         -1 /*cuj*/)));
                     } else {
-                        startItemInfoActivity(itemInfos.get(0), foundTask);
+                        if (areDesktopTasksVisible()) {
+                            RunnableList runnableList = recents.launchDesktopTaskView();
+                            // Wrapping it in runnable so we post after DW is ready for the app
+                            // launch.
+                            if (runnableList != null) {
+                                runnableList.add(() -> UI_HELPER_EXECUTOR.execute(
+                                        () -> startItemInfoActivity(itemInfos.get(0), foundTask)));
+                            }
+                        } else {
+                            startItemInfoActivity(itemInfos.get(0), foundTask);
+                        }
                     }
                 }
         );
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
index bdc7f92..444c356 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
@@ -49,6 +49,8 @@
     public static final int FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS = 1 << 6;
     // User has multi instance window open.
     public static final int FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN = 1 << 7;
+    // User has taskbar overflow open.
+    public static final int FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW = 1 << 8;
 
     @IntDef(flag = true, value = {
             FLAG_AUTOHIDE_SUSPEND_FULLSCREEN,
@@ -59,6 +61,7 @@
             FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR,
             FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
             FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
+            FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AutohideSuspendFlag {}
@@ -138,6 +141,8 @@
                 "FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR");
         appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
                 "FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN");
+        appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW,
+                "FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW");
         return str.toString();
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 2db7961..250e33a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -1018,7 +1018,12 @@
 
         @Override
         public void onRecentsAnimationFinished(RecentsAnimationController controller) {
-            endGestureStateOverride(!controller.getFinishTargetIsLauncher(), false /*canceled*/);
+            endGestureStateOverride(!controller.getFinishTargetIsLauncher(),
+                    controller.getLauncherIsVisibleAtFinish(), false /*canceled*/);
+        }
+
+        private void endGestureStateOverride(boolean finishedToApp, boolean canceled) {
+            endGestureStateOverride(finishedToApp, finishedToApp, canceled);
         }
 
         /**
@@ -1028,11 +1033,13 @@
          *
          * @param finishedToApp {@code true} if the recents animation finished to showing an app and
          *                      not workspace or overview
+         * @param launcherIsVisible {code true} if launcher is visible at finish
          * @param canceled      {@code true} if the recents animation was canceled instead of
          *                      finishing
          *                      to completion
          */
-        private void endGestureStateOverride(boolean finishedToApp, boolean canceled) {
+        private void endGestureStateOverride(boolean finishedToApp, boolean launcherIsVisible,
+                boolean canceled) {
             mCallbacks.removeListener(this);
             mTaskBarRecentsAnimationListener = null;
             ((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(null);
@@ -1041,18 +1048,27 @@
                 mSkipNextRecentsAnimEnd = false;
                 return;
             }
-            updateStateForUserFinishedToApp(finishedToApp);
+            updateStateForUserFinishedToApp(finishedToApp, launcherIsVisible);
         }
     }
 
     /**
+     * @see #updateStateForUserFinishedToApp(boolean, boolean)
+     */
+    private void updateStateForUserFinishedToApp(boolean finishedToApp) {
+        updateStateForUserFinishedToApp(finishedToApp, !finishedToApp);
+    }
+
+    /**
      * Updates the visible state immediately to ensure a seamless handoff.
      *
      * @param finishedToApp True iff user is in an app.
+     * @param launcherIsVisible True iff launcher is still visible (ie. transparent app)
      */
-    private void updateStateForUserFinishedToApp(boolean finishedToApp) {
+    private void updateStateForUserFinishedToApp(boolean finishedToApp,
+            boolean launcherIsVisible) {
         // Update the visible state immediately to ensure a seamless handoff
-        boolean launcherVisible = !finishedToApp;
+        boolean launcherVisible = !finishedToApp || launcherIsVisible;
         updateStateForFlag(FLAG_TRANSITION_TO_VISIBLE, false);
         updateStateForFlag(FLAG_VISIBLE, launcherVisible);
         applyState();
@@ -1061,7 +1077,7 @@
         if (DEBUG) {
             Log.d(TAG, "endGestureStateOverride - FLAG_IN_APP: " + finishedToApp);
         }
-        controller.updateStateForFlag(FLAG_IN_APP, finishedToApp);
+        controller.updateStateForFlag(FLAG_IN_APP, finishedToApp && !launcherIsVisible);
         controller.applyState();
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index 2e0bae5..abf35a2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.taskbar;
 
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
 import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition;
 
@@ -309,9 +310,7 @@
      */
     SystemShortcut.Factory<BaseTaskbarContext> createNewWindowShortcutFactory() {
         return (context, itemInfo, originalView) -> {
-            ComponentKey key = itemInfo.getComponentKey();
-            AppInfo app = getApp(key);
-            if (app != null && app.supportsMultiInstance()) {
+            if (shouldShowMultiInstanceOptions(itemInfo)) {
                 return new NewWindowTaskbarShortcut<>(context, itemInfo, originalView);
             }
             return null;
@@ -325,9 +324,7 @@
      */
     public SystemShortcut.Factory<BaseTaskbarContext> createManageWindowsShortcutFactory() {
         return (context, itemInfo, originalView) -> {
-            ComponentKey key = itemInfo.getComponentKey();
-            AppInfo app = getApp(key);
-            if (app != null && app.supportsMultiInstance()) {
+            if (shouldShowMultiInstanceOptions(itemInfo)) {
                 return new ManageWindowsTaskbarShortcut<>(context, itemInfo, originalView,
                         mControllers);
             }
@@ -336,6 +333,16 @@
     }
 
     /**
+     * Determines whether to show multi-instance options for a given item.
+     */
+    private boolean shouldShowMultiInstanceOptions(ItemInfo itemInfo) {
+        ComponentKey key = itemInfo.getComponentKey();
+        AppInfo app = getApp(key);
+        return app != null && app.supportsMultiInstance()
+                && itemInfo.container != CONTAINER_ALL_APPS;
+    }
+
+    /**
      * A single menu item ("Split left," "Split right," or "Split top") that executes a split
      * from the taskbar, as if the user performed a drag and drop split.
      * Includes an onClick method that initiates the actual split.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index 3d57de4..a059b22 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -326,8 +326,8 @@
     }
 
     /**
-     * Returns the hotseat items updated so that any item that points to a package with a running
-     * task also references that task.
+     * Returns the hotseat items updated so that any item that points to a package+user with a
+     * running task also references that task.
      */
     private fun updateHotseatItemsFromRunningTasks(
         groupTasks: List<GroupTask>,
@@ -338,8 +338,10 @@
                 itemInfo
             } else {
                 val foundTask =
-                    groupTasks.find { task -> task.task1.key.packageName == itemInfo.targetPackage }
-                        ?: return@map itemInfo
+                    groupTasks.find { task ->
+                        task.task1.key.packageName == itemInfo.targetPackage &&
+                            task.task1.key.userId == itemInfo.user.identifier
+                    } ?: return@map itemInfo
                 TaskItemInfo(foundTask.task1.key.id, itemInfo as WorkspaceItemInfo)
             }
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
index 4d77ab2..f2bd4d0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_LONG_PRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP;
+import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
@@ -180,6 +181,9 @@
         if (mTaskbarView.getTaskbarOverflowView() != null) {
             mTaskbarView.getTaskbarOverflowView().setIsActive(
                     !mTaskbarView.getTaskbarOverflowView().getIsActive());
+            mControllers.taskbarAutohideSuspendController
+                    .updateFlag(FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW,
+                            mTaskbarView.getTaskbarOverflowView().getIsActive());
         }
         mControllers.keyboardQuickSwitchController.toggleQuickSwitchViewForTaskbar(
                 mControllers.taskbarViewController.getTaskIdsForPinnedApps(),
@@ -190,6 +194,8 @@
         if (mTaskbarView.getTaskbarOverflowView() != null) {
             mTaskbarView.getTaskbarOverflowView().setIsActive(false);
         }
+        mControllers.taskbarAutohideSuspendController.updateFlag(
+                FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW, false);
     }
 
     private float getDividerCenterX() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 987937e..5b3c233 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -405,8 +405,12 @@
             mBubbleBarViewController.showOverflow(true);
         }
 
-        // Adds and removals have happened, update visibility before any other visual changes.
-        mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty());
+        // Update the visibility if this is the initial state or if there are no bubbles.
+        // If this is the initial bubble, the bubble bar will become visible as part of the
+        // animation.
+        if (update.initialState || mBubbles.isEmpty()) {
+            mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty());
+        }
         mBubbleStashedHandleViewController.ifPresent(
                 controller -> controller.setHiddenForBubbles(mBubbles.isEmpty()));
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index df448d0..02dda35 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -170,7 +170,8 @@
                 mBubbleBarContainer, createFlyoutPositioner(), createFlyoutCallbacks());
         mBubbleBarViewAnimator = new BubbleBarViewAnimator(
                 mBarView, mBubbleStashController, mBubbleBarFlyoutController,
-                createBubbleBarParentViewController(), mBubbleBarController::showExpandedView);
+                createBubbleBarParentViewController(), mBubbleBarController::showExpandedView,
+                () -> setHiddenForBubbles(false));
         mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider;
         onBubbleBarConfigurationChanged(/* animate= */ false);
         mActivity.addOnDeviceProfileChangeListener(
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index f5a6655..fdd04eb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -42,6 +42,7 @@
     private val bubbleBarFlyoutController: BubbleBarFlyoutController,
     private val bubbleBarParentViewHeightUpdateNotifier: BubbleBarParentViewHeightUpdateNotifier,
     private val onExpanded: Runnable,
+    private val onBubbleBarVisible: Runnable,
     private val scheduler: Scheduler = HandlerScheduler(bubbleBarView),
 ) {
 
@@ -397,6 +398,7 @@
         // prepare the bubble bar for the animation
         bubbleBarView.translationY = bubbleBarView.height.toFloat()
         bubbleBarView.visibility = VISIBLE
+        onBubbleBarVisible.run()
         bubbleBarView.alpha = 1f
         bubbleBarView.scaleX = 1f
         bubbleBarView.scaleY = 1f
@@ -511,23 +513,21 @@
 
     /** Interrupts the animation due to touching the bubble bar or flyout. */
     fun interruptForTouch() {
+        animatingBubble?.hideAnimation?.let { scheduler.cancel(it) }
         PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning()
         bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
         cancelFlyout()
-        val hideAnimation = animatingBubble?.hideAnimation ?: return
-        scheduler.cancel(hideAnimation)
-        bubbleBarView.relativePivotY = 1f
+        resetBubbleBarPropertiesOnInterrupt()
         clearAnimatingBubble()
     }
 
     /** Notifies the animator that the taskbar area was touched during an animation. */
     fun onStashStateChangingWhileAnimating() {
+        animatingBubble?.hideAnimation?.let { scheduler.cancel(it) }
         cancelFlyout()
-        val hideAnimation = animatingBubble?.hideAnimation ?: return
-        scheduler.cancel(hideAnimation)
         clearAnimatingBubble()
         bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
-        bubbleBarView.relativePivotY = 1f
+        resetBubbleBarPropertiesOnInterrupt()
         bubbleStashController.onNewBubbleAnimationInterrupted(
             /* isStashed= */ bubbleBarView.alpha == 0f,
             bubbleBarView.translationY,
@@ -541,7 +541,7 @@
         scheduler.cancel(hideAnimation)
         animatingBubble = null
         bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
-        bubbleBarView.relativePivotY = 1f
+        resetBubbleBarPropertiesOnInterrupt()
         // stash the bubble bar since the IME is now visible
         bubbleStashController.onNewBubbleAnimationInterrupted(
             /* isStashed= */ true,
@@ -679,6 +679,12 @@
         bubbleStashController.showBubbleBarImmediate()
     }
 
+    private fun resetBubbleBarPropertiesOnInterrupt() {
+        bubbleBarView.relativePivotY = 1f
+        bubbleBarView.scaleX = 1f
+        bubbleBarView.scaleY = 1f
+    }
+
     private fun <T> PhysicsAnimator<T>?.cancelIfRunning() {
         if (this?.isRunning() == true) cancel()
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index cc51adc..5cb6e86 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -801,6 +801,10 @@
         if (mLauncherUnfoldAnimationController != null) {
             mLauncherUnfoldAnimationController.onResume();
         }
+
+        if (mTaskbarUIController != null && FeatureFlags.enableHomeTransitionListener()) {
+            mTaskbarUIController.onLauncherResume();
+        }
     }
 
     @Override
@@ -821,6 +825,18 @@
                         .playPlaceholderDismissAnim(this, LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
             }
         }
+
+        if (mTaskbarUIController != null && FeatureFlags.enableHomeTransitionListener()) {
+            mTaskbarUIController.onLauncherPause();
+        }
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        if (mTaskbarUIController != null && FeatureFlags.enableHomeTransitionListener()) {
+            mTaskbarUIController.onLauncherStop();
+        }
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index a006198..fbc8d6a 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -80,6 +80,8 @@
 import android.os.SystemClock;
 import android.util.Log;
 import android.util.Pair;
+import android.util.TimeUtils;
+import android.view.Choreographer;
 import android.view.MotionEvent;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
@@ -1734,13 +1736,30 @@
     }
 
     private void handOffAnimation(PointF velocityPxPerMs) {
-        if (!TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()
-                || mRecentsAnimationController == null) {
+        if (!TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()) {
+            return;
+        }
+
+        // This function is not guaranteed to be called inside a frame. We try to access the frame
+        // time immediately, but if we're not inside a frame we must post a callback to be run at
+        // the beginning of the next frame.
+        try  {
+            handOffAnimationInternal(Choreographer.getInstance().getFrameTime(), velocityPxPerMs);
+        } catch (IllegalStateException e) {
+            Choreographer.getInstance().postFrameCallback(
+                    frameTimeNanos -> handOffAnimationInternal(
+                            frameTimeNanos / TimeUtils.NANOS_PER_MS, velocityPxPerMs));
+        }
+    }
+
+    private void handOffAnimationInternal(long timestamp, PointF velocityPxPerMs) {
+        if (mRecentsAnimationController == null) {
             return;
         }
 
         Pair<RemoteAnimationTarget[], WindowAnimationState[]> targetsAndStates =
-                extractTargetsAndStates(mRemoteTargetHandles, velocityPxPerMs);
+                extractTargetsAndStates(
+                        mRemoteTargetHandles, timestamp, velocityPxPerMs);
         mRecentsAnimationController.handOffAnimation(
                 targetsAndStates.first, targetsAndStates.second);
         ActiveGestureProtoLogProxy.logHandOffAnimation();
@@ -2085,6 +2104,7 @@
         if (mRecentsView != null) {
             mRecentsView.removeOnScrollChangedListener(mOnRecentsScrollListener);
         }
+        mGestureState.getContainerInterface().setOnDeferredActivityLaunchCallback(null);
     }
 
     private void resetStateForAnimationCancel() {
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 461f963..c09bf3e 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -70,7 +70,7 @@
     private val taskAnimationManager: TaskAnimationManager,
     private val dispatcherProvider: DispatcherProvider = ProductionDispatchers,
 ) {
-    private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.default)
+    private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.background)
 
     private val commandQueue = ConcurrentLinkedDeque<CommandInfo>()
 
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 055aadb..145773d 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -56,6 +56,8 @@
     private boolean mFinishRequested = false;
     // Only valid when mFinishRequested == true.
     private boolean mFinishTargetIsLauncher;
+    // Only valid when mFinishRequested == true
+    private boolean mLauncherIsVisibleAtFinish;
     private RunnableList mPendingFinishCallbacks = new RunnableList();
 
     public RecentsAnimationController(RecentsAnimationControllerCompat controller,
@@ -130,13 +132,27 @@
     }
 
     @UiThread
+    public void finish(boolean toRecents, boolean launcherIsVisibleAtFinish,
+            Runnable onFinishComplete, boolean sendUserLeaveHint) {
+        Preconditions.assertUIThread();
+        finishController(toRecents, launcherIsVisibleAtFinish, onFinishComplete, sendUserLeaveHint,
+                false);
+    }
+
+    @UiThread
     public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint) {
-        finishController(toRecents, callback, sendUserLeaveHint, false /* forceFinish */);
+        finishController(toRecents, false, callback, sendUserLeaveHint, false /* forceFinish */);
     }
 
     @UiThread
     public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint,
             boolean forceFinish) {
+        finishController(toRecents, toRecents, callback, sendUserLeaveHint, forceFinish);
+    }
+
+    @UiThread
+    public void finishController(boolean toRecents, boolean launcherIsVisibleAtFinish,
+            Runnable callback, boolean sendUserLeaveHint, boolean forceFinish) {
         mPendingFinishCallbacks.add(callback);
         if (!forceFinish && mFinishRequested) {
             // If finish has already been requested, then add the callback to the pending list.
@@ -148,6 +164,7 @@
         // Finish not yet requested
         mFinishRequested = true;
         mFinishTargetIsLauncher = toRecents;
+        mLauncherIsVisibleAtFinish = launcherIsVisibleAtFinish;
         mOnFinishedListener.accept(this);
         Runnable finishCb = () -> {
             mController.finish(toRecents, sendUserLeaveHint, new IResultReceiver.Stub() {
@@ -224,6 +241,14 @@
         return mFinishTargetIsLauncher;
     }
 
+    /**
+     * RecentsAnimationListeners can check this in onRecentsAnimationFinished() to determine whether
+     * the animation was finished to launcher vs an app.
+     */
+    public boolean getLauncherIsVisibleAtFinish() {
+        return mLauncherIsVisibleAtFinish;
+    }
+
     public void dump(String prefix, PrintWriter pw) {
         pw.println(prefix + "RecentsAnimationController:");
 
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 59d4547..2991e64 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -55,6 +55,7 @@
 
 import androidx.annotation.MainThread;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
 import com.android.internal.logging.InstanceId;
@@ -70,6 +71,7 @@
 import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
 import com.android.systemui.shared.system.RecentsAnimationListener;
@@ -1634,4 +1636,24 @@
         pw.println("\tmUnfoldAnimationListener=" + mUnfoldAnimationListener);
         pw.println("\tmDragAndDrop=" + mDragAndDrop);
     }
+
+    /**
+     * Adds all interfaces held by this proxy to the bundle
+     */
+    @VisibleForTesting
+    public void addAllInterfaces(Bundle out) {
+        QuickStepContract.addInterface(mSystemUiProxy, out);
+        QuickStepContract.addInterface(mPip, out);
+        QuickStepContract.addInterface(mBubbles, out);
+        QuickStepContract.addInterface(mSysuiUnlockAnimationController, out);
+        QuickStepContract.addInterface(mSplitScreen, out);
+        QuickStepContract.addInterface(mOneHanded, out);
+        QuickStepContract.addInterface(mShellTransitions, out);
+        QuickStepContract.addInterface(mStartingWindow, out);
+        QuickStepContract.addInterface(mRecentTasks, out);
+        QuickStepContract.addInterface(mBackAnimation, out);
+        QuickStepContract.addInterface(mDesktopMode, out);
+        QuickStepContract.addInterface(mUnfoldAnimation, out);
+        QuickStepContract.addInterface(mDragAndDrop, out);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 783c87c..084cede 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -795,14 +795,15 @@
      * second applies to the target in the same index of the first.
      *
      * @param handles The handles wrapping each target.
+     * @param timestamp The start time of the current frame.
      * @param velocityPxPerMs The current velocity of the target animations.
      */
     @NonNull
     public static Pair<RemoteAnimationTarget[], WindowAnimationState[]> extractTargetsAndStates(
-            @NonNull RemoteTargetHandle[] handles, @NonNull PointF velocityPxPerMs) {
+            @NonNull RemoteTargetHandle[] handles, long timestamp,
+            @NonNull PointF velocityPxPerMs) {
         RemoteAnimationTarget[] targets = new RemoteAnimationTarget[handles.length];
         WindowAnimationState[] animationStates = new WindowAnimationState[handles.length];
-        long timestamp = System.currentTimeMillis();
 
         for (int i = 0; i < handles.length; i++) {
             targets[i] = handles[i].getTransformParams().getTargetSet().apps[i];
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 75e7c64..50d4dab 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -37,19 +37,6 @@
 import static com.android.quickstep.InputConsumerUtils.newConsumer;
 import static com.android.quickstep.InputConsumerUtils.tryCreateAssistantInputConsumer;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER;
-import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
-import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES;
-import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE;
-import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP;
-import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED;
-import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_PIP;
-import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
-import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
-import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN;
-import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW;
 
 import android.app.PendingIntent;
 import android.app.Service;
@@ -148,7 +135,6 @@
 public class TouchInteractionService extends Service {
 
     private static final String SUBSTRING_PREFIX = "; ";
-    private static final String NEWLINE_PREFIX = "\n\t\t\t-> ";
 
     private static final String TAG = "TouchInteractionService";
 
@@ -173,30 +159,30 @@
         @BinderThread
         public void onInitialize(Bundle bundle) {
             ISystemUiProxy proxy = ISystemUiProxy.Stub.asInterface(
-                    bundle.getBinder(KEY_EXTRA_SYSUI_PROXY));
-            IPip pip = IPip.Stub.asInterface(bundle.getBinder(KEY_EXTRA_SHELL_PIP));
-            IBubbles bubbles = IBubbles.Stub.asInterface(bundle.getBinder(KEY_EXTRA_SHELL_BUBBLES));
+                    bundle.getBinder(ISystemUiProxy.DESCRIPTOR));
+            IPip pip = IPip.Stub.asInterface(bundle.getBinder(IPip.DESCRIPTOR));
+            IBubbles bubbles = IBubbles.Stub.asInterface(bundle.getBinder(IBubbles.DESCRIPTOR));
             ISplitScreen splitscreen = ISplitScreen.Stub.asInterface(bundle.getBinder(
-                    KEY_EXTRA_SHELL_SPLIT_SCREEN));
+                    ISplitScreen.DESCRIPTOR));
             IOneHanded onehanded = IOneHanded.Stub.asInterface(
-                    bundle.getBinder(KEY_EXTRA_SHELL_ONE_HANDED));
+                    bundle.getBinder(IOneHanded.DESCRIPTOR));
             IShellTransitions shellTransitions = IShellTransitions.Stub.asInterface(
-                    bundle.getBinder(KEY_EXTRA_SHELL_SHELL_TRANSITIONS));
+                    bundle.getBinder(IShellTransitions.DESCRIPTOR));
             IStartingWindow startingWindow = IStartingWindow.Stub.asInterface(
-                    bundle.getBinder(KEY_EXTRA_SHELL_STARTING_WINDOW));
+                    bundle.getBinder(IStartingWindow.DESCRIPTOR));
             ISysuiUnlockAnimationController launcherUnlockAnimationController =
                     ISysuiUnlockAnimationController.Stub.asInterface(
-                            bundle.getBinder(KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER));
+                            bundle.getBinder(ISysuiUnlockAnimationController.DESCRIPTOR));
             IRecentTasks recentTasks = IRecentTasks.Stub.asInterface(
-                    bundle.getBinder(KEY_EXTRA_SHELL_RECENT_TASKS));
+                    bundle.getBinder(IRecentTasks.DESCRIPTOR));
             IBackAnimation backAnimation = IBackAnimation.Stub.asInterface(
-                    bundle.getBinder(KEY_EXTRA_SHELL_BACK_ANIMATION));
+                    bundle.getBinder(IBackAnimation.DESCRIPTOR));
             IDesktopMode desktopMode = IDesktopMode.Stub.asInterface(
-                    bundle.getBinder(KEY_EXTRA_SHELL_DESKTOP_MODE));
+                    bundle.getBinder(IDesktopMode.DESCRIPTOR));
             IUnfoldAnimation unfoldTransition = IUnfoldAnimation.Stub.asInterface(
-                    bundle.getBinder(KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER));
+                    bundle.getBinder(IUnfoldAnimation.DESCRIPTOR));
             IDragAndDrop dragAndDrop = IDragAndDrop.Stub.asInterface(
-                    bundle.getBinder(KEY_EXTRA_SHELL_DRAG_AND_DROP));
+                    bundle.getBinder(IDragAndDrop.DESCRIPTOR));
             MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis -> {
                 SystemUiProxy.INSTANCE.get(tis).setProxy(proxy, pip,
                         bubbles, splitscreen, onehanded, shellTransitions, startingWindow,
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
index 86f9829..f4c8c99 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -457,7 +457,7 @@
     }
 
     override fun isRecentsViewVisible(): Boolean {
-        return getStateManager().state!!.isRecentsViewVisible
+        return isShowing() || getStateManager().state!!.isRecentsViewVisible
     }
 
     override fun createAtomicAnimationFactory(): AtomicAnimationFactory<RecentsState?>? {
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
index b6cb984..e5bad67 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
@@ -61,12 +61,14 @@
 
     override val dimProgress: Flow<Float> =
         combine(taskContainerData.taskMenuOpenProgress, recentsViewData.tintAmount) {
-            taskMenuOpenProgress,
-            tintAmount ->
-            max(taskMenuOpenProgress * MAX_SCRIM_ALPHA, tintAmount)
-        }
+                taskMenuOpenProgress,
+                tintAmount ->
+                max(taskMenuOpenProgress * MAX_SCRIM_ALPHA, tintAmount)
+            }
+            .flowOn(dispatcherProvider.background)
 
-    override val splashAlpha = splashProgress.flatMapLatest { it }
+    override val splashAlpha =
+        splashProgress.flatMapLatest { it }.flowOn(dispatcherProvider.background)
 
     private val isLiveTile =
         combine(
@@ -77,7 +79,6 @@
                 runningTaskIds.contains(taskId) && !runningTaskShowScreenshot
             }
             .distinctUntilChanged()
-            .flowOn(dispatcherProvider.default)
 
     override val uiState: Flow<TaskThumbnailUiState> =
         combine(task.flatMapLatest { it }, isLiveTile) { taskVal, isRunning ->
@@ -99,7 +100,7 @@
                 }
             }
             .distinctUntilChanged()
-            .flowOn(dispatcherProvider.default)
+            .flowOn(dispatcherProvider.background)
 
     override fun bind(taskId: Int) {
         Log.d(TAG, "bind taskId: $taskId")
diff --git a/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
index c3b072d..908e9f9 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
@@ -48,16 +48,6 @@
         return otherTasks + desktopTasks
     }
 
-    /** Returns the expected index of the focus task. */
-    fun getFocusedTaskIndex(taskGroups: List<GroupTask>): Int {
-        // The focused task index is placed after the desktop tasks views.
-        return if (enableLargeDesktopWindowingTile()) {
-            taskGroups.count { it.taskViewType == TaskViewType.DESKTOP }
-        } else {
-            0
-        }
-    }
-
     /** Counts [TaskView]s that are [DesktopTaskView] instances. */
     fun getDesktopTaskViewCount(taskViews: Iterable<TaskView>): Int =
         taskViews.count { it is DesktopTaskView }
@@ -81,6 +71,30 @@
     ): TaskView? =
         taskViews.firstOrNull { it.isLargeTile && !(splitSelectActive && it is DesktopTaskView) }
 
+    /** Returns the expected focus task. */
+    fun getExpectedFocusedTask(taskViews: Iterable<TaskView>): TaskView? =
+        if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView }
+        else taskViews.firstOrNull()
+
+    /**
+     * Returns the [TaskView] that should be the current page during task binding, in the following
+     * priorities:
+     * 1. Running task
+     * 2. Focused task
+     * 3. First non-desktop task
+     * 4. Last desktop task
+     * 5. null otherwise
+     */
+    fun getExpectedCurrentTask(
+        runningTaskView: TaskView?,
+        focusedTaskView: TaskView?,
+        taskViews: Iterable<TaskView>,
+    ): TaskView? =
+        runningTaskView
+            ?: focusedTaskView
+            ?: taskViews.firstOrNull { it !is DesktopTaskView }
+            ?: taskViews.lastOrNull()
+
     /**
      * Returns the first TaskView that is not large
      *
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index d35a36a..1eb91ae 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -106,6 +106,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.Consumer;
 
 /**
@@ -941,6 +942,10 @@
                         mLauncher, mLauncher.getDragLayer(),
                         null /* thumbnail */,
                         mAppIcon, new RectF());
+                floatingTaskView.setOnClickListener(view ->
+                        getSplitAnimationController()
+                                .playAnimPlaceholderToFullscreen(mContainer, view,
+                                        Optional.of(() -> resetState())));
                 floatingTaskView.setAlpha(1);
                 floatingTaskView.addStagingAnimation(anim, mTaskBounds, mTempRect,
                         false /* fadeWithThumbnail */, true /* isStagedTask */);
diff --git a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
new file mode 100644
index 0000000..f973dd0
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.ImageButton
+
+/**
+ * Button for supporting multiple desktop sessions. The button will be next to the first TaskView
+ * inside overview, while clicking this button will create a new desktop session.
+ */
+class AddDesktopButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+    ImageButton(context, attrs) {
+    // TODO(b/382057498): add this button the overview.
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 6fc33dc..e7cb05e 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1510,6 +1510,19 @@
     }
 
     /**
+     * Launch DesktopTaskView if found.
+     * @return provides runnable list to attach runnable at end of Desktop Mode launch
+     */
+    public RunnableList launchDesktopTaskView() {
+        for (TaskView taskView : getTaskViews()) {
+            if (taskView instanceof DesktopTaskView) {
+                return taskView.launchWithAnimation();
+            }
+        }
+        return null;
+    }
+
+    /**
      * Returns a {@link TaskView} that has taskId matching {@code taskId} or null if no match.
      */
     @Nullable
@@ -1957,18 +1970,20 @@
         }
 
         // Keep same previous focused task
-        TaskView newFocusedTaskView = getTaskViewByTaskIds(focusedTaskIds);
-        if (enableLargeDesktopWindowingTile() && newFocusedTaskView instanceof DesktopTaskView) {
-            newFocusedTaskView = null;
+        TaskView newFocusedTaskView = null;
+        if (!enableGridOnlyOverview()) {
+            newFocusedTaskView = getTaskViewByTaskIds(focusedTaskIds);
+            if (enableLargeDesktopWindowingTile()
+                    && newFocusedTaskView instanceof DesktopTaskView) {
+                newFocusedTaskView = null;
+            }
+            // If the list changed, maybe the focused task doesn't exist anymore.
+            if (newFocusedTaskView == null) {
+                newFocusedTaskView = mUtils.getExpectedFocusedTask(getTaskViews());
+            }
         }
-        // If the list changed, maybe the focused task doesn't exist anymore
-        int newFocusedTaskViewIndex = mUtils.getFocusedTaskIndex(taskGroups);
-        if (newFocusedTaskView == null && getTaskViewCount() > newFocusedTaskViewIndex) {
-            newFocusedTaskView = getTaskViewAt(newFocusedTaskViewIndex);
-        }
-
-        setFocusedTaskViewId(newFocusedTaskView != null && !enableGridOnlyOverview()
-                ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID);
+        setFocusedTaskViewId(
+                newFocusedTaskView != null ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID);
 
         updateTaskSize();
         updateChildTaskOrientations();
@@ -1987,6 +2002,7 @@
                     // for cases where the running task isn't included in this load plan (e.g. if
                     // the current running task is excludedFromRecents.)
                     showCurrentTask(mActiveGestureRunningTasks, "applyLoadPlan");
+                    newRunningTaskView = getRunningTaskView();
                 } else {
                     setRunningTaskViewId(INVALID_TASK_ID);
                 }
@@ -2006,12 +2022,9 @@
         } else if (previousFocusedPage != INVALID_PAGE) {
             targetPage = previousFocusedPage;
         } else {
-            // Set the current page to the running task, but not if settling on new task.
-            if (hasAllValidTaskIds(runningTaskIds)) {
-                targetPage = indexOfChild(newRunningTaskView);
-            } else if (getTaskViewCount() > newFocusedTaskViewIndex) {
-                targetPage = indexOfChild(requireTaskViewAt(newFocusedTaskViewIndex));
-            }
+            targetPage = indexOfChild(
+                    mUtils.getExpectedCurrentTask(newRunningTaskView, newFocusedTaskView,
+                            getTaskViews()));
         }
         if (targetPage != -1 && mCurrentPage != targetPage) {
             int finalTargetPage = targetPage;
@@ -5888,11 +5901,18 @@
     }
 
     /**
+     * Finish recents animation.
+     */
+    public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
+            @Nullable Runnable onFinishComplete) {
+        finishRecentsAnimation(toRecents, shouldPip, false, onFinishComplete);
+    }
+    /**
      * NOTE: Whatever value gets passed through to the toRecents param may need to also be set on
      * {@link #mRecentsAnimationController#setWillFinishToHome}.
      */
     public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
-            @Nullable Runnable onFinishComplete) {
+            boolean allAppTargetsAreTranslucent, @Nullable Runnable onFinishComplete) {
         Log.d(TAG, "finishRecentsAnimation - mRecentsAnimationController: "
                 + mRecentsAnimationController);
         // TODO(b/197232424#comment#10) Move this back into onRecentsAnimationComplete(). Maybe?
@@ -5906,8 +5926,9 @@
         }
 
         final boolean sendUserLeaveHint = toRecents && shouldPip;
-        if (sendUserLeaveHint) {
+        if (sendUserLeaveHint && !com.android.wm.shell.Flags.enablePip2()) {
             // Notify the SysUI to use fade-in animation when entering PiP from live tile.
+            // Note: PiP2 handles entering differently, so skip if enable_pip2=true.
             final SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(getContext());
             systemUiProxy.setPipAnimationTypeToAlpha();
             systemUiProxy.setShelfHeight(true, mContainer.getDeviceProfile().hotseatBarSizePx);
@@ -5924,7 +5945,7 @@
                         tx, null /* overlay */);
             }
         }
-        mRecentsAnimationController.finish(toRecents, () -> {
+        mRecentsAnimationController.finish(toRecents, allAppTargetsAreTranslucent, () -> {
             if (onFinishComplete != null) {
                 onFinishComplete.run();
             }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index 29d142f..44070cf 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -23,6 +23,7 @@
 import android.graphics.drawable.ColorDrawable
 import android.view.LayoutInflater
 import android.view.View
+import android.view.View.INVISIBLE
 import android.view.View.VISIBLE
 import android.widget.FrameLayout
 import android.widget.TextView
@@ -78,7 +79,7 @@
     private lateinit var flyoutContainer: FrameLayout
     private lateinit var bubbleStashController: BubbleStashController
     private lateinit var flyoutController: BubbleBarFlyoutController
-    private val onExpandedNoOp = Runnable {}
+    private val emptyRunnable = Runnable {}
 
     private val flyoutView: View?
         get() = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view)
@@ -106,7 +107,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpandedNoOp,
+                onExpanded = emptyRunnable,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -162,7 +164,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpandedNoOp,
+                onExpanded = emptyRunnable,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -197,6 +200,8 @@
         assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
         assertThat(animatorScheduler.delayedBlock).isNull()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
         assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
         assertThat(animator.isAnimating).isFalse()
@@ -217,7 +222,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpandedNoOp,
+                onExpanded = emptyRunnable,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -241,6 +247,8 @@
         // verify that the hide animation was canceled
         assertThat(animatorScheduler.delayedBlock).isNull()
         assertThat(animator.isAnimating).isFalse()
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
         verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
 
         // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
@@ -264,7 +272,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpandedNoOp,
+                onExpanded = emptyRunnable,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -316,7 +325,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpandedNoOp,
+                onExpanded = emptyRunnable,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -355,7 +365,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpanded,
+                onExpanded = onExpanded,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -401,7 +412,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpanded,
+                onExpanded = onExpanded,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -453,7 +465,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpanded,
+                onExpanded = onExpanded,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -504,17 +517,21 @@
 
         val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
 
+        var notifiedBubbleBarVisible = false
+        val onBubbleBarVisible = Runnable { notifiedBubbleBarVisible = true }
         val animator =
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpandedNoOp,
+                onExpanded = emptyRunnable,
+                onBubbleBarVisible = onBubbleBarVisible,
                 animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleBarView.visibility = INVISIBLE
             animator.animateToInitialState(bubble, isInApp = true, isExpanding = false)
         }
 
@@ -542,6 +559,8 @@
         assertThat(bubbleBarView.alpha).isEqualTo(0)
         assertThat(handle.translationY).isEqualTo(0)
         assertThat(handle.alpha).isEqualTo(1)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(notifiedBubbleBarVisible).isTrue()
 
         verify(bubbleStashController).stashBubbleBarImmediate()
     }
@@ -567,7 +586,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpanded,
+                onExpanded = onExpanded,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -603,7 +623,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpandedNoOp,
+                onExpanded = emptyRunnable,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -649,7 +670,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpanded,
+                onExpanded = onExpanded,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -699,7 +721,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpanded,
+                onExpanded = onExpanded,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -747,7 +770,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpandedNoOp,
+                onExpanded = emptyRunnable,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -803,7 +827,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpanded,
+                onExpanded = onExpanded,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -858,7 +883,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpanded,
+                onExpanded = onExpanded,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -923,7 +949,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpanded,
+                onExpanded = onExpanded,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -983,7 +1010,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpandedNoOp,
+                onExpanded = emptyRunnable,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -1053,7 +1081,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpandedNoOp,
+                onExpanded = emptyRunnable,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -1129,7 +1158,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpandedNoOp,
+                onExpanded = emptyRunnable,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -1216,7 +1246,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpandedNoOp,
+                onExpanded = emptyRunnable,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
@@ -1330,7 +1361,8 @@
                 bubbleStashController,
                 flyoutController,
                 bubbleBarParentViewController,
-                onExpandedNoOp,
+                onExpanded = emptyRunnable,
+                onBubbleBarVisible = emptyRunnable,
                 animatorScheduler,
             )
 
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 066ddc0..ed0c928 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -847,12 +847,42 @@
         verify(taskbarViewController, times(2)).commitRunningAppsToUI()
     }
 
+    @Test
+    fun onRecentTasksChanged_inDesktopMode_sameHotseatPackage_differentUser_isInShownTasks() {
+        setInDesktopMode(true)
+        val hotseatPackageUser = PackageUser(HOTSEAT_PACKAGE_1, USER_HANDLE_2)
+        val hotseatPackageUsers = listOf(hotseatPackageUser)
+        val runningTask = createTask(id = 1, HOTSEAT_PACKAGE_1, localUserHandle = USER_HANDLE_1)
+        val runningTasks = listOf(runningTask)
+        prepareHotseatAndRunningAndRecentAppsInternal(
+            hotseatPackageUsers = hotseatPackageUsers,
+            runningTasks = runningTasks,
+            recentTaskPackages = emptyList(),
+        )
+        val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+        assertThat(shownTasks).contains(runningTask)
+        assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1))
+    }
+
     private fun prepareHotseatAndRunningAndRecentApps(
         hotseatPackages: List<String>,
         runningTasks: List<Task>,
         recentTaskPackages: List<String>,
     ): Array<ItemInfo?> {
-        val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages)
+        val hotseatPackageUsers = hotseatPackages.map { PackageUser(it, myUserHandle) }
+        return prepareHotseatAndRunningAndRecentAppsInternal(
+            hotseatPackageUsers,
+            runningTasks,
+            recentTaskPackages,
+        )
+    }
+
+    private fun prepareHotseatAndRunningAndRecentAppsInternal(
+        hotseatPackageUsers: List<PackageUser>,
+        runningTasks: List<Task>,
+        recentTaskPackages: List<String>,
+    ): Array<ItemInfo?> {
+        val hotseatItems = createHotseatItemsFromPackageUsers(hotseatPackageUsers)
         recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
         updateRecentTasks(runningTasks, recentTaskPackages)
         return recentAppsController.shownHotseatItems.toTypedArray()
@@ -877,12 +907,14 @@
         recentTasksChangedListener?.onRecentTasksChanged()
     }
 
-    private fun createHotseatItemsFromPackageNames(packageNames: List<String>): List<ItemInfo> {
-        return packageNames
+    private fun createHotseatItemsFromPackageUsers(
+        packageUsers: List<PackageUser>
+    ): List<ItemInfo> {
+        return packageUsers
             .map {
-                createTestAppInfo(packageName = it).apply {
+                createTestAppInfo(packageName = it.packageName, userHandle = it.userHandle).apply {
                     container =
-                        if (it.startsWith("predicted")) {
+                        if (it.packageName.startsWith("predicted")) {
                             CONTAINER_HOTSEAT_PREDICTION
                         } else {
                             CONTAINER_HOTSEAT
@@ -895,13 +927,8 @@
     private fun createTestAppInfo(
         packageName: String = "testPackageName",
         className: String = "testClassName",
-    ) =
-        AppInfo(
-            ComponentName(packageName, className),
-            className /* title */,
-            myUserHandle,
-            Intent(),
-        )
+        userHandle: UserHandle,
+    ) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent())
 
     private fun createRecentTasksFromPackageNames(packageNames: List<String>): List<GroupTask> {
         return packageNames.map { packageName ->
@@ -969,4 +996,6 @@
         const val RECENT_PACKAGE_3 = "recent3"
         const val RECENT_SPLIT_PACKAGES_1 = "split1_split2"
     }
+
+    data class PackageUser(val packageName: String, val userHandle: UserHandle)
 }
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index aa105f9..695211b 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -19,6 +19,7 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.launcher3.Flags.enableFallbackOverviewInWindow;
 import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
 import static com.android.launcher3.tapl.TestHelpers.getHomeIntentInPackage;
 import static com.android.launcher3.tapl.TestHelpers.getLauncherInMyProcess;
@@ -63,7 +64,9 @@
 import com.android.launcher3.util.rule.TestIsolationRule;
 import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.launcher3.util.rule.ViewCaptureRule;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.RecentsViewContainer;
 
 import org.junit.After;
 import org.junit.Before;
@@ -191,27 +194,29 @@
     @Test
     public void goToOverviewFromApp() {
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
-        waitForRecentsActivityStop();
+        waitForRecentsClosed();
 
         mLauncher.getLaunchedAppState().switchToOverview();
     }
 
-    protected void executeOnRecents(Consumer<RecentsActivity> f) {
+    protected void executeOnRecents(Consumer<RecentsViewContainer> f) {
         getFromRecents(r -> {
             f.accept(r);
             return true;
         });
     }
 
-    protected <T> T getFromRecents(Function<RecentsActivity, T> f) {
+    protected <T> T getFromRecents(Function<RecentsViewContainer, T> f) {
         if (!TestHelpers.isInLauncherProcess()) return null;
         Object[] result = new Object[1];
         Wait.atMost("Failed to get from recents", () -> MAIN_EXECUTOR.submit(() -> {
-            RecentsActivity activity = RecentsActivity.ACTIVITY_TRACKER.getCreatedContext();
-            if (activity == null) {
+            RecentsViewContainer recentsViewContainer = enableFallbackOverviewInWindow()
+                    ? RecentsWindowManager.getRecentsWindowTracker().getCreatedContext()
+                    : RecentsActivity.ACTIVITY_TRACKER.getCreatedContext();
+            if (recentsViewContainer == null) {
                 return false;
             }
-            result[0] = f.apply(activity);
+            result[0] = f.apply(recentsViewContainer);
             return true;
         }).get(), mLauncher);
         return (T) result[0];
@@ -224,14 +229,19 @@
 
     private void pressHomeAndWaitForOverviewClose() {
         mDevice.pressHome();
-        waitForRecentsActivityStop();
+        waitForRecentsClosed();
     }
 
-    private void waitForRecentsActivityStop() {
+    private void waitForRecentsClosed() {
         try {
-            final boolean recentsActivityIsNull = MAIN_EXECUTOR.submit(
-                    () -> RecentsActivity.ACTIVITY_TRACKER.getCreatedContext() == null).get();
-            if (recentsActivityIsNull) {
+            final boolean isRecentsContainerNUll = MAIN_EXECUTOR.submit(() -> {
+                RecentsViewContainer recentsViewContainer = enableFallbackOverviewInWindow()
+                        ? RecentsWindowManager.getRecentsWindowTracker().getCreatedContext()
+                        : RecentsActivity.ACTIVITY_TRACKER.getCreatedContext();
+
+                return recentsViewContainer == null;
+            }).get();
+            if (isRecentsContainerNUll) {
                 // Null activity counts as a "stopped" one.
                 return;
             }
@@ -241,7 +251,7 @@
             throw new RuntimeException(e);
         }
 
-        Wait.atMost("Recents activity didn't stop",
+        Wait.atMost("Recents view container didn't close",
                 () -> getFromRecents(recents -> !recents.isStarted()),
                 mLauncher);
     }
@@ -251,7 +261,7 @@
         startAppFast(getAppPackageName());
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
         startTestActivity(2);
-        waitForRecentsActivityStop();
+        waitForRecentsClosed();
         Wait.atMost("Expected three apps in the task list",
                 () -> mLauncher.getRecentTasks().size() >= 3,
                 mLauncher);
@@ -312,12 +322,12 @@
         );
     }
 
-    private int getCurrentOverviewPage(RecentsActivity recents) {
-        return recents.<RecentsView>getOverviewPanel().getCurrentPage();
+    private int getCurrentOverviewPage(RecentsViewContainer recentsViewContainer) {
+        return recentsViewContainer.<RecentsView>getOverviewPanel().getCurrentPage();
     }
 
-    private int getTaskCount(RecentsActivity recents) {
-        return recents.<RecentsView>getOverviewPanel().getTaskViewCount();
+    private int getTaskCount(RecentsViewContainer recentsViewContainer) {
+        return recentsViewContainer.<RecentsView>getOverviewPanel().getTaskViewCount();
     }
 
     private class OverviewUpdateHandler {
diff --git a/res/drawable/ic_desktop_add.xml b/res/drawable/ic_desktop_add.xml
new file mode 100644
index 0000000..fa5e0de
--- /dev/null
+++ b/res/drawable/ic_desktop_add.xml
@@ -0,0 +1,24 @@
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+    <path
+        android:pathData="M140,740v-520,520ZM240,640v-200h360v200L240,640ZM140,800q-24,0 -42,-18t-18,-42v-520q0,-24 18,-42t42,-18h680q24,0 42,18t18,42v300h-60v-300L140,220v520h420v60L140,800ZM680,520v-160L360,360v-40h360v200h-40ZM740,880v-120L620,760v-60h120v-120h60v120h120v60L800,760v120h-60Z"
+        android:fillColor="#5f6368"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/inset_rounded_action_button.xml b/res/drawable/inset_rounded_action_button.xml
new file mode 100644
index 0000000..8ae40c0
--- /dev/null
+++ b/res/drawable/inset_rounded_action_button.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:insetTop="@dimen/inset_rounded_action_button"
+    android:insetBottom="@dimen/inset_rounded_action_button"
+    android:insetLeft="@dimen/inset_rounded_action_button"
+    android:insetRight="@dimen/inset_rounded_action_button">
+    <shape
+        android:shape="rectangle">
+        <solid android:color="@color/materialColorSurfaceContainerLow" />
+        <corners android:radius="@dimen/rounded_button_radius" />
+        <stroke
+            android:width="1dp"
+            android:color="@color/materialColorSurfaceContainerLow" />
+    </shape>
+</inset>
diff --git a/res/layout/work_apps_edu.xml b/res/layout/work_apps_edu.xml
index a45d585..0e2c19a 100644
--- a/res/layout/work_apps_edu.xml
+++ b/res/layout/work_apps_edu.xml
@@ -25,9 +25,8 @@
         android:orientation="horizontal"
         android:background="@drawable/work_card"
         android:layout_gravity="center_horizontal"
-        android:paddingEnd="@dimen/work_card_margin"
         android:paddingStart="@dimen/work_card_margin"
-        android:paddingTop="@dimen/work_card_margin"
+        android:paddingEnd="@dimen/work_card_margin_end"
         android:paddingBottom="@dimen/work_card_margin"
         android:id="@+id/wrapper">
         <TextView
@@ -37,18 +36,22 @@
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_weight="1"
+            android:layout_marginTop="@dimen/work_card_margin"
             android:paddingEnd="@dimen/work_edu_card_text_end_margin"
             android:text="@string/work_profile_edu_work_apps"
             android:textDirection="locale"
             android:textSize="18sp" />
         <FrameLayout
+            android:id="@+id/action_btn"
             android:layout_width="@dimen/rounded_button_width"
             android:layout_height="@dimen/rounded_button_width"
-            android:background="@drawable/rounded_action_button">
+            android:layout_marginTop="@dimen/work_edu_card_button_margin_top"
+            android:gravity="center"
+            android:background="@drawable/inset_rounded_action_button">
             <ImageButton
-                android:id="@+id/action_btn"
                 android:layout_width="@dimen/x_icon_size"
                 android:layout_height="@dimen/x_icon_size"
+                android:clickable="false"
                 android:scaleType="centerInside"
                 android:layout_gravity="center"
                 android:contentDescription="@string/accessibility_close"
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index c477633..698877a 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -162,10 +162,11 @@
         <attr name="layout_sticky" format="boolean" />
     </declare-styleable>
 
-    <declare-styleable name="GridDimension">
-        <attr name="minDeviceWidthDp" format="integer"/>
-        <attr name="minDeviceHeightDp" format="integer"/>
-        <attr name="numGridDimension" format="integer"/>
+    <declare-styleable name="GridSize">
+        <attr name="minDeviceWidthDp" format="float"/>
+        <attr name="minDeviceHeightDp" format="float"/>
+        <attr name="numGridRows" format="integer"/>
+        <attr name="numGridColumns" format="integer"/>
         <attr name="dbFile" />
         <attr name="defaultLayoutId"/>
         <attr name="demoModeLayoutId"/>
@@ -261,7 +262,7 @@
         <!-- File that contains the specs for all apps icon and text size.
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="allAppsCellSpecsId" format="reference" />
-        <attr name="rowCountSpecsId" format="reference" />
+        <attr name="gridSizeSpecsId" format="reference" />
         <!-- defaults to allAppsCellSpecsId, if not specified -->
         <attr name="allAppsCellSpecsTwoPanelId" format="reference" />
         <!-- defaults to false, if not specified -->
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 326ee06..c0bd956 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -173,13 +173,16 @@
     <dimen name="work_edu_card_margin">16dp</dimen>
     <dimen name="work_edu_card_radius">16dp</dimen>
     <dimen name="work_edu_card_bottom_margin">26dp</dimen>
-    <dimen name="work_edu_card_text_end_margin">32dp</dimen>
+    <dimen name="work_edu_card_text_end_margin">12dp</dimen>
     <dimen name="work_apps_paused_button_stroke">1dp</dimen>
+    <dimen name="work_edu_card_button_margin_top">12dp</dimen>
 
     <dimen name="work_card_margin">24dp</dimen>
+    <dimen name="work_card_margin_end">12dp</dimen>
     <!-- (x) icon button inside work edu card -->
-    <dimen name="rounded_button_width">24dp</dimen>
+    <dimen name="rounded_button_width">48dp</dimen>
     <dimen name="x_icon_size">16dp</dimen>
+    <dimen name="inset_rounded_action_button">12dp</dimen>
 
     <!-- rounded button shown inside card views, and snack bars  -->
     <dimen name="padded_rounded_button_height">48dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f7069a6..cdfbefe 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -154,6 +154,10 @@
     <!-- A widget category label for grouping widgets related to note taking. [CHAR_LIMIT=30] -->
     <string name="widget_category_note_taking">Note-taking</string>
 
+    <!-- Accessibility label on the widget preview that on click (if add button is hidden) shows the button to add widget to the home screen. [CHAR_LIMIT=none] -->
+    <string name="widget_cell_tap_to_show_add_button_label">Show add button</string>
+    <!-- Accessibility label on the widget preview that on click (if add button is showing) hides the button to add widget to the home screen. [CHAR_LIMIT=none] -->
+    <string name="widget_cell_tap_to_hide_add_button_label">Hide add button</string>
     <!-- Text on the button that adds a widget to the home screen. [CHAR_LIMIT=15] -->
     <string name="widget_add_button_label">Add</string>
     <!-- Accessibility content description for the button that adds a widget to the home screen. The
diff --git a/res/xml/backupscheme.xml b/res/xml/backupscheme.xml
index 34b80b1..083af5c 100644
--- a/res/xml/backupscheme.xml
+++ b/res/xml/backupscheme.xml
@@ -11,6 +11,7 @@
     <include domain="database" path="launcher_3_by_3.db" />
     <include domain="database" path="launcher_2_by_2.db" />
     <include domain="database" path="launcher_7_by_3.db" />
+    <include domain="database" path="launcher_8_by_3.db" />
     <include domain="sharedpref" path="com.android.launcher3.prefs.xml" />
     <include domain="file" path="downgrade_schema.json" />
 
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 50e78ac..3b93cf4 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -80,12 +80,16 @@
         updateTheme();
     }
 
-    protected void updateTheme() {
+    private void updateTheme() {
         if (mThemeRes != Themes.getActivityThemeRes(this)) {
-            recreate();
+            recreateToUpdateTheme();
         }
     }
 
+    protected void recreateToUpdateTheme() {
+        recreate();
+    }
+
     @Override
     public void onActionModeStarted(ActionMode mode) {
         super.onActionModeStarted(mode);
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 9b6fe4e..f1274dc 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -425,7 +425,9 @@
                 && WindowManagerProxy.INSTANCE.get(context).isTaskbarDrawnInProcess();
 
         // Some more constants.
-        context = getContext(context, info, isVerticalBarLayout() || (isTablet && isLandscape)
+        context = getContext(context, info, inv.isFixedLandscape
+                        || isVerticalBarLayout()
+                        || (isTablet && isLandscape)
                         ? Configuration.ORIENTATION_LANDSCAPE
                         : Configuration.ORIENTATION_PORTRAIT,
                 windowBounds);
@@ -865,7 +867,7 @@
         canQsbInline = canQsbInline && hotseatQsbHeight > 0;
 
         return (mIsScalableGrid && inv.inlineQsb[mTypeIndex] && canQsbInline)
-                || inv.isFixedLandscapeMode;
+                || inv.isFixedLandscape;
     }
 
     private static DotRenderer createDotRenderer(
@@ -1834,7 +1836,7 @@
             }
             int paddingTop = workspaceTopPadding + (mIsScalableGrid ? 0 : edgeMarginPx);
             // On isFixedLandscapeMode on phones we already have padding because of the camera hole
-            int paddingSide = inv.isFixedLandscapeMode ? 0 : desiredWorkspaceHorizontalMarginPx;
+            int paddingSide = inv.isFixedLandscape ? 0 : desiredWorkspaceHorizontalMarginPx;
 
             padding.set(paddingSide, paddingTop, paddingSide, paddingBottom);
         }
@@ -1934,7 +1936,7 @@
                 hotseatBarPadding.set(mHotseatBarWorkspaceSpacePx, paddingTop,
                         mInsets.right + mHotseatBarEdgePaddingPx, paddingBottom);
             }
-        } else if (isTaskbarPresent || inv.isFixedLandscapeMode) {
+        } else if (isTaskbarPresent || inv.isFixedLandscape) {
             // Center the QSB vertically with hotseat
             int hotseatBarBottomPadding = getHotseatBarBottomPadding();
             int hotseatBarTopPadding =
@@ -1953,7 +1955,7 @@
             }
             startSpacing += getAdditionalQsbSpace();
 
-            if (inv.isFixedLandscapeMode) {
+            if (inv.isFixedLandscape) {
                 endSpacing += mInsets.right;
                 startSpacing +=  mInsets.left;
             }
@@ -2568,7 +2570,7 @@
             }
             if (mTransposeLayoutWithOrientation == null) {
                 mTransposeLayoutWithOrientation =
-                        !(mInfo.isTablet(mWindowBounds) || mInv.isFixedLandscapeMode);
+                        !(mInfo.isTablet(mWindowBounds) || mInv.isFixedLandscape);
             }
             if (mIsGestureMode == null) {
                 mIsGestureMode = mInfo.getNavigationMode().hasGestures;
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 6be8098..b20d8a5 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -36,6 +36,7 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.celllayout.CellLayoutLayoutParams;
 import com.android.launcher3.util.HorizontalInsettableView;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
@@ -201,13 +202,16 @@
         AnimatorSet animatorSet = new AnimatorSet();
         for (int i = 0; i < icons.getChildCount(); i++) {
             View child = icons.getChildAt(i);
-            float tx = shouldAdjustHotseat ? dp.getHotseatAdjustedTranslation(getContext(), i) : 0;
-            if (child instanceof Reorderable) {
-                MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate();
-                animatorSet.play(
-                        mtd.getTranslationX(INDEX_BUBBLE_ADJUSTMENT_ANIM).animateToValue(tx));
-            } else {
-                animatorSet.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_X, tx));
+            if (child.getLayoutParams() instanceof CellLayoutLayoutParams lp) {
+                float tx = shouldAdjustHotseat
+                        ? dp.getHotseatAdjustedTranslation(getContext(), lp.getCellX()) : 0;
+                if (child instanceof Reorderable) {
+                    MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate();
+                    animatorSet.play(
+                            mtd.getTranslationX(INDEX_BUBBLE_ADJUSTMENT_ANIM).animateToValue(tx));
+                } else {
+                    animatorSet.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_X, tx));
+                }
             }
         }
         //TODO(b/381109832) refactor & simplify adjustment logic
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index c044c52..5becdde 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -63,6 +63,7 @@
 import com.android.launcher3.util.ResourceHelper;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.WindowBounds;
+import com.android.launcher3.util.window.CachedDisplayInfo;
 import com.android.launcher3.util.window.WindowManagerProxy;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -188,7 +189,7 @@
     @XmlRes
     public int workspaceSpecsId = INVALID_RESOURCE_HANDLE;
     @XmlRes
-    public int rowCountSpecsId = INVALID_RESOURCE_HANDLE;;
+    public int gridSizeSpecsId = INVALID_RESOURCE_HANDLE;;
     @XmlRes
     public int workspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
     @XmlRes
@@ -216,7 +217,7 @@
     /**
      * Fixed landscape mode is the landscape on the phones.
      */
-    public boolean isFixedLandscapeMode = false;
+    public boolean isFixedLandscape = false;
     private LauncherPrefChangeListener mLandscapeModePreferenceListener;
 
     public String dbFile;
@@ -240,10 +241,7 @@
     @TargetApi(23)
     private InvariantDeviceProfile(Context context) {
         String gridName = getCurrentGridName(context);
-        String newGridName = initGrid(context, gridName);
-        if (!newGridName.equals(gridName)) {
-            LauncherPrefs.get(context).put(GRID_NAME, newGridName);
-        }
+        initGrid(context, gridName);
 
         DisplayController.INSTANCE.get(context).setPriorityListener(
                 (displayContext, info, flags) -> {
@@ -255,9 +253,12 @@
                 });
         if (Flags.oneGridSpecs()) {
             mLandscapeModePreferenceListener = (String s) -> {
-                boolean newFixedLandscapeValue = FIXED_LANDSCAPE_MODE.get(context);
-                if (isFixedLandscapeMode != newFixedLandscapeValue) {
-                    setFixedLandscape(context, newFixedLandscapeValue);
+                if (isFixedLandscape != FIXED_LANDSCAPE_MODE.get(context)) {
+                    MAIN_EXECUTOR.execute(() -> {
+                        Trace.beginSection("InvariantDeviceProfile#setFixedLandscape");
+                        onConfigChanged(context.getApplicationContext());
+                        Trace.endSection();
+                    });
                 }
             };
             LauncherPrefs.INSTANCE.get(context).addListener(
@@ -295,7 +296,7 @@
                         gridName,
                         defaultInfo,
                         /*allowDisabledGrid=*/false,
-                        isFixedLandscapeMode
+                        FIXED_LANDSCAPE_MODE.get(context)
                 ),
                 defaultDeviceType);
 
@@ -309,7 +310,7 @@
                         gridName,
                         myInfo,
                         /*allowDisabledGrid=*/false,
-                        isFixedLandscapeMode
+                        FIXED_LANDSCAPE_MODE.get(context)
                 ),
                 deviceType);
 
@@ -345,13 +346,7 @@
     }
 
     private String initGrid(Context context, String gridName) {
-        if (!Flags.oneGridSpecs() && (isFixedLandscapeMode || FIXED_LANDSCAPE_MODE.get(context))) {
-            LauncherPrefs.get(context).put(FIXED_LANDSCAPE_MODE, false);
-            isFixedLandscapeMode = false;
-        }
-
         Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
-
         List<DisplayOption> allOptions = getPredefinedDeviceProfiles(
                 context,
                 gridName,
@@ -370,6 +365,11 @@
                                 ? new ArrayList<>(allOptions)
                                 : new ArrayList<>(allOptionsFilteredByColCount),
                         displayInfo.getDeviceType());
+
+        if (!displayOption.grid.name.equals(gridName)) {
+            LauncherPrefs.get(context).put(GRID_NAME, displayOption.grid.name);
+        }
+
         initGrid(context, displayInfo, displayOption);
         return displayOption.grid.name;
     }
@@ -413,7 +413,7 @@
         isScalable = closestProfile.isScalable;
         devicePaddingId = closestProfile.devicePaddingId;
         workspaceSpecsId = closestProfile.mWorkspaceSpecsId;
-        rowCountSpecsId = closestProfile.mRowCountSpecsId;
+        gridSizeSpecsId = closestProfile.mGridSizeSpecsId;
         workspaceSpecsTwoPanelId = closestProfile.mWorkspaceSpecsTwoPanelId;
         allAppsSpecsId = closestProfile.mAllAppsSpecsId;
         allAppsSpecsTwoPanelId = closestProfile.mAllAppsSpecsTwoPanelId;
@@ -473,7 +473,7 @@
         startAlignTaskbar = displayOption.startAlignTaskbar;
 
         // Fixed Landscape mode
-        isFixedLandscapeMode = FIXED_LANDSCAPE_MODE.get(context) && Flags.oneGridSpecs();
+        isFixedLandscape = closestProfile.mIsFixedLandscape;
 
         // If the partner customization apk contains any grid overrides, apply them
         // Supported overrides: numRows, numColumns, iconSize
@@ -543,24 +543,6 @@
         });
     }
 
-    /**
-     * Updates the fixed landscape mode, this triggers a new IDP, reloads the database and triggers
-     * a grid migration.
-     */
-    public void setFixedLandscape(Context context, boolean isFixedLandscape) {
-        this.isFixedLandscapeMode = isFixedLandscape;
-        if (isFixedLandscape) {
-            // When in isFixedLandscape there should only be one default grid to choose from
-            MAIN_EXECUTOR.execute(() -> {
-                Trace.beginSection("InvariantDeviceProfile#setFixedLandscape");
-                onConfigChanged(context.getApplicationContext());
-                Trace.endSection();
-            });
-        } else {
-            setCurrentGrid(context, LauncherPrefs.get(context).get(GRID_NAME));
-        }
-    }
-
     private Object[] toModelState() {
         return new Object[]{
                 numColumns, numRows, numSearchContainerColumns, numDatabaseHotseatIcons,
@@ -651,14 +633,14 @@
     }
 
     /**
-     * Parses through the xml to find GridDimension specs. Then calls findBestRowCount to get the
-     * correct row count for this GridOption.
+     * Parses through the xml to find GridSize specs. Then calls findBestGridSize to get the
+     * correct grid size for this GridOption.
      *
-     * @return the result of {@link #findBestRowCount(List, int, int)}.
+     * @return the result of {@link #findBestGridSize(List, int, int)}.
      */
-    private static GridDimension getRowCount(ResourceHelper resourceHelper, Context context,
+    private static GridSize getGridSize(ResourceHelper resourceHelper, Context context,
             Info displayInfo) {
-        ArrayList<GridDimension> rowCounts = new ArrayList<>();
+        ArrayList<GridSize> gridSizes = new ArrayList<>();
 
         try (XmlResourceParser parser = resourceHelper.getXml()) {
             final int depth = parser.getDepth();
@@ -666,8 +648,8 @@
             while (((type = parser.next()) != XmlPullParser.END_TAG
                     || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
                 if ((type == XmlPullParser.START_TAG)
-                        && "GridDimension".equals(parser.getName())) {
-                    rowCounts.add(new GridDimension(context, Xml.asAttributeSet(parser)));
+                        && "GridSize".equals(parser.getName())) {
+                    gridSizes.add(new GridSize(context, Xml.asAttributeSet(parser)));
                 }
             }
         } catch (IOException | XmlPullParserException e) {
@@ -677,49 +659,38 @@
         // Finds the min width and height in dp for all displays.
         int[] dimens = findMinWidthAndHeightDpForDevice(displayInfo);
 
-        return findBestRowCount(rowCounts, dimens[0], dimens[1]);
+        return findBestGridSize(gridSizes, dimens[0], dimens[1]);
     }
 
     /**
-     * @return the biggest row count that fits the display dimensions spec using GridDimension to
-     * determine that. If no best row count is found, return null.
+     * @return the biggest grid size that fits the display dimensions.
+     * If no best grid size is found, return null.
      */
-    private static GridDimension findBestRowCount(List<GridDimension> list, int minWidthDp,
+    private static GridSize findBestGridSize(List<GridSize> list, int minWidthDp,
             int minHeightDp) {
-        GridDimension selectedRow = null;
-        for (GridDimension item: list) {
+        GridSize selectedGridSize = null;
+        for (GridSize item: list) {
             if (minWidthDp >= item.mMinDeviceWidthDp && minHeightDp >= item.mMinDeviceHeightDp) {
-                if (selectedRow == null || selectedRow.mNumGridDimension < item.mNumGridDimension) {
-                    selectedRow = item;
+                if (selectedGridSize == null
+                        || (selectedGridSize.mNumColumns <= item.mNumColumns
+                        && selectedGridSize.mNumRows <= item.mNumRows)) {
+                    selectedGridSize = item;
                 }
             }
         }
-        return selectedRow;
+        return selectedGridSize;
     }
 
     private static int[] findMinWidthAndHeightDpForDevice(Info displayInfo) {
-        int minWidthPx = Integer.MAX_VALUE;
-        int minHeightPx = Integer.MAX_VALUE;
-        for (WindowBounds bounds : displayInfo.supportedBounds) {
-            boolean isTablet = displayInfo.isTablet(bounds);
-            if (isTablet && displayInfo.getDeviceType() == TYPE_MULTI_DISPLAY) {
-                // For split displays, take half width per page.
-                minWidthPx = Math.min(minWidthPx, bounds.availableSize.x / 2);
-                minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
-            } else if (!isTablet && bounds.isLandscape()) {
-                // We will use transposed layout in this case.
-                minWidthPx = Math.min(minWidthPx, bounds.availableSize.y);
-                minHeightPx = Math.min(minHeightPx, bounds.availableSize.x);
-            } else {
-                minWidthPx = Math.min(minWidthPx, bounds.availableSize.x);
-                minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
-            }
+        int minDisplayWidthDp = Integer.MAX_VALUE;
+        int minDisplayHeightDp = Integer.MAX_VALUE;
+        for (CachedDisplayInfo display: displayInfo.getAllDisplays()) {
+            minDisplayWidthDp = Math.min(minDisplayWidthDp,
+                    (int) dpiFromPx(display.size.x, DisplayMetrics.DENSITY_DEVICE_STABLE));
+            minDisplayHeightDp = Math.min(minDisplayHeightDp,
+                    (int) dpiFromPx(display.size.y, DisplayMetrics.DENSITY_DEVICE_STABLE));
         }
-
-        int minWidthDp = (int) dpiFromPx(minWidthPx, DisplayMetrics.DENSITY_DEVICE_STABLE);
-        int minHeightDp = (int) dpiFromPx(minHeightPx, DisplayMetrics.DENSITY_DEVICE_STABLE);
-
-        return new int[]{minWidthDp, minHeightDp};
+        return new int[]{minDisplayWidthDp, minDisplayHeightDp};
     }
 
     /**
@@ -767,7 +738,7 @@
         return parseAllDefinedGridOptions(context, displayInfo)
                 .stream()
                 .filter(go -> go.isEnabled(deviceType))
-                .filter(go -> go.filterByFlag(deviceType, isFixedLandscapeMode))
+                .filter(go -> go.filterByFlag(deviceType, isFixedLandscape))
                 .collect(Collectors.toList());
     }
 
@@ -1037,7 +1008,7 @@
         private final int mWorkspaceCellSpecsTwoPanelId;
         private final int mAllAppsCellSpecsId;
         private final int mAllAppsCellSpecsTwoPanelId;
-        private final int mRowCountSpecsId;
+        private final int mGridSizeSpecsId;
         private final boolean mIsFixedLandscape;
         private final boolean mIsOldGrid;
 
@@ -1048,18 +1019,20 @@
             title = a.getString(R.styleable.GridDisplayOption_title);
             deviceCategory = a.getInt(R.styleable.GridDisplayOption_deviceCategory,
                     DEVICE_CATEGORY_ALL);
-            mRowCountSpecsId = a.getResourceId(
-                    R.styleable.GridDisplayOption_rowCountSpecsId, INVALID_RESOURCE_HANDLE);
+            mGridSizeSpecsId = a.getResourceId(
+                    R.styleable.GridDisplayOption_gridSizeSpecsId, INVALID_RESOURCE_HANDLE);
             mIsDualGrid = a.getBoolean(R.styleable.GridDisplayOption_isDualGrid, false);
-            if (mRowCountSpecsId != INVALID_RESOURCE_HANDLE) {
-                ResourceHelper resourceHelper = new ResourceHelper(context, mRowCountSpecsId);
-                GridDimension numR = getRowCount(resourceHelper, context, displayInfo);
-                numRows = numR.mNumGridDimension;
-                dbFile = numR.mDbFile;
-                defaultLayoutId = numR.mDefaultLayoutId;
-                demoModeLayoutId = numR.mDemoModeLayoutId;
+            if (mGridSizeSpecsId != INVALID_RESOURCE_HANDLE) {
+                ResourceHelper resourceHelper = new ResourceHelper(context, mGridSizeSpecsId);
+                GridSize gridSize = getGridSize(resourceHelper, context, displayInfo);
+                numColumns = gridSize.mNumColumns;
+                numRows = gridSize.mNumRows;
+                dbFile = gridSize.mDbFile;
+                defaultLayoutId = gridSize.mDefaultLayoutId;
+                demoModeLayoutId = gridSize.mDemoModeLayoutId;
             } else {
                 numRows = a.getInt(R.styleable.GridDisplayOption_numRows, 0);
+                numColumns = a.getInt(R.styleable.GridDisplayOption_numColumns, 0);
                 dbFile = a.getString(R.styleable.GridDisplayOption_dbFile);
                 defaultLayoutId = a.getResourceId(
                         R.styleable.GridDisplayOption_defaultLayoutId, 0);
@@ -1067,7 +1040,6 @@
                         R.styleable.GridDisplayOption_demoModeLayoutId, defaultLayoutId);
             }
 
-            numColumns = a.getInt(R.styleable.GridDisplayOption_numColumns, 0);
             numSearchContainerColumns = a.getInt(
                     R.styleable.GridDisplayOption_numSearchContainerColumns, numColumns);
 
@@ -1230,7 +1202,7 @@
             }
 
             // Here we return true if we want to show the new grids.
-            if (mRowCountSpecsId != INVALID_RESOURCE_HANDLE) {
+            if (mGridSizeSpecsId != INVALID_RESOURCE_HANDLE) {
                 return Flags.oneGridSpecs();
             }
 
@@ -1243,26 +1215,28 @@
         }
     }
 
-    public static final class GridDimension {
-        final int mNumGridDimension;
-        final int mMinDeviceWidthDp;
-        final int mMinDeviceHeightDp;
+    public static final class GridSize {
+        final int mNumRows;
+        final int mNumColumns;
+        final float mMinDeviceWidthDp;
+        final float mMinDeviceHeightDp;
         final String mDbFile;
         final int mDefaultLayoutId;
         final int mDemoModeLayoutId;
 
 
-        GridDimension(Context context, AttributeSet attrs) {
-            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GridDimension);
+        GridSize(Context context, AttributeSet attrs) {
+            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GridSize);
 
-            mNumGridDimension = (int) a.getFloat(R.styleable.GridDimension_numGridDimension, 0);
-            mMinDeviceWidthDp = a.getInt(R.styleable.GridDimension_minDeviceWidthDp, 0);
-            mMinDeviceHeightDp = a.getInt(R.styleable.GridDimension_minDeviceHeightDp, 0);
-            mDbFile = a.getString(R.styleable.GridDimension_dbFile);
+            mNumRows = (int) a.getFloat(R.styleable.GridSize_numGridRows, 0);
+            mNumColumns = (int) a.getFloat(R.styleable.GridSize_numGridColumns, 0);
+            mMinDeviceWidthDp = a.getFloat(R.styleable.GridSize_minDeviceWidthDp, 0);
+            mMinDeviceHeightDp = a.getFloat(R.styleable.GridSize_minDeviceHeightDp, 0);
+            mDbFile = a.getString(R.styleable.GridSize_dbFile);
             mDefaultLayoutId = a.getResourceId(
-                    R.styleable.GridDimension_defaultLayoutId, 0);
+                    R.styleable.GridSize_defaultLayoutId, 0);
             mDemoModeLayoutId = a.getResourceId(
-                    R.styleable.GridDimension_demoModeLayoutId, mDefaultLayoutId);
+                    R.styleable.GridSize_demoModeLayoutId, mDefaultLayoutId);
 
             a.recycle();
         }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 8981024..5b8d2fc 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -46,6 +46,7 @@
 import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE_PENDING_ACTIVITY_RESULT;
 import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE_PENDING_REQUEST_ARGS;
 import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE_PENDING_REQUEST_CODE;
+import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE_RECREATE_TO_UPDATE_THEME;
 import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE_WIDGET_PANEL;
 import static com.android.launcher3.LauncherConstants.TraceEvents.COLD_STARTUP_TRACE_COOKIE;
 import static com.android.launcher3.LauncherConstants.TraceEvents.COLD_STARTUP_TRACE_METHOD_NAME;
@@ -422,6 +423,8 @@
     private final SettingsCache.OnChangeListener mNaturalScrollingChangedListener =
             enabled -> mIsNaturalScrollingEnabled = enabled;
 
+    private boolean mRecreateToUpdateTheme = false;
+
     public static Launcher getLauncher(Context context) {
         return fromContext(context);
     }
@@ -789,7 +792,7 @@
             LauncherPrefs.get(this).put(LauncherPrefs.ALLOW_ROTATION, false);
         }
         getRotationHelper().setFixedLandscape(
-                Objects.requireNonNull(mDeviceProfile.inv).isFixedLandscapeMode
+                Objects.requireNonNull(mDeviceProfile.inv).isFixedLandscape
         );
     }
 
@@ -1352,7 +1355,8 @@
 
         NonConfigInstance lastInstance = (NonConfigInstance) getLastNonConfigurationInstance();
         boolean forceRestore = lastInstance != null
-                && (lastInstance.config.diff(mOldConfig) & CONFIG_UI_MODE) != 0;
+                && ((lastInstance.config.diff(mOldConfig) & CONFIG_UI_MODE) != 0
+                || savedState.getBoolean(RUNTIME_STATE_RECREATE_TO_UPDATE_THEME));
         if (forceRestore || !state.shouldDisableRestore()) {
             mStateManager.goToState(state, false /* animated */);
         }
@@ -1747,6 +1751,12 @@
     }
 
     @Override
+    protected void recreateToUpdateTheme() {
+        mRecreateToUpdateTheme = true;
+        super.recreateToUpdateTheme();
+    }
+
+    @Override
     public void onRestoreInstanceState(Bundle state) {
         super.onRestoreInstanceState(state);
         IntSet synchronouslyBoundPages = mModelCallbacks.getSynchronouslyBoundPages();
@@ -1791,6 +1801,8 @@
             outState.putParcelable(RUNTIME_STATE_PENDING_ACTIVITY_RESULT, mPendingActivityResult);
         }
 
+        outState.putBoolean(RUNTIME_STATE_RECREATE_TO_UPDATE_THEME, mRecreateToUpdateTheme);
+
         super.onSaveInstanceState(outState);
     }
 
diff --git a/src/com/android/launcher3/LauncherConstants.java b/src/com/android/launcher3/LauncherConstants.java
index 445fb41..0ed239d 100644
--- a/src/com/android/launcher3/LauncherConstants.java
+++ b/src/com/android/launcher3/LauncherConstants.java
@@ -67,5 +67,8 @@
         static final String RUNTIME_STATE_WIDGET_PANEL = "launcher.widget_panel";
         // Type int[]
         static final String RUNTIME_STATE_CURRENT_SCREEN_IDS = "launcher.current_screen_ids";
+        // Type: boolean
+        static final String RUNTIME_STATE_RECREATE_TO_UPDATE_THEME =
+                "launcher.recreate_to_update_theme";
     }
 }
diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java
index a5b8168..c702414 100644
--- a/src/com/android/launcher3/LauncherFiles.java
+++ b/src/com/android/launcher3/LauncherFiles.java
@@ -24,6 +24,7 @@
     public static final String LAUNCHER_4_BY_4_DB = "launcher_4_by_4.db";
     public static final String LAUNCHER_3_BY_3_DB = "launcher_3_by_3.db";
     public static final String LAUNCHER_2_BY_2_DB = "launcher_2_by_2.db";
+    public static final String LAUNCHER_7_BY_3_DB = "launcher_7_by_3.db";
     public static final String LAUNCHER_8_BY_3_DB = "launcher_8_by_3.db";
     public static final String BACKUP_DB = "backup.db";
     public static final String SHARED_PREFERENCES_KEY = "com.android.launcher3.prefs";
@@ -45,6 +46,7 @@
             LAUNCHER_4_BY_4_DB,
             LAUNCHER_3_BY_3_DB,
             LAUNCHER_2_BY_2_DB,
+            LAUNCHER_7_BY_3_DB,
             LAUNCHER_8_BY_3_DB));
 
     public static final List<String> OTHER_FILES = Collections.unmodifiableList(Arrays.asList(
diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java
index d645734..a5b95c7 100644
--- a/src/com/android/launcher3/LauncherRootView.java
+++ b/src/com/android/launcher3/LauncherRootView.java
@@ -10,7 +10,6 @@
 import android.view.WindowInsets;
 
 import com.android.launcher3.graphics.SysUiScrim;
-import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.util.window.WindowManagerProxy;
 
@@ -56,7 +55,10 @@
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
         mStatefulContainer.handleConfigurationChanged(
                 mStatefulContainer.getContext().getResources().getConfiguration());
+        return updateInsets(insets);
+    }
 
+    private WindowInsets updateInsets(WindowInsets insets) {
         insets = WindowManagerProxy.INSTANCE.get(getContext())
                 .normalizeWindowInsets(getContext(), insets, mTempRect);
         handleSystemWindowInsets(mTempRect);
@@ -74,7 +76,11 @@
     }
 
     public void dispatchInsets() {
-        mStatefulContainer.getDeviceProfile().updateInsets(mInsets);
+        if (isAttachedToWindow()) {
+            updateInsets(getRootWindowInsets());
+        } else {
+            mStatefulContainer.getDeviceProfile().updateInsets(mInsets);
+        }
         super.setInsets(mInsets);
     }
 
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 814d142..68a6e62 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -48,6 +48,8 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.ArrowPopup;
 import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.shortcuts.DeepShortcutTextView;
+import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
@@ -104,11 +106,15 @@
                 R.string.action_deep_shortcut, KeyEvent.KEYCODE_S));
     }
 
+    private static boolean isNotInShortcutMenu(@Nullable View view) {
+        return view == null || !(view.getParent() instanceof DeepShortcutView);
+    }
+
     @Override
     protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) {
         // If the request came from keyboard, do not add custom shortcuts as that is already
         // exposed as a direct shortcut
-        if (ShortcutUtil.supportsShortcuts(item)) {
+        if (isNotInShortcutMenu(host) && ShortcutUtil.supportsShortcuts(item)) {
             out.add(mActions.get(DEEP_SHORTCUTS));
         }
 
@@ -415,7 +421,6 @@
                         screenId, coordinates[0], coordinates[1]);
 
                 bindItem(info, accessibility, finishCallback);
-                announceConfirmation(R.string.item_added_to_workspace);
             } else if (item instanceof PendingAddItemInfo) {
                 PendingAddItemInfo info = (PendingAddItemInfo) item;
                 if (info instanceof PendingAddWidgetInfo widgetInfo
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index df383bf..b6ba264 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -125,6 +125,12 @@
         }
 
         @Override
+        public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
+                RecyclerView.State state) {
+            return mAppsPerRow;
+        }
+
+        @Override
         public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
                 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
             super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info);
diff --git a/src/com/android/launcher3/allapps/WorkPausedCard.java b/src/com/android/launcher3/allapps/WorkPausedCard.java
index e1eeabe..a14ac98 100644
--- a/src/com/android/launcher3/allapps/WorkPausedCard.java
+++ b/src/com/android/launcher3/allapps/WorkPausedCard.java
@@ -79,7 +79,6 @@
 
     @Override
     public void onClick(View view) {
-        setEnabled(false);
         mActivityContext.getAppsView().getWorkManager().setWorkProfileEnabled(true);
         mActivityContext.getStatsLogManager().logger().log(LAUNCHER_TURN_ON_WORK_APPS_TAP);
     }
diff --git a/src/com/android/launcher3/allapps/WorkUtilityView.java b/src/com/android/launcher3/allapps/WorkUtilityView.java
index 4b58ab0..5949b78 100644
--- a/src/com/android/launcher3/allapps/WorkUtilityView.java
+++ b/src/com/android/launcher3/allapps/WorkUtilityView.java
@@ -33,6 +33,7 @@
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 import androidx.core.graphics.Insets;
 import androidx.core.view.WindowInsetsCompat;
 
@@ -126,6 +127,7 @@
         setInsets(mActivityContext.getDeviceProfile().getInsets());
         updateStringFromCache();
         mSchedulerButton.setVisibility(GONE);
+        mSchedulerButton.setOnClickListener(null);
         if (shouldUseScheduler()) {
             mSchedulerButton.setVisibility(VISIBLE);
             mSchedulerButton.setOnClickListener(view ->
@@ -393,7 +395,13 @@
         }
     }
 
-    private boolean shouldUseScheduler() {
+    @VisibleForTesting
+    boolean shouldUseScheduler() {
         return Flags.workSchedulerInWorkProfile() && !mWorkSchedulerIntentAction.isEmpty();
     }
+
+    @VisibleForTesting
+    ImageButton getSchedulerButton() {
+        return mSchedulerButton;
+    }
 }
diff --git a/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt b/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt
index b05539a..1502811 100644
--- a/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt
+++ b/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt
@@ -16,11 +16,16 @@
     @Retention(AnnotationRetention.SOURCE)
     @StringDef(
         RestoreError.PROFILE_DELETED,
-        RestoreError.MISSING_INFO,
         RestoreError.MISSING_WIDGET_PROVIDER,
-        RestoreError.INVALID_LOCATION,
+        RestoreError.OVERLAPPING_ITEM,
+        RestoreError.INVALID_WIDGET_SIZE,
+        RestoreError.INVALID_WIDGET_CONTAINER,
         RestoreError.SHORTCUT_NOT_FOUND,
-        RestoreError.APP_NOT_INSTALLED,
+        RestoreError.APP_NO_TARGET_PACKAGE,
+        RestoreError.APP_NO_DB_INTENT,
+        RestoreError.APP_NO_LAUNCH_INTENT,
+        RestoreError.APP_NOT_RESTORED_OR_INSTALLING,
+        RestoreError.APP_NOT_INSTALLED_EXTERNAL_MEDIA,
         RestoreError.WIDGETS_DISABLED,
         RestoreError.PROFILE_NOT_RESTORED,
         RestoreError.WIDGET_REMOVED,
@@ -28,15 +33,24 @@
         RestoreError.GRID_MIGRATION_FAILURE,
         RestoreError.NO_SEARCH_WIDGET,
         RestoreError.INVALID_WIDGET_ID,
+        RestoreError.OTHER_WIDGET_INFLATION_FAIL,
+        RestoreError.UNSPECIFIED_WIDGET_INFLATION_RESULT,
+        RestoreError.UNRESTORED_PENDING_WIDGET,
+        RestoreError.INVALID_CUSTOM_WIDGET_ID,
     )
     annotation class RestoreError {
         companion object {
             const val PROFILE_DELETED = "user_profile_deleted"
-            const val MISSING_INFO = "missing_information_when_loading"
             const val MISSING_WIDGET_PROVIDER = "missing_widget_provider"
-            const val INVALID_LOCATION = "invalid_size_or_location"
+            const val OVERLAPPING_ITEM = "overlapping_item"
+            const val INVALID_WIDGET_SIZE = "invalid_widget_size"
+            const val INVALID_WIDGET_CONTAINER = "invalid_widget_container"
             const val SHORTCUT_NOT_FOUND = "shortcut_not_found"
-            const val APP_NOT_INSTALLED = "app_not_installed"
+            const val APP_NO_TARGET_PACKAGE = "app_no_target_package"
+            const val APP_NO_DB_INTENT = "app_no_db_intent"
+            const val APP_NO_LAUNCH_INTENT = "app_no_launch_intent"
+            const val APP_NOT_RESTORED_OR_INSTALLING = "app_not_restored_or_installed"
+            const val APP_NOT_INSTALLED_EXTERNAL_MEDIA = "app_not_installed_external_media"
             const val WIDGETS_DISABLED = "widgets_disabled"
             const val PROFILE_NOT_RESTORED = "profile_not_restored"
             const val DATABASE_FILE_NOT_RESTORED = "db_file_not_restored"
@@ -44,6 +58,10 @@
             const val GRID_MIGRATION_FAILURE = "grid_migration_failed"
             const val NO_SEARCH_WIDGET = "no_search_widget"
             const val INVALID_WIDGET_ID = "invalid_widget_id"
+            const val OTHER_WIDGET_INFLATION_FAIL = "other_widget_fail"
+            const val UNSPECIFIED_WIDGET_INFLATION_RESULT = "unspecified_widget_inflation_result"
+            const val UNRESTORED_PENDING_WIDGET = "unrestored_pending_widget"
+            const val INVALID_CUSTOM_WIDGET_ID = "invalid_custom_widget_id"
         }
     }
 
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index fb486f7..1e3df1e 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -24,6 +24,7 @@
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.util.ApiWrapper;
 import com.android.launcher3.util.DaggerSingletonTracker;
+import com.android.launcher3.util.DynamicResource;
 import com.android.launcher3.util.MSDLPlayerWrapper;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PluginManagerWrapper;
@@ -48,6 +49,7 @@
     ApiWrapper getApiWrapper();
     ContextualEduStatsManager getContextualEduStatsManager();
     CustomWidgetManager getCustomWidgetManager();
+    DynamicResource getDynamicResource();
     IconShape getIconShape();
     InstallSessionHelper getInstallSessionHelper();
     ItemInstallQueue getItemInstallQueue();
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 84130c7..c01b1b6 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -498,7 +498,7 @@
                 mRestoreEventLogger.logSingleFavoritesItemRestored(itemType);
             }
         } else {
-            markDeleted("Item position overlap", RestoreError.INVALID_LOCATION);
+            markDeleted("Item position overlap", RestoreError.OVERLAPPING_ITEM);
         }
     }
 
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 83eace8..f96e959 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -389,7 +389,7 @@
             }
         } catch (CancellationException e) {
             // Loader stopped, ignore
-            logASplit("Cancelled");
+            FileLog.w(TAG, "LoaderTask cancelled:", e);
         } catch (Exception e) {
             memoryLogger.printLogs();
             throw e;
@@ -398,6 +398,7 @@
     }
 
     public synchronized void stopLocked() {
+        FileLog.w(TAG, "LoaderTask#stopLocked:", new Exception());
         mStopped = true;
         this.notify();
     }
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index c02336e..e86b592 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -141,7 +141,7 @@
         var allowMissingTarget = false
         var intent = c.parseIntent()
         if (intent == null) {
-            c.markDeleted("Null intent from db for item id=${c.id}", RestoreError.MISSING_INFO)
+            c.markDeleted("Null intent from db for item id=${c.id}", RestoreError.APP_NO_DB_INTENT)
             return
         }
         var disabledState =
@@ -151,7 +151,10 @@
         val cn = intent.component
         val targetPkg = cn?.packageName ?: intent.getPackage()
         if (targetPkg.isNullOrEmpty()) {
-            c.markDeleted("No target package for item id=${c.id}", RestoreError.MISSING_INFO)
+            c.markDeleted(
+                "No target package for item id=${c.id}",
+                RestoreError.APP_NO_TARGET_PACKAGE,
+            )
             return
         }
         val appInfoWrapper = ApplicationInfoWrapper(app.context, targetPkg, c.user)
@@ -180,7 +183,7 @@
                     c.markDeleted(
                         "No Activities found for id=${c.id}, targetPkg=$targetPkg, component=$cn." +
                             " Unable to create launch Intent.",
-                        RestoreError.MISSING_INFO,
+                        RestoreError.APP_NO_LAUNCH_INTENT,
                     )
                     return
                 }
@@ -215,7 +218,7 @@
                             else -> {
                                 c.markDeleted(
                                     "removing app that is not restored and not installing. package: $targetPkg",
-                                    RestoreError.APP_NOT_INSTALLED,
+                                    RestoreError.APP_NOT_RESTORED_OR_INSTALLING,
                                 )
                                 return
                             }
@@ -240,7 +243,7 @@
                         // Do not wait for external media load anymore.
                         c.markDeleted(
                             "Invalid package removed: $targetPkg",
-                            RestoreError.APP_NOT_INSTALLED,
+                            RestoreError.APP_NOT_INSTALLED_EXTERNAL_MEDIA,
                         )
                         return
                     }
@@ -448,7 +451,7 @@
                     ", id=${c.id}," +
                     ", appWidgetId=${c.appWidgetId}," +
                     ", component=${component}",
-                RestoreError.INVALID_LOCATION,
+                RestoreError.INVALID_WIDGET_SIZE,
             )
             return
         }
@@ -459,7 +462,7 @@
                     ", appWidgetId=${c.appWidgetId}," +
                     ", component=${component}," +
                     ", container=${c.container}",
-                RestoreError.INVALID_LOCATION,
+                RestoreError.INVALID_WIDGET_CONTAINER,
             )
             return
         }
@@ -500,7 +503,7 @@
                             ", appWidgetId=${c.appWidgetId}" +
                             ", component=${component}" +
                             ", restoreFlag:=${c.restoreFlag}",
-                        RestoreError.APP_NOT_INSTALLED,
+                        RestoreError.UNRESTORED_PENDING_WIDGET,
                     )
                     return
                 } else if (
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index 5068b48..6008287 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -23,6 +23,7 @@
 import static com.android.launcher3.BuildConfig.IS_DEBUG_DEVICE;
 import static com.android.launcher3.BuildConfig.IS_STUDIO_BUILD;
 import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY;
+import static com.android.launcher3.InvariantDeviceProfile.TYPE_TABLET;
 import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
 
 import android.app.Activity;
@@ -315,7 +316,9 @@
                     if (!Flags.oneGridSpecs()
                             // adding this condition until fixing b/378972567
                             || InvariantDeviceProfile.INSTANCE.get(getContext()).deviceType
-                            == TYPE_MULTI_DISPLAY) {
+                            == TYPE_MULTI_DISPLAY
+                            || InvariantDeviceProfile.INSTANCE.get(getContext()).deviceType
+                            == TYPE_TABLET) {
                         return false;
                     }
                     // When the setting changes rotate the screen accordingly to showcase the result
diff --git a/src/com/android/launcher3/util/DynamicResource.java b/src/com/android/launcher3/util/DynamicResource.java
index fbdb5c2..fe9fd7c 100644
--- a/src/com/android/launcher3/util/DynamicResource.java
+++ b/src/com/android/launcher3/util/DynamicResource.java
@@ -22,35 +22,39 @@
 import androidx.annotation.FractionRes;
 import androidx.annotation.IntegerRes;
 
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.ResourceProvider;
 
+import javax.inject.Inject;
+
 /**
  * Utility class to support customizing resource values using plugins
  *
  * To load resources, call
- *    DynamicResource.provider(context).getInt(resId) or any other supported methods
+ * DynamicResource.provider(context).getInt(resId) or any other supported methods
  *
  * To allow customization for a particular resource, add them to dynamic_resources.xml
  */
+@LauncherAppSingleton
 public class DynamicResource implements
-        ResourceProvider, PluginListener<ResourceProvider>, SafeCloseable {
+        ResourceProvider, PluginListener<ResourceProvider> {
 
-    private static final MainThreadInitializedObject<DynamicResource> INSTANCE =
-            new MainThreadInitializedObject<>(DynamicResource::new);
+    private static final DaggerSingletonObject<DynamicResource> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getDynamicResource);
 
     private final Context mContext;
     private ResourceProvider mPlugin;
 
-    private DynamicResource(Context context) {
+    @Inject
+    public DynamicResource(@ApplicationContext Context context,
+            PluginManagerWrapper pluginManagerWrapper, DaggerSingletonTracker tracker) {
         mContext = context;
-        PluginManagerWrapper.INSTANCE.get(context).addPluginListener(this,
+        pluginManagerWrapper.addPluginListener(this,
                 ResourceProvider.class, false /* allowedMultiple */);
-    }
-
-    @Override
-    public void close() {
-        PluginManagerWrapper.INSTANCE.get(mContext).removePluginListener(this);
+        tracker.addCloseable(() -> pluginManagerWrapper.removePluginListener(this));
     }
 
     @Override
diff --git a/src/com/android/launcher3/util/coroutines/DispatcherProvider.kt b/src/com/android/launcher3/util/coroutines/DispatcherProvider.kt
index e9691a8..8877535 100644
--- a/src/com/android/launcher3/util/coroutines/DispatcherProvider.kt
+++ b/src/com/android/launcher3/util/coroutines/DispatcherProvider.kt
@@ -17,18 +17,44 @@
 package com.android.launcher3.util.coroutines
 
 import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.DelicateCoroutinesApi
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.newFixedThreadPoolContext
 
 interface DispatcherProvider {
     val default: CoroutineDispatcher
-    val io: CoroutineDispatcher
+    val background: CoroutineDispatcher
     val main: CoroutineDispatcher
     val unconfined: CoroutineDispatcher
 }
 
 object ProductionDispatchers : DispatcherProvider {
+    private val bgDispatcher = CoroutinesHelper.bgDispatcher()
+
     override val default: CoroutineDispatcher = Dispatchers.Default
-    override val io: CoroutineDispatcher = Dispatchers.IO
+    override val background: CoroutineDispatcher = bgDispatcher
     override val main: CoroutineDispatcher = Dispatchers.Main
     override val unconfined: CoroutineDispatcher = Dispatchers.Unconfined
 }
+
+private object CoroutinesHelper {
+    /**
+     * Default Coroutine dispatcher for background operations.
+     *
+     * Note that this is explicitly limiting the number of threads. In the past, we used
+     * [Dispatchers.IO]. This caused >40 threads to be spawned, and a lot of thread list lock
+     * contention between then, eventually causing jank.
+     */
+    @OptIn(DelicateCoroutinesApi::class)
+    fun bgDispatcher(): CoroutineDispatcher {
+        // Why a new ThreadPool instead of just using Dispatchers.IO with
+        // CoroutineDispatcher.limitedParallelism? Because, if we were to use Dispatchers.IO, we
+        // would share those threads with other dependencies using Dispatchers.IO.
+        // Using a dedicated thread pool we have guarantees only Launcher is able to schedule
+        // code on those.
+        return newFixedThreadPoolContext(
+            nThreads = Runtime.getRuntime().availableProcessors(),
+            name = "LauncherBg",
+        )
+    }
+}
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 44ab966..0cf1f2e 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -68,6 +68,8 @@
 
     private static final String TRACE_METHOD_NAME = "appwidget load-widget ";
 
+    private static final Integer NO_LAYOUT_ID = Integer.valueOf(0);
+
     private final CheckLongPressHelper mLongPressHelper;
     protected final ActivityContext mActivityContext;
 
@@ -164,6 +166,21 @@
         return false;
     }
 
+    private boolean isTaggedAsScrollable() {
+        // TODO: Introduce new api in AppWidgetHostView to indicate whether the widget is
+        // scrollable.
+        for (int i = 0; i < this.getChildCount(); i++) {
+            View child = this.getChildAt(i);
+            final Integer layoutId = (Integer) child.getTag(android.R.id.widget_frame);
+            if (layoutId != null) {
+                // The layout id is only set to 0 when RemoteViews is created from
+                // DrawInstructions.
+                return NO_LAYOUT_ID.equals(layoutId);
+            }
+        }
+        return false;
+    }
+
     /**
      * Returns true if the application of {@link RemoteViews} through {@link #updateAppWidget} are
      * currently being deferred.
@@ -266,7 +283,7 @@
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
-        mIsScrollable = checkScrollableRecursively(this);
+        mIsScrollable = isTaggedAsScrollable() || checkScrollableRecursively(this);
     }
 
     /**
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index b7ad95e..9e635a3 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.widget;
 
 import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
 
 import static com.android.launcher3.Flags.enableWidgetTapToAdd;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
@@ -40,6 +41,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewPropertyAnimator;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.Button;
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
@@ -154,7 +156,21 @@
         mWidgetDescription = findViewById(R.id.widget_description);
         mWidgetTextContainer = findViewById(R.id.widget_text_container);
         mWidgetAddButton = findViewById(R.id.widget_add_button);
+
         if (enableWidgetTapToAdd()) {
+
+            setAccessibilityDelegate(new AccessibilityDelegate() {
+                @Override
+                public void onInitializeAccessibilityNodeInfo(View host,
+                        AccessibilityNodeInfo info) {
+                    super.onInitializeAccessibilityNodeInfo(host, info);
+                    String accessibilityLabel = getResources().getString(mWidgetAddButton.isShown()
+                            ? R.string.widget_cell_tap_to_hide_add_button_label
+                            : R.string.widget_cell_tap_to_show_add_button_label);
+                    info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK,
+                            accessibilityLabel));
+                }
+            });
             mWidgetAddButton.setVisibility(INVISIBLE);
         }
     }
diff --git a/src/com/android/launcher3/widget/WidgetInflater.kt b/src/com/android/launcher3/widget/WidgetInflater.kt
index 271c9c2..d6cadc7 100644
--- a/src/com/android/launcher3/widget/WidgetInflater.kt
+++ b/src/com/android/launcher3/widget/WidgetInflater.kt
@@ -30,16 +30,14 @@
 
     private val widgetHelper = WidgetManagerHelper(context)
 
-    fun inflateAppWidget(
-        item: LauncherAppWidgetInfo,
-    ): InflationResult {
+    fun inflateAppWidget(item: LauncherAppWidgetInfo): InflationResult {
         if (item.hasOptionFlag(LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET)) {
             item.providerName = QsbContainerView.getSearchComponentName(context)
             if (item.providerName == null) {
                 return InflationResult(
                     TYPE_DELETE,
                     reason = "search widget removed because search component cannot be found",
-                    restoreErrorType = RestoreError.NO_SEARCH_WIDGET
+                    restoreErrorType = RestoreError.NO_SEARCH_WIDGET,
                 )
             }
         }
@@ -48,7 +46,7 @@
         }
         val appWidgetInfo: LauncherAppWidgetProviderInfo?
         var removalReason = ""
-        @RestoreError var logReason = RestoreError.APP_NOT_INSTALLED
+        @RestoreError var logReason = RestoreError.OTHER_WIDGET_INFLATION_FAIL
         var update = false
 
         if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
@@ -74,7 +72,7 @@
             if (appWidgetInfo == null) {
                 if (item.appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) {
                     removalReason = "CustomWidgetManager cannot find provider from that widget id."
-                    logReason = RestoreError.MISSING_INFO
+                    logReason = RestoreError.INVALID_CUSTOM_WIDGET_ID
                 } else {
                     removalReason =
                         ("AppWidgetManager cannot find provider for that widget id." +
@@ -96,7 +94,7 @@
                     type = TYPE_DELETE,
                     reason =
                         "Removing restored widget: id=${item.appWidgetId} belongs to component ${item.providerName} user ${item.user}, as the provider is null and $removalReason",
-                    restoreErrorType = logReason
+                    restoreErrorType = logReason,
                 )
             }
 
@@ -132,7 +130,7 @@
                         widgetHelper.bindAppWidgetIdIfAllowed(
                             item.appWidgetId,
                             appWidgetInfo,
-                            options
+                            options,
                         )
 
                     // We tried to bind once. If we were not able to bind, we would need to
@@ -189,9 +187,10 @@
     data class InflationResult(
         val type: Int,
         val reason: String? = null,
-        @RestoreError val restoreErrorType: String = RestoreError.APP_NOT_INSTALLED,
+        @RestoreError
+        val restoreErrorType: String = RestoreError.UNSPECIFIED_WIDGET_INFLATION_RESULT,
         val isUpdate: Boolean = false,
-        val widgetInfo: LauncherAppWidgetProviderInfo? = null
+        val widgetInfo: LauncherAppWidgetProviderInfo? = null,
     )
 
     companion object {
diff --git a/tests/Launcher3Tests.xml b/tests/Launcher3Tests.xml
index 270a610..a876860 100644
--- a/tests/Launcher3Tests.xml
+++ b/tests/Launcher3Tests.xml
@@ -48,6 +48,8 @@
 
         <option name="run-command" value="settings put system pointer_location 1" />
         <option name="run-command" value="settings put system show_touches 1" />
+
+        <option name="run-command" value="setprop pixel_legal_joint_permission_v2 true" />
     </target_preparer>
 
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
index ed8b397..7099d38 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -36,10 +36,7 @@
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
 import com.android.launcher3.Utilities.EMPTY_PERSON_ARRAY
-import com.android.launcher3.backuprestore.LauncherRestoreEventLogger
-import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError.Companion.MISSING_INFO
-import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError.Companion.MISSING_WIDGET_PROVIDER
-import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError.Companion.PROFILE_DELETED
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError
 import com.android.launcher3.icons.CacheableShortcutInfo
 import com.android.launcher3.model.data.FolderInfo
 import com.android.launcher3.model.data.IconRequestInfo
@@ -224,7 +221,8 @@
         itemProcessorUnderTest.processItem()
 
         // Then
-        verify(mockCursor).markDeleted("User has been deleted for item id=1", PROFILE_DELETED)
+        verify(mockCursor)
+            .markDeleted("User has been deleted for item id=1", RestoreError.PROFILE_DELETED)
         verify(mockCursor, times(0)).checkAndAddItem(any(), any(), anyOrNull())
     }
 
@@ -237,7 +235,8 @@
         itemProcessorUnderTest = createWorkspaceItemProcessorUnderTest()
         itemProcessorUnderTest.processItem()
         // Then
-        verify(mockCursor).markDeleted("Null intent from db for item id=1", MISSING_INFO)
+        verify(mockCursor)
+            .markDeleted("Null intent from db for item id=1", RestoreError.APP_NO_DB_INTENT)
         verify(mockCursor, times(0)).checkAndAddItem(any(), any(), anyOrNull())
     }
 
@@ -255,7 +254,8 @@
         itemProcessorUnderTest.processItem()
 
         // Then
-        verify(mockCursor).markDeleted("No target package for item id=1", MISSING_INFO)
+        verify(mockCursor)
+            .markDeleted("No target package for item id=1", RestoreError.APP_NO_TARGET_PACKAGE)
         verify(mockCursor, times(0)).checkAndAddItem(any(), any(), anyOrNull())
     }
 
@@ -272,7 +272,8 @@
         itemProcessorUnderTest.processItem()
 
         // Then
-        verify(mockCursor).markDeleted("No target package for item id=1", MISSING_INFO)
+        verify(mockCursor)
+            .markDeleted("No target package for item id=1", RestoreError.APP_NO_TARGET_PACKAGE)
         verify(mockCursor, times(0)).checkAndAddItem(any(), any(), anyOrNull())
     }
 
@@ -350,7 +351,7 @@
                     " targetPkg=package," +
                     " component=ComponentInfo{package/class}." +
                     " Unable to create launch Intent.",
-                MISSING_INFO,
+                RestoreError.APP_NO_LAUNCH_INTENT,
             )
         verify(mockCursor, times(0)).checkAndAddItem(any(), any(), anyOrNull())
     }
@@ -663,7 +664,7 @@
         verify(mockCursor)
             .markDeleted(
                 "processWidget: Unrestored Pending widget removed: id=1, appWidgetId=0, component=$expectedComponentName, restoreFlag:=4",
-                LauncherRestoreEventLogger.RestoreError.APP_NOT_INSTALLED,
+                RestoreError.UNRESTORED_PENDING_WIDGET,
             )
     }
 
@@ -689,7 +690,7 @@
                 type = WidgetInflater.TYPE_DELETE,
                 widgetInfo = null,
                 reason = "test_delete_reason",
-                restoreErrorType = MISSING_WIDGET_PROVIDER,
+                restoreErrorType = RestoreError.MISSING_WIDGET_PROVIDER,
             )
         mockWidgetInflater =
             mock<WidgetInflater>().apply {
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/TestDispatcherProvider.kt b/tests/multivalentTests/src/com/android/launcher3/util/TestDispatcherProvider.kt
index 39e1ec5..3319c53 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/TestDispatcherProvider.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/TestDispatcherProvider.kt
@@ -21,7 +21,7 @@
 
 class TestDispatcherProvider(testDispatcher: CoroutineDispatcher) : DispatcherProvider {
     override val default: CoroutineDispatcher = testDispatcher
-    override val io: CoroutineDispatcher = testDispatcher
+    override val background: CoroutineDispatcher = testDispatcher
     override val main: CoroutineDispatcher = testDispatcher
     override val unconfined: CoroutineDispatcher = testDispatcher
 }
diff --git a/tests/src/com/android/launcher3/allapps/WorkUtilityViewTest.java b/tests/src/com/android/launcher3/allapps/WorkUtilityViewTest.java
new file mode 100644
index 0000000..b88d12e
--- /dev/null
+++ b/tests/src/com/android/launcher3/allapps/WorkUtilityViewTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.allapps;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.launcher3.Flags.FLAG_WORK_SCHEDULER_IN_WORK_PROFILE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+
+import android.content.Context;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.view.ViewGroup;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.util.ActivityContextWrapper;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class WorkUtilityViewTest {
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule =
+            new SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);
+
+    private WorkUtilityView mVut;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        Context context = new ActivityContextWrapper(getApplicationContext(),
+                com.android.launcher3.R.style.DynamicColorsBaseLauncherTheme);
+        mVut = (WorkUtilityView) ViewGroup.inflate(context,
+                com.android.launcher3.R.layout.work_mode_utility_view, null);
+    }
+
+    @Test
+    @EnableFlags(FLAG_WORK_SCHEDULER_IN_WORK_PROFILE)
+    public void testInflateFlagOn_visible() {
+        WorkUtilityView workUtilityView = Mockito.spy(mVut);
+        doReturn(true).when(workUtilityView).shouldUseScheduler();
+
+        workUtilityView.onFinishInflate();
+
+        assertThat(workUtilityView.getSchedulerButton().getVisibility()).isEqualTo(VISIBLE);
+        assertThat(workUtilityView.getSchedulerButton().hasOnClickListeners()).isEqualTo(true);
+    }
+
+    @Test
+    @DisableFlags(FLAG_WORK_SCHEDULER_IN_WORK_PROFILE)
+    public void testInflateFlagOff_gone() {
+        mVut.onFinishInflate();
+
+        assertThat(mVut.getSchedulerButton().getVisibility()).isEqualTo(GONE);
+        assertThat(mVut.getSchedulerButton().hasOnClickListeners()).isEqualTo(false);
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index bb645d7..64ad305 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -123,7 +123,7 @@
                 v instanceof WidgetCell
                         && v.getTag() instanceof PendingAddWidgetInfo pawi
                         && mWidgetInfo.provider.equals(pawi.componentName)));
-        addToWorkspace(widgetView);
+        addWidgetToWorkspace(widgetView);
 
         // Widget id for which the config activity was opened
         mWidgetId = monitor.getWidgetId();
diff --git a/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt b/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt
index 61fa7d5..b5702c9 100644
--- a/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt
+++ b/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt
@@ -170,6 +170,18 @@
         UiDevice.getInstance(getInstrumentation()).waitForIdle()
     }
 
+    /**
+     * Match the behavior with how widget is added in reality with "tap to add" (even with screen
+     * readers).
+     */
+    fun addWidgetToWorkspace(view: View) {
+        executeOnLauncher {
+            view.performClick()
+            UiDevice.getInstance(getInstrumentation()).waitForIdle()
+            view.findViewById<View>(R.id.widget_add_button).performClick()
+        }
+    }
+
     fun ViewGroup.searchView(filter: Predicate<View>): View? {
         if (filter.test(this)) return this
         for (child in children) {