Merge "Add flags for OneGrid project" into main
diff --git a/Android.bp b/Android.bp
index a0187e3..bcbd362 100644
--- a/Android.bp
+++ b/Android.bp
@@ -333,7 +333,7 @@
         "com_android_wm_shell_flags_lib",
         "dagger2",
         "jsr330",
-
+        "com_android_systemui_shared_flags_lib",
     ],
     manifest: "AndroidManifest-common.xml",
     sdk_version: "current",
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index e11b00c..23733a4 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -39,3 +39,12 @@
     bug: "353947137"
 }
 
+flag {
+    name: "enable_overview_command_helper_timeout"
+    namespace: "launcher_overview"
+    description: "Enables OverviewCommandHelper new version with a timeout to prevent the queue to be unresponsive."
+    bug: "351122926"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
\ No newline at end of file
diff --git a/quickstep/res/layout/digital_wellbeing_toast.xml b/quickstep/res/layout/digital_wellbeing_toast.xml
index 3973e56..0551c12 100644
--- a/quickstep/res/layout/digital_wellbeing_toast.xml
+++ b/quickstep/res/layout/digital_wellbeing_toast.xml
@@ -27,4 +27,5 @@
     android:textColor="?attr/materialColorOnSecondaryFixed"
     android:textSize="14sp"
     android:autoSizeTextType="uniform"
-    android:autoSizeMaxTextSize="14sp"/>
\ No newline at end of file
+    android:autoSizeMaxTextSize="14sp"
+    android:visibility="gone"/>
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index bdfd241..760bcdb 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -49,6 +49,5 @@
         android:layout_width="wrap_content" />
 
     <include layout="@layout/digital_wellbeing_toast"
-        android:id="@+id/digital_wellbeing_toast"
-        android:visibility="invisible"/>
+        android:id="@+id/digital_wellbeing_toast"/>
 </com.android.quickstep.views.TaskView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index 00a990b..c36a45e 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -75,10 +75,8 @@
         android:layout_width="wrap_content" />
 
     <include layout="@layout/digital_wellbeing_toast"
-        android:id="@+id/digital_wellbeing_toast"
-        android:visibility="invisible"/>
+        android:id="@+id/digital_wellbeing_toast"/>
 
     <include layout="@layout/digital_wellbeing_toast"
-        android:id="@+id/bottomRight_digital_wellbeing_toast"
-        android:visibility="invisible"/>
+        android:id="@+id/bottomRight_digital_wellbeing_toast"/>
 </com.android.quickstep.views.GroupedTaskView>
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index 8ceb77d..ce96556 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -36,14 +36,15 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
 import com.android.quickstep.util.BorderAnimator;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
-import java.util.function.Consumer;
-
 import kotlin.Unit;
 
+import java.util.function.Consumer;
+
 /**
  * A view that displays a recent task during a keyboard quick switch.
  */
@@ -173,6 +174,61 @@
         });
     }
 
+    protected void setThumbnailsForSplitTasks(
+            @NonNull Task task1,
+            @Nullable Task task2,
+            @Nullable ThumbnailUpdateFunction thumbnailUpdateFunction,
+            @Nullable IconUpdateFunction iconUpdateFunction,
+            @Nullable SplitBounds splitBounds) {
+        setThumbnails(task1, task2, thumbnailUpdateFunction, iconUpdateFunction);
+
+        if (splitBounds == null) {
+            return;
+        }
+
+
+        final boolean isLeftRightSplit = !splitBounds.appsStackedVertically;
+        final float leftOrTopTaskPercent = isLeftRightSplit
+                ? splitBounds.leftTaskPercent : splitBounds.topTaskPercent;
+
+        ConstraintLayout.LayoutParams leftTopParams = (ConstraintLayout.LayoutParams)
+                mThumbnailView1.getLayoutParams();
+        ConstraintLayout.LayoutParams rightBottomParams = (ConstraintLayout.LayoutParams)
+                mThumbnailView2.getLayoutParams();
+
+        if (isLeftRightSplit) {
+            // Set thumbnail view ratio in left right split mode.
+            leftTopParams.width = 0; // Set width to 0dp, so it uses the constraint dimension ratio.
+            leftTopParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT;
+            leftTopParams.matchConstraintPercentWidth = leftOrTopTaskPercent;
+            leftTopParams.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID;
+            leftTopParams.rightToLeft = R.id.thumbnail_2;
+            mThumbnailView1.setLayoutParams(leftTopParams);
+
+            rightBottomParams.width = 0;
+            rightBottomParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT;
+            rightBottomParams.matchConstraintPercentWidth = 1 - leftOrTopTaskPercent;
+            rightBottomParams.leftToRight = R.id.thumbnail_1;
+            rightBottomParams.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID;
+            mThumbnailView2.setLayoutParams(rightBottomParams);
+        } else {
+            // Set thumbnail view ratio in top bottom split mode.
+            leftTopParams.height = 0;
+            leftTopParams.width = ConstraintLayout.LayoutParams.MATCH_PARENT;
+            leftTopParams.matchConstraintPercentHeight = leftOrTopTaskPercent;
+            leftTopParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
+            leftTopParams.bottomToTop = R.id.thumbnail_2;
+            mThumbnailView1.setLayoutParams(leftTopParams);
+
+            rightBottomParams.height = 0;
+            rightBottomParams.width = ConstraintLayout.LayoutParams.MATCH_PARENT;
+            rightBottomParams.matchConstraintPercentHeight = 1 - leftOrTopTaskPercent;
+            rightBottomParams.topToBottom = R.id.thumbnail_1;
+            rightBottomParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
+            mThumbnailView2.setLayoutParams(rightBottomParams);
+        }
+    }
+
     private void applyThumbnail(
             @Nullable ImageView thumbnailView,
             @Nullable Task task,
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index a527c82..b4102a9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -214,11 +214,13 @@
                     groupTask.mSplitBounds == null
                             || groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id
                             || groupTask.task2 == null;
-            currentTaskView.setThumbnails(
+
+            currentTaskView.setThumbnailsForSplitTasks(
                     firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2,
                     firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1,
                     updateTasks ? mViewCallbacks::updateThumbnailInBackground : null,
-                    updateTasks ? mViewCallbacks::updateIconInBackground : null);
+                    updateTasks ? mViewCallbacks::updateIconInBackground : null,
+                    groupTask.mSplitBounds);
 
             previousTaskView = currentTaskView;
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 47ae741..5f733b0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -1431,7 +1431,7 @@
                                 && !(foundTaskView instanceof DesktopTaskView)) {
                             TestLogging.recordEvent(
                                     TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon");
-                            foundTaskView.launchTasks();
+                            foundTaskView.launchWithAnimation();
                             return;
                         }
                     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 55c1885..0b385d9 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -610,7 +610,7 @@
                                         .append(" is missing."),
                                 QUICK_SWITCH_FROM_HOME_FALLBACK);
                     }
-                    taskToLaunch.launchTask(success -> {
+                    taskToLaunch.launchWithoutAnimation(success -> {
                         if (!success) {
                             getStateManager().goToState(OVERVIEW);
                         } else {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 6822f1b..b165cdd 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -209,7 +209,7 @@
         TaskView taskView = recentsView.getRunningTaskView();
         if (taskView != null) {
             if (recentsView.isTaskViewFullyVisible(taskView)) {
-                taskView.launchTasks();
+                taskView.launchWithAnimation();
             } else {
                 recentsView.snapToPage(recentsView.indexOfChild(taskView));
             }
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 38d08e0..55489bb 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -2370,7 +2370,7 @@
                     ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED);
                 }
                 ActiveGestureLog.INSTANCE.addLog(nextTaskLog);
-                nextTask.launchTask(success -> {
+                nextTask.launchWithoutAnimation(true, success -> {
                     resultCallback.accept(success);
                     if (success) {
                         if (hasTaskPreviouslyAppeared) {
@@ -2383,7 +2383,7 @@
                         }
                     }
                     return Unit.INSTANCE;
-                }, true /* freezeTaskList */);
+                }  /* freezeTaskList */);
             } else {
                 mContainerInterface.onLaunchTaskFailed();
                 Toast.makeText(mContext, R.string.activity_not_available, LENGTH_SHORT).show();
diff --git a/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt b/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
index 904ed69..e6822ff 100644
--- a/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
+++ b/quickstep/src/com/android/quickstep/DeviceConfigWrapper.kt
@@ -52,7 +52,11 @@
         )
 
     val lpnhTimeoutMs =
-        propReader.get("LPNH_TIMEOUT_MS", 450, "Controls lpnh timeout in milliseconds")
+        propReader.get(
+            "LPNH_TIMEOUT_MS",
+            DEFAULT_LPNH_TIMEOUT_MS,
+            "Controls lpnh timeout in milliseconds"
+        )
 
     val lpnhSlopPercentage =
         propReader.get("LPNH_SLOP_PERCENTAGE", 100, "Controls touch slop percentage for lpnh")
@@ -172,5 +176,7 @@
         @JvmStatic val configHelper by lazy { DeviceConfigHelper(::DeviceConfigWrapper) }
 
         @JvmStatic fun get() = configHelper.config
+
+        const val DEFAULT_LPNH_TIMEOUT_MS = 450
     }
 }
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 8873275..c1f9963 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -25,16 +25,26 @@
 import android.view.View
 import androidx.annotation.BinderThread
 import androidx.annotation.UiThread
+import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.Cuj
+import com.android.launcher3.Flags.enableOverviewCommandHelperTimeout
 import com.android.launcher3.PagedView
 import com.android.launcher3.config.FeatureFlags
 import com.android.launcher3.logger.LauncherAtom
 import com.android.launcher3.logging.StatsLogManager
-import com.android.launcher3.logging.StatsLogManager.LauncherEvent.*
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.RunnableList
+import com.android.launcher3.util.coroutines.DispatcherProvider
+import com.android.launcher3.util.coroutines.ProductionDispatchers
 import com.android.quickstep.OverviewCommandHelper.CommandInfo.CommandStatus
-import com.android.quickstep.OverviewCommandHelper.CommandType.*
+import com.android.quickstep.OverviewCommandHelper.CommandType.HIDE
+import com.android.quickstep.OverviewCommandHelper.CommandType.HOME
+import com.android.quickstep.OverviewCommandHelper.CommandType.KEYBOARD_INPUT
+import com.android.quickstep.OverviewCommandHelper.CommandType.SHOW
+import com.android.quickstep.OverviewCommandHelper.CommandType.TOGGLE
 import com.android.quickstep.util.ActiveGestureLog
 import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.RecentsViewContainer
@@ -43,13 +53,25 @@
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper
 import java.io.PrintWriter
 import java.util.concurrent.ConcurrentLinkedDeque
+import kotlin.coroutines.resume
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeout
 
 /** Helper class to handle various atomic commands for switching between Overview. */
-class OverviewCommandHelper(
+class OverviewCommandHelper
+@JvmOverloads
+constructor(
     private val touchInteractionService: TouchInteractionService,
     private val overviewComponentObserver: OverviewComponentObserver,
-    private val taskAnimationManager: TaskAnimationManager
+    private val taskAnimationManager: TaskAnimationManager,
+    private val dispatcherProvider: DispatcherProvider = ProductionDispatchers,
 ) {
+    private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.default)
+
     private val commandQueue = ConcurrentLinkedDeque<CommandInfo>()
 
     /**
@@ -79,10 +101,10 @@
      * dropped.
      */
     @BinderThread
-    fun addCommand(type: CommandType) {
+    fun addCommand(type: CommandType): CommandInfo? {
         if (commandQueue.size >= MAX_QUEUE_SIZE) {
-            Log.d(TAG, "commands queue is full ($commandQueue). command not added: $type")
-            return
+            Log.d(TAG, "command not added: $type - queue is full ($commandQueue).")
+            return null
         }
 
         val command = CommandInfo(type)
@@ -90,8 +112,17 @@
         Log.d(TAG, "command added: $command")
 
         if (commandQueue.size == 1) {
-            Executors.MAIN_EXECUTOR.execute { executeNext() }
+            Log.d(TAG, "execute: $command - queue size: ${commandQueue.size}")
+            if (enableOverviewCommandHelperTimeout()) {
+                coroutineScope.launch(dispatcherProvider.main) { processNextCommand() }
+            } else {
+                Executors.MAIN_EXECUTOR.execute { processNextCommand() }
+            }
+        } else {
+            Log.d(TAG, "not executed: $command - queue size: ${commandQueue.size}")
         }
+
+        return command
     }
 
     fun canStartHomeSafely(): Boolean = commandQueue.isEmpty() || commandQueue.first().type == HOME
@@ -108,7 +139,7 @@
      * completion (returns false).
      */
     @UiThread
-    private fun executeNext() {
+    private fun processNextCommand() {
         val command: CommandInfo =
             commandQueue.firstOrNull()
                 ?: run {
@@ -119,12 +150,22 @@
         command.status = CommandStatus.PROCESSING
         Log.d(TAG, "executing command: $command")
 
-        val result = executeCommand(command)
-        Log.d(TAG, "command executed: $command with result: $result")
-        if (result) {
-            onCommandFinished(command)
+        if (enableOverviewCommandHelperTimeout()) {
+            coroutineScope.launch(dispatcherProvider.main) {
+                withTimeout(QUEUE_WAIT_DURATION_IN_MS) {
+                    executeCommandSuspended(command)
+                    ensureActive()
+                    onCommandFinished(command)
+                }
+            }
         } else {
-            Log.d(TAG, "waiting for command callback: $command")
+            val result = executeCommand(command, onCallbackResult = { onCommandFinished(command) })
+            Log.d(TAG, "command executed: $command with result: $result")
+            if (result) {
+                onCommandFinished(command)
+            } else {
+                Log.d(TAG, "waiting for command callback: $command")
+            }
         }
     }
 
@@ -132,7 +173,9 @@
      * Executes the task and returns true if next task can be executed. If false, then the next task
      * is deferred until [.scheduleNextTask] is called
      */
-    private fun executeCommand(command: CommandInfo): Boolean {
+    @VisibleForTesting
+    fun executeCommand(command: CommandInfo, onCallbackResult: () -> Unit): Boolean {
+        // This shouldn't happen if we execute 1 command per time.
         if (waitForToggleCommandComplete && command.type == TOGGLE) {
             Log.d(TAG, "executeCommand: $command - waiting for toggle command complete")
             return true
@@ -141,15 +184,37 @@
         val recentsView = visibleRecentsView
         Log.d(TAG, "executeCommand: $command - visibleRecentsView: $recentsView")
         return if (recentsView != null) {
-            executeWhenRecentsIsVisible(command, recentsView)
+            executeWhenRecentsIsVisible(command, recentsView, onCallbackResult)
         } else {
-            executeWhenRecentsIsNotVisible(command)
+            executeWhenRecentsIsNotVisible(command, onCallbackResult)
         }
     }
 
+    /**
+     * Executes the task and returns true if next task can be executed. If false, then the next task
+     * is deferred until [.scheduleNextTask] is called
+     */
+    private suspend fun executeCommandSuspended(command: CommandInfo) =
+        suspendCancellableCoroutine { continuation ->
+            fun processResult(isCompleted: Boolean) {
+                Log.d(TAG, "command executed: $command with result: $isCompleted")
+                if (isCompleted) {
+                    continuation.resume(Unit)
+                } else {
+                    Log.d(TAG, "waiting for command callback: $command")
+                }
+            }
+
+            val result = executeCommand(command, onCallbackResult = { processResult(true) })
+            processResult(result)
+
+            continuation.invokeOnCancellation { cancelCommand(command, it) }
+        }
+
     private fun executeWhenRecentsIsVisible(
         command: CommandInfo,
         recentsView: RecentsView<*, *>,
+        onCallbackResult: () -> Unit,
     ): Boolean =
         when (command.type) {
             SHOW -> true // already visible
@@ -161,7 +226,7 @@
                     keyboardTaskFocusIndex = PagedView.INVALID_PAGE
                     val currentPage = recentsView.nextPage
                     val taskView = recentsView.getTaskViewAt(currentPage)
-                    launchTask(recentsView, taskView, command)
+                    launchTask(recentsView, taskView, command, onCallbackResult)
                 }
             }
             TOGGLE -> {
@@ -171,7 +236,7 @@
                     } else {
                         recentsView.nextTaskView ?: recentsView.runningTaskView
                     }
-                launchTask(recentsView, taskView, command)
+                launchTask(recentsView, taskView, command, onCallbackResult)
             }
             HOME -> {
                 recentsView.startHome()
@@ -182,19 +247,20 @@
     private fun launchTask(
         recents: RecentsView<*, *>,
         taskView: TaskView?,
-        command: CommandInfo
+        command: CommandInfo,
+        onCallbackResult: () -> Unit
     ): Boolean {
         var callbackList: RunnableList? = null
         if (taskView != null) {
             waitForToggleCommandComplete = true
             taskView.isEndQuickSwitchCuj = true
-            callbackList = taskView.launchTasks()
+            callbackList = taskView.launchWithAnimation()
         }
 
         if (callbackList != null) {
             callbackList.add {
                 Log.d(TAG, "launching task callback: $command")
-                onCommandFinished(command)
+                onCallbackResult()
                 waitForToggleCommandComplete = false
             }
             Log.d(TAG, "launching task - waiting for callback: $command")
@@ -206,7 +272,10 @@
         }
     }
 
-    private fun executeWhenRecentsIsNotVisible(command: CommandInfo): Boolean {
+    private fun executeWhenRecentsIsNotVisible(
+        command: CommandInfo,
+        onCallbackResult: () -> Unit
+    ): Boolean {
         val recentsViewContainer = activityInterface.getCreatedContainer() as? RecentsViewContainer
         val recentsView: RecentsView<*, *>? = recentsViewContainer?.getOverviewPanel()
         val deviceProfile = recentsViewContainer?.getDeviceProfile()
@@ -263,7 +332,7 @@
                     Log.d(TAG, "switching to Overview state - onAnimationEnd: $command")
                     super.onAnimationEnd(animation)
                     onRecentsViewFocusUpdated(command)
-                    onCommandFinished(command)
+                    onCallbackResult()
                 }
             }
         if (activityInterface.switchToRecentsIfVisible(animatorListener)) {
@@ -289,7 +358,7 @@
                 command.createTime
             )
         interactionHandler.setGestureEndCallback {
-            onTransitionComplete(command, interactionHandler)
+            onTransitionComplete(command, interactionHandler, onCallbackResult)
         }
         interactionHandler.initWhenReady("OverviewCommandHelper: command.type=${command.type}")
 
@@ -321,11 +390,6 @@
                 }
             }
 
-        // TODO(b/361768912): Dead code. Remove or update after this bug is fixed.
-        //        if (visibleRecentsView != null) {
-        //            visibleRecentsView.moveRunningTaskToFront();
-        //        }
-
         if (taskAnimationManager.isRecentsAnimationRunning) {
             command.setAnimationCallbacks(
                 taskAnimationManager.continueRecentsAnimation(gestureState)
@@ -351,29 +415,40 @@
         return false
     }
 
-    private fun onTransitionComplete(command: CommandInfo, handler: AbsSwipeUpHandler<*, *, *>) {
+    private fun onTransitionComplete(
+        command: CommandInfo,
+        handler: AbsSwipeUpHandler<*, *, *>,
+        onCommandResult: () -> Unit
+    ) {
         Log.d(TAG, "switching via recents animation - onTransitionComplete: $command")
         command.removeListener(handler)
         Trace.endAsyncSection(TRANSITION_NAME, 0)
         onRecentsViewFocusUpdated(command)
-        onCommandFinished(command)
+        onCommandResult()
     }
 
     /** Called when the command finishes execution. */
     private fun onCommandFinished(command: CommandInfo) {
         command.status = CommandStatus.COMPLETED
-        if (commandQueue.first() !== command) {
+        if (commandQueue.firstOrNull() !== command) {
             Log.d(
                 TAG,
                 "next task not scheduled. First pending command type " +
-                    "is ${commandQueue.first()} - command type is: $command"
+                    "is ${commandQueue.firstOrNull()} - command type is: $command"
             )
             return
         }
 
         Log.d(TAG, "command executed successfully! $command")
         commandQueue.remove(command)
-        executeNext()
+        processNextCommand()
+    }
+
+    private fun cancelCommand(command: CommandInfo, throwable: Throwable?) {
+        command.status = CommandStatus.CANCELED
+        Log.e(TAG, "command cancelled: $command - $throwable")
+        commandQueue.remove(command)
+        processNextCommand()
     }
 
     private fun updateRecentsViewFocus(command: CommandInfo) {
@@ -447,7 +522,8 @@
         pw.println("  waitForToggleCommandComplete=$waitForToggleCommandComplete")
     }
 
-    private data class CommandInfo(
+    @VisibleForTesting
+    data class CommandInfo(
         val type: CommandType,
         var status: CommandStatus = CommandStatus.IDLE,
         val createTime: Long = SystemClock.elapsedRealtime(),
@@ -468,7 +544,8 @@
         enum class CommandStatus {
             IDLE,
             PROCESSING,
-            COMPLETED
+            COMPLETED,
+            CANCELED
         }
     }
 
@@ -489,5 +566,6 @@
          * should be enough. We'll toss in one more because we're kind hearted.
          */
         private const val MAX_QUEUE_SIZE = 3
+        private const val QUEUE_WAIT_DURATION_IN_MS = 5000L
     }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 9e6e2f3..785666f 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -431,7 +431,7 @@
 
         @Override
         public void onClick(View view) {
-            if (mTaskView.launchTaskAnimated() != null) {
+            if (mTaskView.launchAsStaticTile() != null) {
                 SystemUiProxy.INSTANCE.get(mTarget.asContext()).startScreenPinning(
                         mTaskView.getFirstTask().key.id);
             }
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index 32d9052..cc022b2 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -535,6 +535,18 @@
             int parentWidth, int parentHeight, SplitBounds splitBoundsConfig,
             DeviceProfile dp, boolean isRtl) {
         int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
+
+        FrameLayout.LayoutParams primaryParams =
+                (FrameLayout.LayoutParams) primarySnapshot.getLayoutParams();
+        FrameLayout.LayoutParams secondaryParams =
+                (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams();
+
+        // Reset margin and translations that aren't used in this method, but are used in other
+        // `RecentsPagedOrientationHandler` variants.
+        secondaryParams.topMargin = 0;
+        primaryParams.topMargin = spaceAboveSnapshot;
+        primarySnapshot.setTranslationY(0);
+
         int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
         float dividerScale = splitBoundsConfig.appsStackedVertically
                 ? splitBoundsConfig.dividerHeightPercent
@@ -552,24 +564,14 @@
                 secondarySnapshot.setTranslationX(translationX);
                 primarySnapshot.setTranslationX(0);
             }
-            secondarySnapshot.setTranslationY(spaceAboveSnapshot);
 
-            // Reset unused translations
-            primarySnapshot.setTranslationY(0);
+            secondarySnapshot.setTranslationY(spaceAboveSnapshot);
         } else {
             float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale);
             float translationY = taskViewSizes.first.y + spaceAboveSnapshot + finalDividerHeight;
             secondarySnapshot.setTranslationY(translationY);
 
-            FrameLayout.LayoutParams primaryParams =
-                    (FrameLayout.LayoutParams) primarySnapshot.getLayoutParams();
-            FrameLayout.LayoutParams secondaryParams =
-                    (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams();
-            secondaryParams.topMargin = 0;
-            primaryParams.topMargin = spaceAboveSnapshot;
-
-            // Reset unused translations
-            primarySnapshot.setTranslationY(0);
+            // Reset unused translations.
             secondarySnapshot.setTranslationX(0);
             primarySnapshot.setTranslationX(0);
         }
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 41add54..6db0923 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -262,7 +262,7 @@
         }
         Log.d(
             TAG,
-            "launchTaskAnimated - launchTaskWithDesktopController: ${taskIds.contentToString()}, withRemoteTransition: $animated"
+            "launchTaskWithDesktopController: ${taskIds.contentToString()}, withRemoteTransition: $animated"
         )
 
         // Callbacks get run from recentsView for case when recents animation already running
@@ -270,11 +270,12 @@
         return endCallback
     }
 
-    override fun launchTaskAnimated() = launchTaskWithDesktopController(animated = true)
+    override fun launchAsStaticTile() = launchTaskWithDesktopController(animated = true)
 
-    override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) {
-        launchTaskWithDesktopController(animated = false)?.add { callback(true) } ?: callback(false)
-    }
+    override fun launchWithoutAnimation(
+        isQuickSwitch: Boolean,
+        callback: (launched: Boolean) -> Unit
+    ) = launchTaskWithDesktopController(animated = false)?.add { callback(true) } ?: callback(false)
 
     // Desktop tile can't be in split screen
     override fun confirmSecondSplitSelectApp(): Boolean = false
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
index f0fdd81..7b97c23 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
@@ -39,6 +39,7 @@
 import androidx.annotation.VisibleForTesting
 import androidx.core.util.component1
 import androidx.core.util.component2
+import androidx.core.view.isVisible
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
 import com.android.launcher3.util.Executors
@@ -108,18 +109,18 @@
     }
 
     private fun setNoLimit() {
+        isVisible = false
         hasLimit = false
-        setContentDescription(appUsageLimitTimeMs = -1, appRemainingTimeMs = -1)
-        visibility = INVISIBLE
         appRemainingTimeMs = -1
+        setContentDescription(appUsageLimitTimeMs = -1, appRemainingTimeMs = -1)
     }
 
     private fun setLimit(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) {
-        this.appRemainingTimeMs = appRemainingTimeMs
+        isVisible = true
         hasLimit = true
-        text = Utilities.prefixTextWithIcon(context, R.drawable.ic_hourglass_top, getBannerText())
-        visibility = VISIBLE
+        this.appRemainingTimeMs = appRemainingTimeMs
         setContentDescription(appUsageLimitTimeMs, appRemainingTimeMs)
+        text = Utilities.prefixTextWithIcon(context, R.drawable.ic_hourglass_top, getBannerText())
     }
 
     private fun setContentDescription(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) {
@@ -172,7 +173,7 @@
 
     /** Mark the DWB toast as destroyed and hide it. */
     fun destroy() {
-        visibility = INVISIBLE
+        isVisible = false
         isDestroyed = true
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index 4fae01e..3fd1a6b 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -218,11 +218,7 @@
         invalidate()
     }
 
-    override fun launchTaskAnimated(): RunnableList? {
-        if (taskContainers.isEmpty()) {
-            Log.d(TAG, "launchTaskAnimated - task is not bound")
-            return null
-        }
+    override fun launchAsStaticTile(): RunnableList? {
         val recentsView = recentsView ?: return null
         val endCallback = RunnableList()
         // Callbacks run from remote animation when recents animation not currently running
@@ -241,8 +237,11 @@
         return endCallback
     }
 
-    override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) {
-        launchTaskInternal(isQuickSwitch, false, callback /*launchingExistingTaskview*/)
+    override fun launchWithoutAnimation(
+        isQuickSwitch: Boolean,
+        callback: (launched: Boolean) -> Unit
+    ) {
+        launchTaskInternal(isQuickSwitch, launchingExistingTaskView = false, callback)
     }
 
     /**
@@ -266,7 +265,10 @@
                 isQuickSwitch,
                 snapPosition
             )
-            Log.d(TAG, "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}")
+            Log.d(
+                TAG,
+                "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}, launchingExistingTaskView: $launchingExistingTaskView"
+            )
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 226ecf5..709e0aa 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -635,13 +635,16 @@
         @Override
         public void onTaskRemoved(int taskId) {
             if (!mHandleTaskStackChanges) {
+                Log.d(TAG, "onTaskRemoved: " + taskId + ", not handling task stack changes");
                 return;
             }
 
             TaskView taskView = getTaskViewByTaskId(taskId);
             if (taskView == null) {
+                Log.d(TAG, "onTaskRemoved: " + taskId + ", no associated TaskView");
                 return;
             }
+            Log.d(TAG, "onTaskRemoved: " + taskId);
             Task.TaskKey taskKey = taskView.getFirstTask().key;
             UI_HELPER_EXECUTOR.execute(new CancellableTask<>(
                     () -> PackageManagerWrapper.getInstance()
@@ -2138,6 +2141,7 @@
         boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow()
                 && getWindowVisibility() == VISIBLE;
         if (handleTaskStackChanges != mHandleTaskStackChanges) {
+            Log.d(TAG, "updateTaskStackListenerState: " + handleTaskStackChanges);
             mHandleTaskStackChanges = handleTaskStackChanges;
             if (handleTaskStackChanges) {
                 reloadIfNeeded();
@@ -2731,9 +2735,12 @@
         if (!mModel.isTaskListValid(mTaskListChangeId)) {
             mTaskListChangeId = mModel.getTasks(this::applyLoadPlan, RecentsFilterState
                     .getFilter(mFilterState.getPackageNameToFilter()));
+            Log.d(TAG, "reloadIfNeeded - getTasks: " + mTaskListChangeId);
             if (enableRefactorTaskThumbnail()) {
                 mRecentsViewModel.refreshAllTaskData();
             }
+        } else {
+            Log.d(TAG, "reloadIfNeeded - task list still valid: " + mTaskListChangeId);
         }
     }
 
@@ -4382,8 +4389,10 @@
     private void dismissTask(int taskId) {
         TaskView taskView = getTaskViewByTaskId(taskId);
         if (taskView == null) {
+            Log.d(TAG, "dismissTask: " + taskId + ",  no associated TaskView");
             return;
         }
+        Log.d(TAG, "dismissTask: " + taskId);
         dismissTask(taskView, true /* animate */, false /* removeTask */);
     }
 
@@ -5485,7 +5494,7 @@
                     finishRecentsAnimation(false /* toRecents */, null);
                     onTaskLaunchAnimationEnd(true /* success */);
                 } else {
-                    taskView.launchTask(this::onTaskLaunchAnimationEnd);
+                    taskView.launchWithoutAnimation(this::onTaskLaunchAnimationEnd);
                 }
                 mContainer.getStatsLogManager().logger().withItemInfo(taskView.getFirstItemInfo())
                         .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index d34a93b..13c4f78 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -177,6 +177,7 @@
         addAccessibleChildToList(iconView.asView(), outChildren)
         addAccessibleChildToList(snapshotView, outChildren)
         showWindowsView?.let { addAccessibleChildToList(it, outChildren) }
+        digitalWellBeingToast?.let { addAccessibleChildToList(it, outChildren) }
     }
 
     private fun addAccessibleChildToList(view: View, outChildren: ArrayList<View>) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 5614af6..601dae8 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -1003,7 +1003,7 @@
             return
         }
         val callbackList =
-            launchTasks()?.apply {
+            launchWithAnimation()?.apply {
                 add {
                     Log.d("b/310064698", "${taskIds.contentToString()} - onClick - launchCompleted")
                 }
@@ -1015,12 +1015,106 @@
             .log(LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP)
     }
 
+    /** Launch of the current task (both live and inactive tasks) with an animation. */
+    fun launchWithAnimation(): RunnableList? {
+        return if (isRunningTask && recentsView?.remoteTargetHandles != null) {
+            launchAsLiveTile()
+        } else {
+            launchAsStaticTile()
+        }
+    }
+
+    private fun launchAsLiveTile(): RunnableList? {
+        val recentsView = recentsView ?: return null
+        val remoteTargetHandles = recentsView.remoteTargetHandles
+        if (!isClickableAsLiveTile) {
+            Log.e(
+                TAG,
+                "launchAsLiveTile - TaskView is not clickable as a live tile; returning to home: ${taskIds.contentToString()}"
+            )
+            return null
+        }
+        isClickableAsLiveTile = false
+        val targets =
+            if (remoteTargetHandles.size == 1) {
+                remoteTargetHandles[0].transformParams.targetSet
+            } else {
+                val apps =
+                    remoteTargetHandles.flatMap { it.transformParams.targetSet.apps.asIterable() }
+                val wallpapers =
+                    remoteTargetHandles.flatMap {
+                        it.transformParams.targetSet.wallpapers.asIterable()
+                    }
+                RemoteAnimationTargets(
+                    apps.toTypedArray(),
+                    wallpapers.toTypedArray(),
+                    remoteTargetHandles[0].transformParams.targetSet.nonApps,
+                    remoteTargetHandles[0].transformParams.targetSet.targetMode
+                )
+            }
+        if (targets == null) {
+            // If the recents animation is cancelled somehow between the parent if block and
+            // here, try to launch the task as a non live tile task.
+            val runnableList = launchAsStaticTile()
+            if (runnableList == null) {
+                Log.e(
+                    TAG,
+                    "launchAsLiveTile - Recents animation cancelled and cannot launch task as non-live tile; returning to home: ${taskIds.contentToString()}"
+                )
+            }
+            isClickableAsLiveTile = true
+            return runnableList
+        }
+        TestLogging.recordEvent(
+            TestProtocol.SEQUENCE_MAIN,
+            "composeRecentsLaunchAnimator",
+            taskIds.contentToString()
+        )
+        val runnableList = RunnableList()
+        with(AnimatorSet()) {
+            TaskViewUtils.composeRecentsLaunchAnimator(
+                this,
+                this@TaskView,
+                targets.apps,
+                targets.wallpapers,
+                targets.nonApps,
+                true /* launcherClosing */,
+                recentsView.stateManager,
+                recentsView,
+                recentsView.depthController
+            )
+            addListener(
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animator: Animator) {
+                        if (taskContainers.any { it.task.key.displayId != rootViewDisplayId }) {
+                            launchAsStaticTile()
+                        }
+                        isClickableAsLiveTile = true
+                        runEndCallback()
+                    }
+
+                    override fun onAnimationCancel(animation: Animator) {
+                        runEndCallback()
+                    }
+
+                    private fun runEndCallback() {
+                        runnableList.executeAllAndDestroy()
+                    }
+                }
+            )
+            start()
+        }
+        Log.d(TAG, "launchAsLiveTile - composeRecentsLaunchAnimator: ${taskIds.contentToString()}")
+        recentsView.onTaskLaunchedInLiveTileMode()
+        return runnableList
+    }
+
     /**
      * Starts the task associated with this view and animates the startup.
      *
      * @return CompletionStage to indicate the animation completion or null if the launch failed.
      */
-    open fun launchTaskAnimated(): RunnableList? {
+    open fun launchAsStaticTile(): RunnableList? {
         TestLogging.recordEvent(
             TestProtocol.SEQUENCE_MAIN,
             "startActivityFromRecentsAsync",
@@ -1036,7 +1130,7 @@
         ) {
             Log.d(
                 TAG,
-                "launchTaskAnimated - startActivityFromRecents: ${taskIds.contentToString()}"
+                "launchAsStaticTile - startActivityFromRecents: ${taskIds.contentToString()}"
             )
             ActiveGestureLog.INSTANCE.trackEvent(
                 ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED
@@ -1060,18 +1154,17 @@
             recentsView.addSideTaskLaunchCallback(opts.onEndCallback)
             return opts.onEndCallback
         } else {
-            notifyTaskLaunchFailed()
+            notifyTaskLaunchFailed("launchAsStaticTile")
             return null
         }
     }
 
     /** Starts the task associated with this view without any animation */
-    fun launchTask(callback: (launched: Boolean) -> Unit) {
-        launchTask(callback, isQuickSwitch = false)
-    }
-
-    /** Starts the task associated with this view without any animation */
-    open fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) {
+    @JvmOverloads
+    open fun launchWithoutAnimation(
+        isQuickSwitch: Boolean = false,
+        callback: (launched: Boolean) -> Unit
+    ) {
         TestLogging.recordEvent(
             TestProtocol.SEQUENCE_MAIN,
             "startActivityFromRecentsAsync",
@@ -1084,7 +1177,7 @@
             // gesture launcher is in the background state, vs other launches which are in
             // the actual overview state
             failureListener.register(container, firstContainer.task.key.id) {
-                notifyTaskLaunchFailed()
+                notifyTaskLaunchFailed("launchWithoutAnimation")
                 recentsView?.let {
                     // Disable animations for now, as it is an edge case and the app usually
                     // covers launcher and also any state transition animation also gets
@@ -1128,103 +1221,20 @@
                 // otherwise, wait for the animation start callback from the activity options
                 // above
                 Executors.MAIN_EXECUTOR.post {
-                    notifyTaskLaunchFailed()
+                    notifyTaskLaunchFailed("launchTask")
                     callback(false)
                 }
             }
-            Log.d(TAG, "launchTask - startActivityFromRecents: ${taskIds.contentToString()}")
+            Log.d(
+                TAG,
+                "launchWithoutAnimation - startActivityFromRecents: ${taskIds.contentToString()}"
+            )
         }
     }
 
-    /** Launch of the current task (both live and inactive tasks) with an animation. */
-    fun launchTasks(): RunnableList? {
-        val recentsView = recentsView ?: return null
-        val remoteTargetHandles = recentsView.mRemoteTargetHandles
-        if (!isRunningTask || remoteTargetHandles == null) {
-            return launchTaskAnimated()
-        }
-        if (!isClickableAsLiveTile) {
-            Log.e(TAG, "TaskView is not clickable as a live tile; returning to home.")
-            return null
-        }
-        isClickableAsLiveTile = false
-        val targets =
-            if (remoteTargetHandles.size == 1) {
-                remoteTargetHandles[0].transformParams.targetSet
-            } else {
-                val apps =
-                    remoteTargetHandles.flatMap { it.transformParams.targetSet.apps.asIterable() }
-                val wallpapers =
-                    remoteTargetHandles.flatMap {
-                        it.transformParams.targetSet.wallpapers.asIterable()
-                    }
-                RemoteAnimationTargets(
-                    apps.toTypedArray(),
-                    wallpapers.toTypedArray(),
-                    remoteTargetHandles[0].transformParams.targetSet.nonApps,
-                    remoteTargetHandles[0].transformParams.targetSet.targetMode
-                )
-            }
-        if (targets == null) {
-            // If the recents animation is cancelled somehow between the parent if block and
-            // here, try to launch the task as a non live tile task.
-            val runnableList = launchTaskAnimated()
-            if (runnableList == null) {
-                Log.e(
-                    TAG,
-                    "Recents animation cancelled and cannot launch task as non-live tile" +
-                        "; returning to home"
-                )
-            }
-            isClickableAsLiveTile = true
-            return runnableList
-        }
-        TestLogging.recordEvent(
-            TestProtocol.SEQUENCE_MAIN,
-            "composeRecentsLaunchAnimator",
-            taskIds.contentToString()
-        )
-        val runnableList = RunnableList()
-        with(AnimatorSet()) {
-            TaskViewUtils.composeRecentsLaunchAnimator(
-                this,
-                this@TaskView,
-                targets.apps,
-                targets.wallpapers,
-                targets.nonApps,
-                true /* launcherClosing */,
-                recentsView.stateManager,
-                recentsView,
-                recentsView.depthController
-            )
-            addListener(
-                object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animator: Animator) {
-                        if (taskContainers.any { it.task.key.displayId != rootViewDisplayId }) {
-                            launchTaskAnimated()
-                        }
-                        isClickableAsLiveTile = true
-                        runEndCallback()
-                    }
-
-                    override fun onAnimationCancel(animation: Animator) {
-                        runEndCallback()
-                    }
-
-                    private fun runEndCallback() {
-                        runnableList.executeAllAndDestroy()
-                    }
-                }
-            )
-            start()
-        }
-        Log.d(TAG, "launchTasks - composeRecentsLaunchAnimator: ${taskIds.contentToString()}")
-        recentsView.onTaskLaunchedInLiveTileMode()
-        return runnableList
-    }
-
-    private fun notifyTaskLaunchFailed() {
-        val sb = StringBuilder("Failed to launch task \n")
+    private fun notifyTaskLaunchFailed(launchMethod: String) {
+        val sb =
+            StringBuilder("$launchMethod - Failed to launch task: ${taskIds.contentToString()}\n")
         taskContainers.forEach {
             sb.append("(task=${it.task.key.baseIntent} userId=${it.task.key.userId})\n")
         }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
new file mode 100644
index 0000000..0ae710f
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep
+
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.TestDispatcherProvider
+import com.android.launcher3.util.rule.setFlags
+import com.android.quickstep.OverviewCommandHelper.CommandInfo
+import com.android.quickstep.OverviewCommandHelper.CommandInfo.CommandStatus
+import com.android.quickstep.OverviewCommandHelper.CommandType
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class OverviewCommandHelperTest {
+    @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
+    private lateinit var sut: OverviewCommandHelper
+    private val dispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+
+    private var pendingCallbacksWithDelays = mutableListOf<Long>()
+
+    @Suppress("UNCHECKED_CAST")
+    @Before
+    fun setup() {
+        setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT)
+
+        sut =
+            spy(
+                OverviewCommandHelper(
+                    touchInteractionService = mock(),
+                    overviewComponentObserver = mock(),
+                    taskAnimationManager = mock(),
+                    dispatcherProvider = TestDispatcherProvider(dispatcher)
+                )
+            )
+
+        doAnswer { invocation ->
+                val pendingCallback = invocation.arguments[1] as () -> Unit
+
+                val delayInMillis = pendingCallbacksWithDelays.removeFirstOrNull()
+                if (delayInMillis != null) {
+                    runBlocking {
+                        testScope.backgroundScope.launch {
+                            delay(delayInMillis)
+                            pendingCallback.invoke()
+                        }
+                    }
+                }
+                delayInMillis == null // if no callback to execute, returns success
+            }
+            .`when`(sut)
+            .executeCommand(any<CommandInfo>(), any())
+    }
+
+    private fun addCallbackDelay(delayInMillis: Long = 0) {
+        pendingCallbacksWithDelays.add(delayInMillis)
+    }
+
+    @Test
+    fun whenFirstCommandIsAdded_executeCommandImmediately() =
+        testScope.runTest {
+            // Add command to queue
+            val commandInfo: CommandInfo = sut.addCommand(CommandType.HOME)!!
+            assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE)
+            runCurrent()
+            assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED)
+        }
+
+    @Test
+    fun whenFirstCommandIsAdded_executeCommandImmediately_WithCallbackDelay() =
+        testScope.runTest {
+            addCallbackDelay(100)
+
+            // Add command to queue
+            val commandType = CommandType.HOME
+            val commandInfo: CommandInfo = sut.addCommand(commandType)!!
+            assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE)
+
+            runCurrent()
+            assertThat(commandInfo.status).isEqualTo(CommandStatus.PROCESSING)
+
+            advanceTimeBy(200L)
+            assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED)
+        }
+
+    @Test
+    fun whenFirstCommandIsPendingCallback_NextCommandWillWait() =
+        testScope.runTest {
+            // Add command to queue
+            addCallbackDelay(100)
+            val commandType1 = CommandType.HOME
+            val commandInfo1: CommandInfo = sut.addCommand(commandType1)!!
+            assertThat(commandInfo1.status).isEqualTo(CommandStatus.IDLE)
+
+            addCallbackDelay(100)
+            val commandType2 = CommandType.SHOW
+            val commandInfo2: CommandInfo = sut.addCommand(commandType2)!!
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
+
+            runCurrent()
+            assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING)
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
+
+            advanceTimeBy(101L)
+            assertThat(commandInfo1.status).isEqualTo(CommandStatus.COMPLETED)
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING)
+
+            advanceTimeBy(101L)
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.COMPLETED)
+        }
+
+    @Test
+    fun whenCommandTakesTooLong_TriggerTimeout_AndExecuteNextCommand() =
+        testScope.runTest {
+            // Add command to queue
+            addCallbackDelay(QUEUE_TIMEOUT)
+            val commandType1 = CommandType.HOME
+            val commandInfo1: CommandInfo = sut.addCommand(commandType1)!!
+            assertThat(commandInfo1.status).isEqualTo(CommandStatus.IDLE)
+
+            addCallbackDelay(100)
+            val commandType2 = CommandType.SHOW
+            val commandInfo2: CommandInfo = sut.addCommand(commandType2)!!
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
+
+            runCurrent()
+            assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING)
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE)
+
+            advanceTimeBy(QUEUE_TIMEOUT)
+            assertThat(commandInfo1.status).isEqualTo(CommandStatus.CANCELED)
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING)
+
+            advanceTimeBy(101)
+            assertThat(commandInfo2.status).isEqualTo(CommandStatus.COMPLETED)
+        }
+
+    private companion object {
+        const val QUEUE_TIMEOUT = 5001L
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
index 80b9489..c18f604 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
@@ -25,6 +25,7 @@
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.quickstep.DeviceConfigWrapper.DEFAULT_LPNH_TIMEOUT_MS;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -39,7 +40,6 @@
 
 import android.os.SystemClock;
 import android.view.MotionEvent;
-import android.view.ViewConfiguration;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -142,7 +142,7 @@
     @Test
     public void testLongPressTriggered() {
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
-        SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
@@ -156,7 +156,7 @@
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
         mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
                 -(TOUCH_SLOP - 1)));
-        SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
@@ -170,7 +170,7 @@
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
         mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
                 mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
-        SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
@@ -189,7 +189,7 @@
             mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
                     mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
             // We have entered the second stage, so the normal timeout shouldn't trigger.
-            SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+            SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
             InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
             assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
@@ -200,7 +200,7 @@
             // After an extended time, the long press should trigger.
             float extendedDurationMultiplier =
                     (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
-            SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout()
+            SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
                     * (extendedDurationMultiplier - 1)));  // -1 because we already waited 1x
             InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
@@ -221,7 +221,7 @@
 
             mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
             // We have not entered the second stage, so the normal timeout should trigger.
-            SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+            SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
             InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
             assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
@@ -236,7 +236,7 @@
     @Test
     public void testLongPressAbortedByTouchUp() {
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
-        SystemClock.sleep(ViewConfiguration.getLongPressTimeout() - 10);
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
@@ -256,7 +256,7 @@
     @Test
     public void testLongPressAbortedByTouchCancel() {
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
-        SystemClock.sleep(ViewConfiguration.getLongPressTimeout() - 10);
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
@@ -276,7 +276,7 @@
     @Test
     public void testLongPressAbortedByTouchSlopPassedVertically() {
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
-        SystemClock.sleep(ViewConfiguration.getLongPressTimeout() - 10);
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
@@ -297,7 +297,7 @@
     @Test
     public void testLongPressAbortedByTouchSlopPassedHorizontally() {
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
-        SystemClock.sleep(ViewConfiguration.getLongPressTimeout() - 10);
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
@@ -326,7 +326,7 @@
             mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
                     -(TOUCH_SLOP - 1)));
             // Normal duration shouldn't trigger.
-            SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+            SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
             InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
             assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
@@ -338,7 +338,7 @@
             // Wait past the extended long press timeout, to be sure it wouldn't have triggered.
             float extendedDurationMultiplier =
                     (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
-            SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout()
+            SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
                     * (extendedDurationMultiplier - 1)));  // -1 because we already waited 1x
             InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
@@ -363,7 +363,7 @@
             mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
                     mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
             // Normal duration shouldn't trigger.
-            SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+            SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
             InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
             assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
@@ -375,7 +375,7 @@
             // Wait past the extended long press timeout, to be sure it wouldn't have triggered.
             float extendedDurationMultiplier =
                     (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
-            SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout()
+            SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
                     * (extendedDurationMultiplier - 1)));  // -1 because we already waited 1x
             InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
@@ -393,7 +393,7 @@
     public void testTouchOutsideNavHandleIgnored() {
         // Touch the far left side of the screen. (y=0 is top of navbar region, picked arbitrarily)
         mUnderTest.onMotionEvent(generateMotionEvent(ACTION_DOWN, 0, 0));
-        SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         // Should be ignored because the x position was not centered in the navbar region.
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
index d2479bc..7c48ea4 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
@@ -34,9 +34,6 @@
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_DISABLED
 import com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY
-import com.google.android.apps.nexuslauncher.PrefKey.KEY_ENABLE_MINUS_ONE
-import com.google.android.apps.nexuslauncher.PrefKey.OVERVIEW_SUGGESTED_ACTIONS
-import com.google.android.apps.nexuslauncher.PrefKey.SMARTSPACE_ON_HOME_SCREEN
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
@@ -141,7 +138,14 @@
             .isTrue()
         assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED.id })
             .isTrue()
-        // LAUNCHER_GOOGLE_APP_SWIPE_LEFT_ENABLED
-        assertThat(capturedEvents.any { it.id == 617 }).isTrue()
+        assertThat(capturedEvents.any { it.id == LAUNCHER_GOOGLE_APP_SWIPE_LEFT_ENABLED }).isTrue()
+    }
+
+    companion object {
+        private const val KEY_ENABLE_MINUS_ONE = "pref_enable_minus_one"
+        private const val OVERVIEW_SUGGESTED_ACTIONS = "pref_overview_action_suggestions"
+        private const val SMARTSPACE_ON_HOME_SCREEN = "pref_smartspace_home_screen"
+
+        private const val LAUNCHER_GOOGLE_APP_SWIPE_LEFT_ENABLED = 617
     }
 }
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index dc8694d..374c07b 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -32,6 +32,7 @@
 import android.os.IBinder.DeathRecipient;
 import android.os.Message;
 import android.os.Messenger;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Pair;
@@ -80,8 +81,10 @@
     private static final String KEY_SURFACE_PACKAGE = "surface_package";
     private static final String KEY_CALLBACK = "callback";
     public static final String KEY_HIDE_BOTTOM_ROW = "hide_bottom_row";
+    public static final String KEY_GRID_NAME = "grid_name";
 
     private static final int MESSAGE_ID_UPDATE_PREVIEW = 1337;
+    private static final int MESSAGE_ID_UPDATE_GRID = 7414;
 
     /**
      * Here we use the IBinder and the screen ID as the key of the active previews.
@@ -245,11 +248,22 @@
             if (destroyed) {
                 return true;
             }
-            if (message.what == MESSAGE_ID_UPDATE_PREVIEW) {
-                renderer.hideBottomRow(message.getData().getBoolean(KEY_HIDE_BOTTOM_ROW));
-            } else {
-                destroyObserver(this);
+
+            switch (message.what) {
+                case MESSAGE_ID_UPDATE_PREVIEW:
+                    renderer.hideBottomRow(message.getData().getBoolean(KEY_HIDE_BOTTOM_ROW));
+                    break;
+                case MESSAGE_ID_UPDATE_GRID:
+                    String gridName = message.getData().getString(KEY_GRID_NAME);
+                    if (!TextUtils.isEmpty(gridName)) {
+                        renderer.updateGrid(gridName);
+                    }
+                    break;
+                default:
+                    destroyObserver(this);
+                    break;
             }
+
             return true;
         }
 
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index addd072..56c4ca4 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -38,6 +38,7 @@
 import android.view.SurfaceControlViewHost.SurfacePackage;
 import android.view.View;
 import android.view.animation.AccelerateDecelerateInterpolator;
+import android.widget.FrameLayout;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -61,6 +62,7 @@
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.widget.LocalColorExtractor;
+import com.android.systemui.shared.Flags;
 
 import java.util.ArrayList;
 import java.util.Map;
@@ -96,6 +98,7 @@
     private boolean mDestroyed = false;
     private LauncherPreviewRenderer mRenderer;
     private boolean mHideQsb;
+    @Nullable private FrameLayout mViewRoot = null;
 
     public PreviewSurfaceRenderer(Context context, Bundle bundle) throws Exception {
         mContext = context;
@@ -194,6 +197,19 @@
     }
 
     /**
+     * Update the grid of the launcher preview
+     *
+     * @param gridName Name of the grid, e.g. normal, practical
+     */
+    public void updateGrid(@NonNull String gridName) {
+        if (gridName.equals(mGridName)) {
+            return;
+        }
+        mGridName = gridName;
+        loadAsync();
+    }
+
+    /**
      * Hides the components in the bottom row.
      *
      * @param hide True to hide and false to show.
@@ -302,11 +318,41 @@
         view.setPivotY(0);
         view.setTranslationX((mWidth - scale * view.getWidth()) / 2);
         view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
-        view.setAlpha(0);
-        view.animate().alpha(1)
-                .setInterpolator(new AccelerateDecelerateInterpolator())
-                .setDuration(FADE_IN_ANIMATION_DURATION)
-                .start();
-        mSurfaceControlViewHost.setView(view, view.getMeasuredWidth(), view.getMeasuredHeight());
+        if (!Flags.newCustomizationPickerUi()) {
+            view.setAlpha(0);
+            view.animate().alpha(1)
+                    .setInterpolator(new AccelerateDecelerateInterpolator())
+                    .setDuration(FADE_IN_ANIMATION_DURATION)
+                    .start();
+            mSurfaceControlViewHost.setView(
+                    view,
+                    view.getMeasuredWidth(),
+                    view.getMeasuredHeight()
+            );
+            return;
+        }
+
+        if (mViewRoot == null) {
+            mViewRoot = new FrameLayout(inflationContext);
+            FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
+                    FrameLayout.LayoutParams.WRAP_CONTENT, // Width
+                    FrameLayout.LayoutParams.WRAP_CONTENT  // Height
+            );
+            mViewRoot.setLayoutParams(layoutParams);
+            mViewRoot.addView(view);
+            mViewRoot.setAlpha(0);
+            mViewRoot.animate().alpha(1)
+                    .setInterpolator(new AccelerateDecelerateInterpolator())
+                    .setDuration(FADE_IN_ANIMATION_DURATION)
+                    .start();
+            mSurfaceControlViewHost.setView(
+                    mViewRoot,
+                    view.getMeasuredWidth(),
+                    view.getMeasuredHeight()
+            );
+        } else  {
+            mViewRoot.removeAllViews();
+            mViewRoot.addView(view);
+        }
     }
 }