Merge "Revert^2 "Restore Archived Pinned Shortcuts instead of removing them."" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 9fa2f50..7465a4c 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -559,6 +559,13 @@
 }
 
 flag {
+  name: "enable_launcher_visual_refresh"
+  namespace: "launcher"
+  description: "Adds refresh for font family, app longpress menu icons, and pagination dots"
+  bug: "395145453"
+}
+
+flag {
   name: "restore_archived_shortcuts"
   namespace: "launcher"
   description: "Makes sure pre-archived pinned shortcuts also get restored"
@@ -601,3 +608,10 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "enable_strict_mode"
+  namespace: "launcher"
+  description: "Enable Strict Mode for the Launcher app"
+  bug: "394651876"
+}
diff --git a/quickstep/res/drawable/bg_overview_add_desktop_button.xml b/quickstep/res/drawable/bg_overview_add_desktop_button.xml
new file mode 100644
index 0000000..12581bf
--- /dev/null
+++ b/quickstep/res/drawable/bg_overview_add_desktop_button.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2025 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.
+-->
+<ripple android:color="?android:attr/colorControlHighlight"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape
+            android:shape="rectangle"
+            android:tint="?colorButtonNormal">
+            <corners android:radius="@dimen/add_desktop_button_size" />
+            <solid android:color="@color/materialColorSurfaceBright"/>
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_overview_clear_all_button.xml b/quickstep/res/drawable/bg_overview_clear_all_button.xml
index 7f58cf8..2f28689 100644
--- a/quickstep/res/drawable/bg_overview_clear_all_button.xml
+++ b/quickstep/res/drawable/bg_overview_clear_all_button.xml
@@ -15,8 +15,7 @@
      limitations under the License.
 -->
 <ripple android:color="?android:attr/colorControlHighlight"
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    xmlns:android="http://schemas.android.com/apk/res/android">
     <item>
         <shape android:shape="rectangle"
             android:tint="?colorButtonNormal">
diff --git a/quickstep/res/layout/overview_add_desktop_button.xml b/quickstep/res/layout/overview_add_desktop_button.xml
index e36cf72..a1c64f3 100644
--- a/quickstep/res/layout/overview_add_desktop_button.xml
+++ b/quickstep/res/layout/overview_add_desktop_button.xml
@@ -16,9 +16,11 @@
 -->
 <com.android.quickstep.views.AddDesktopButton
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:launcher="http://schemas.android.com/apgk/res-auto"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto"
     android:id="@+id/add_desktop_button"
     android:layout_width="@dimen/add_desktop_button_size"
     android:layout_height="@dimen/add_desktop_button_size"
     android:src="@drawable/ic_desktop_add"
-    android:padding="10dp" />
\ No newline at end of file
+    android:background="@drawable/bg_overview_add_desktop_button"
+    launcher:focusBorderColor="@color/materialColorOutline"
+    android:padding="10dp" />
diff --git a/quickstep/res/layout/overview_clear_all_button.xml b/quickstep/res/layout/overview_clear_all_button.xml
index 18a6240..034c3c2 100644
--- a/quickstep/res/layout/overview_clear_all_button.xml
+++ b/quickstep/res/layout/overview_clear_all_button.xml
@@ -16,7 +16,6 @@
 -->
 <com.android.quickstep.views.ClearAllButton
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
     style="@style/OverviewClearAllButton"
     android:id="@+id/clear_all"
@@ -25,4 +24,4 @@
     android:text="@string/recents_clear_all"
     android:textColor="@color/materialColorOnSurface"
     launcher:focusBorderColor="@color/materialColorOutline"
-    android:textSize="14sp" />
\ No newline at end of file
+    android:textSize="14sp" />
diff --git a/quickstep/res/values/attrs.xml b/quickstep/res/values/attrs.xml
index 7fd6b5c..28c0d5c 100644
--- a/quickstep/res/values/attrs.xml
+++ b/quickstep/res/values/attrs.xml
@@ -36,6 +36,11 @@
         <attr name="focusBorderColor" />
     </declare-styleable>
 
+    <declare-styleable name="AddDesktopButton">
+        <!-- focus border color for overview add desktop button views -->
+        <attr name="focusBorderColor" />
+    </declare-styleable>
+
     <!--
          Gesture nav edu specific attributes. These attributes are used to customize Gesture nav edu
          view lottie animation colors in XML files.
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index b253343..f2f1ebd 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -108,6 +108,7 @@
 
     <!-- Recents add desktop button -->
     <dimen name="add_desktop_button_size">56dp</dimen>
+    <dimen name="add_desktop_button_outline_padding">2dp</dimen>
 
     <!-- The speed in dp/s at which the user needs to be scrolling in recents such that we start
              loading full resolution screenshots. -->
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 23065b5..5afc5ed 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.taskbar;
 
+import static com.android.launcher3.Flags.enableAltTabKqsOnConnectedDisplays;
+
 import android.content.ComponentName;
 import android.content.pm.ActivityInfo;
 import android.view.MotionEvent;
@@ -272,11 +274,26 @@
     }
 
     private void processLoadedTasksOnDesktop(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
-        // Find the single desktop task that contains a grouping of desktop tasks
-        DesktopTask desktopTask = findDesktopTask(tasks);
+        // Find all desktop tasks.
+        List<DesktopTask> desktopTasks = tasks.stream()
+                .filter(t -> t instanceof DesktopTask)
+                .map(t -> (DesktopTask) t)
+                .toList();
 
-        if (desktopTask != null) {
-            mTasks = desktopTask.getTasks().stream()
+        // Apps on the connected displays seem to be in different Desktop tasks even with the
+        // multiple desktops flag disabled. So, until multiple desktops is implemented the following
+        // should help with team-fooding Alt+tab on connected displays. Post multiple desktop,
+        // further changes maybe required to support launching selected desktops.
+        if (enableAltTabKqsOnConnectedDisplays()) {
+            mTasks = desktopTasks.stream()
+                    .flatMap(t -> t.getTasks().stream())
+                    .map(SingleTask::new)
+                    .filter(task -> !shouldExcludeTask(task, taskIdsToExclude))
+                    .collect(Collectors.toList());
+
+            mNumHiddenTasks = Math.max(0, tasks.size() - desktopTasks.size());
+        } else if (!desktopTasks.isEmpty()) {
+            mTasks = desktopTasks.get(0).getTasks().stream()
                     .map(SingleTask::new)
                     .filter(task -> !shouldExcludeTask(task, taskIdsToExclude))
                     .collect(Collectors.toList());
@@ -289,14 +306,6 @@
         }
     }
 
-    @Nullable
-    private DesktopTask findDesktopTask(List<GroupTask> tasks) {
-        return (DesktopTask) tasks.stream()
-                .filter(t -> t instanceof DesktopTask)
-                .findFirst()
-                .orElse(null);
-    }
-
     void closeQuickSwitchView() {
         closeQuickSwitchView(true);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 9d1fc15..a6d3cde 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -24,6 +24,8 @@
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
 import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED;
 
+import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
+
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
 import static com.android.launcher3.AbstractFloatingView.TYPE_ON_BOARD_POPUP;
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
@@ -2008,7 +2010,8 @@
         return mControllers.uiController.isIconAlignedWithHotseat();
     }
 
-    @VisibleForTesting
+    // TODO(b/395061396): Remove `otherwise` when overview in widow is enabled.
+    @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
     public TaskbarControllers getControllers() {
         return mControllers;
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index ea2dec1..028e9e7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -32,6 +32,7 @@
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
 import static com.android.quickstep.util.SystemActionConstants.ACTION_SHOW_TASKBAR;
 import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
+import static com.android.window.flags.Flags.enableTaskbarConnectedDisplays;
 
 import android.annotation.SuppressLint;
 import android.app.PendingIntent;
@@ -71,6 +72,7 @@
 import com.android.quickstep.AllAppsActionManager;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.fallback.window.RecentsDisplayModel;
 import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.ContextualSearchInvoker;
 import com.android.quickstep.views.RecentsViewContainer;
@@ -182,6 +184,7 @@
             new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::showTaskbarFromBroadcast);
 
     private final AllAppsActionManager mAllAppsActionManager;
+    private final RecentsDisplayModel mRecentsDisplayModel;
 
     private final Runnable mActivityOnDestroyCallback = new Runnable() {
         @Override
@@ -242,10 +245,12 @@
     public TaskbarManager(
             Context context,
             AllAppsActionManager allAppsActionManager,
-            TaskbarNavButtonCallbacks navCallbacks) {
+            TaskbarNavButtonCallbacks navCallbacks,
+            RecentsDisplayModel recentsDisplayModel) {
         mParentContext = context;
         createWindowContext(context.getDisplayId());
         mAllAppsActionManager = allAppsActionManager;
+        mRecentsDisplayModel = recentsDisplayModel;
         if (enableTaskbarNoRecreate()) {
             createTaskbarRootLayout(getDefaultDisplayId());
         }
@@ -487,6 +492,18 @@
         return null;
     }
 
+    /** Creates a {@link TaskbarUIController} to use with non default displays. */
+    private TaskbarUIController createTaskbarUIControllerForNonDefaultDisplay(int displayId) {
+        if (RecentsDisplayModel.enableOverviewInWindow()) {
+            RecentsViewContainer rvc = mRecentsDisplayModel.getRecentsWindowManager(displayId);
+            if (rvc != null) {
+                return createTaskbarUIControllerForRecentsViewContainer(rvc);
+            }
+        }
+
+        return new TaskbarUIController();
+    }
+
     /**
      * Creates a {@link TaskbarUIController} to use while the given StatefulActivity is active.
      */
@@ -561,7 +578,11 @@
             mSharedState.allAppsVisible = mSharedState.allAppsVisible && isLargeScreenTaskbar;
             taskbar.init(mSharedState);
 
-            if (mRecentsViewContainer != null) {
+            // Non default displays should not use LauncherTaskbarUIController as they shouldn't
+            // have access to the Launcher activity.
+            if (enableTaskbarConnectedDisplays() && !isDefaultDisplay(displayId)) {
+                taskbar.setUIController(createTaskbarUIControllerForNonDefaultDisplay(displayId));
+            } else if (mRecentsViewContainer != null) {
                 taskbar.setUIController(
                         createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
             }
@@ -829,6 +850,23 @@
     }
 
     /**
+     * Returns the {@link TaskbarUIController} associated with the given display ID.
+     * TODO(b/395061396): Remove this method when overview in widow is enabled.
+     *
+     * @param displayId The ID of the display to retrieve the taskbar for.
+     * @return The {@link TaskbarUIController} for the specified display, or
+     *         {@code null} if no taskbar is associated with that display.
+     */
+    @Nullable
+    public TaskbarUIController getUIControllerForDisplay(int displayId) {
+        if (!mTaskbars.contains(displayId)) {
+            return null;
+        }
+
+        return getTaskbarForDisplay(displayId).getControllers().uiController;
+    }
+
+    /**
      * Retrieves whether RootLayout was added to window for specific display, or false if no
      * such mapping has been made.
      *
diff --git a/quickstep/src/com/android/quickstep/FocusState.kt b/quickstep/src/com/android/quickstep/FocusState.kt
index ba3991f..7c6aa5b 100644
--- a/quickstep/src/com/android/quickstep/FocusState.kt
+++ b/quickstep/src/com/android/quickstep/FocusState.kt
@@ -27,7 +27,10 @@
 class FocusState {
 
     var focusedDisplayId = DEFAULT_DISPLAY
-        private set
+        private set(value) {
+            field = value
+            listeners.forEach { it.onFocusedDisplayChanged(value) }
+        }
 
     private var listeners = mutableSetOf<FocusChangeListener>()
 
@@ -40,9 +43,7 @@
             transitions?.setFocusTransitionListener(
                 object : Stub() {
                     override fun onFocusedDisplayChanged(displayId: Int) {
-                        Executors.MAIN_EXECUTOR.execute {
-                            listeners.forEach { it.onFocusedDisplayChanged(displayId) }
-                        }
+                        Executors.MAIN_EXECUTOR.execute { focusedDisplayId = displayId }
                     }
                 }
             )
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 94d115b..42aa86e 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -28,6 +28,7 @@
 import androidx.annotation.UiThread
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.Cuj
+import com.android.launcher3.Flags.enableAltTabKqsOnConnectedDisplays
 import com.android.launcher3.Flags.enableFallbackOverviewInWindow
 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
 import com.android.launcher3.Flags.enableLauncherOverviewInWindow
@@ -38,6 +39,8 @@
 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.taskbar.TaskbarManager
+import com.android.launcher3.taskbar.TaskbarUIController
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.RunnableList
 import com.android.launcher3.util.coroutines.DispatcherProvider
@@ -48,6 +51,7 @@
 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.fallback.window.RecentsDisplayModel
 import com.android.quickstep.util.ActiveGestureLog
 import com.android.quickstep.util.ActiveGestureProtoLogProxy
 import com.android.quickstep.views.RecentsView
@@ -72,6 +76,9 @@
     private val overviewComponentObserver: OverviewComponentObserver,
     private val taskAnimationManager: TaskAnimationManager,
     private val dispatcherProvider: DispatcherProvider = ProductionDispatchers,
+    private val recentsDisplayModel: RecentsDisplayModel,
+    private val focusState: FocusState,
+    private val taskbarManager: TaskbarManager,
 ) {
     private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.background)
 
@@ -291,15 +298,51 @@
                 deviceProfile != null &&
                 (deviceProfile.isTablet || deviceProfile.isTwoPanels)
 
+        val focusedDisplayId = focusState.focusedDisplayId
+        val focusedDisplayUIController: TaskbarUIController? =
+            if (RecentsDisplayModel.enableOverviewInWindow()) {
+                Log.d(
+                    TAG,
+                    "Querying RecentsDisplayModel for TaskbarUIController for display: $focusedDisplayId",
+                )
+                recentsDisplayModel.getRecentsWindowManager(focusedDisplayId)?.taskbarUIController
+            } else {
+                Log.d(
+                    TAG,
+                    "Querying TaskbarManager for TaskbarUIController for display: $focusedDisplayId",
+                )
+                // TODO(b/395061396): Remove this path when overview in widow is enabled.
+                taskbarManager.getUIControllerForDisplay(focusedDisplayId)
+            }
+        Log.d(
+            TAG,
+            "TaskbarUIController for display $focusedDisplayId was" +
+                "${if (focusedDisplayUIController == null) " not" else ""} found",
+        )
+
         when (command.type) {
             HIDE -> {
                 if (!allowQuickSwitch) return true
-                keyboardTaskFocusIndex = uiController!!.launchFocusedTask()
+                keyboardTaskFocusIndex =
+                    if (
+                        enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
+                    ) {
+                        focusedDisplayUIController.launchFocusedTask()
+                    } else {
+                        uiController!!.launchFocusedTask()
+                    }
+
                 if (keyboardTaskFocusIndex == -1) return true
             }
             KEYBOARD_INPUT ->
                 if (allowQuickSwitch) {
-                    uiController!!.openQuickSwitchView()
+                    if (
+                        enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
+                    ) {
+                        focusedDisplayUIController.openQuickSwitchView()
+                    } else {
+                        uiController!!.openQuickSwitchView()
+                    }
                     return true
                 } else {
                     keyboardTaskFocusIndex = 0
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.kt b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
index d5382ad..a1ac39e 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.kt
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
@@ -149,7 +149,7 @@
     private var backToLauncherRunner: IRemoteAnimationRunner? = null
     private var dragAndDrop: IDragAndDrop? = null
     val homeVisibilityState = HomeVisibilityState()
-    private val focusState = FocusState()
+    val focusState = FocusState()
 
     // Used to dedupe calls to SystemUI
     private var lastShelfHeight = 0
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 2df4a45..ba4c65a 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -88,6 +88,7 @@
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.OverviewCommandHelper.CommandType;
 import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener;
+import com.android.quickstep.fallback.window.RecentsDisplayModel;
 import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler;
 import com.android.quickstep.inputconsumers.BubbleBarInputConsumer;
 import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer;
@@ -584,7 +585,8 @@
             initInputMonitor("onTrackpadConnected()");
         });
 
-        mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager, mNavCallbacks);
+        mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager, mNavCallbacks,
+                RecentsDisplayModel.getINSTANCE().get(this));
         mDesktopAppLaunchTransitionManager =
                 new DesktopAppLaunchTransitionManager(this, SystemUiProxy.INSTANCE.get(this));
         mDesktopAppLaunchTransitionManager.registerTransitions();
@@ -641,7 +643,9 @@
         mTaskAnimationManager = new TaskAnimationManager(this, mDeviceState);
         mOverviewComponentObserver = OverviewComponentObserver.INSTANCE.get(this);
         mOverviewCommandHelper = new OverviewCommandHelper(this,
-                mOverviewComponentObserver, mTaskAnimationManager);
+                mOverviewComponentObserver, mTaskAnimationManager,
+                RecentsDisplayModel.getINSTANCE().get(this),
+                SystemUiProxy.INSTANCE.get(this).getFocusState(), mTaskbarManager);
         mResetGestureInputConsumer = new ResetGestureInputConsumer(
                 mTaskAnimationManager, mTaskbarManager::getCurrentActivityContext);
         mInputConsumer.registerInputConsumer();
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
index 31a1be8..95a3ec2 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
@@ -50,10 +50,14 @@
             DaggerSingletonObject<RecentsDisplayModel>(
                 QuickstepBaseAppComponent::getRecentsDisplayModel
             )
+
+        @JvmStatic
+        fun enableOverviewInWindow() =
+            Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()
     }
 
     init {
-        if (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()) {
+        if (enableOverviewInWindow()) {
             displayManager.registerDisplayListener(displayListener, Executors.MAIN_EXECUTOR.handler)
             // In the scenario where displays were added before this display listener was
             // registered, we should store the RecentsDisplayResources for those displays
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index 002a4e8..b1a5920 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -66,14 +66,10 @@
                 )
                 tasks.value = MapForStateFlow(recentTasks)
 
-                // Request data for completed tasks to prevent stale data.
-                // This will prevent thumbnail and icon from being replaced and
-                // null due to race condition.
-                taskRequests.values.forEach { (taskKey, job) ->
-                    if (job.isCompleted) {
-                        requestTaskData(taskKey.id)
-                    }
-                }
+                // Request data for tasks to prevent stale data.
+                // This will prevent thumbnail and icon from being replaced and null due to
+                // race condition. The new request will hit the cache and return immediately.
+                taskRequests.keys.forEach(::requestTaskData)
             }
         }
         return tasks.map { it.values.toList() }
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 1f428f3..d2f10b6 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -28,13 +28,11 @@
 import com.android.quickstep.recents.data.TasksRepository
 import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
 import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
 import com.android.quickstep.recents.domain.usecase.OrganizeDesktopTasksUseCase
 import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.usecase.GetThumbnailUseCase
 import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
-import com.android.quickstep.task.thumbnail.TaskThumbnailViewData
-import com.android.quickstep.task.viewmodel.TaskContainerData
 import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
@@ -175,14 +173,8 @@
         val instance: Any =
             when (modelClass) {
                 RecentsViewData::class.java -> RecentsViewData()
-                TaskContainerData::class.java -> TaskContainerData()
-                TaskThumbnailViewData::class.java -> TaskThumbnailViewData()
                 TaskThumbnailViewModel::class.java ->
-                    TaskThumbnailViewModelImpl(
-                        dispatcherProvider = inject(),
-                        getThumbnailPositionUseCase = inject(),
-                        splashAlphaUseCase = inject(scopeId),
-                    )
+                    TaskThumbnailViewModelImpl(getThumbnailPositionUseCase = inject())
                 TaskOverlayViewModel::class.java -> {
                     val task = extras["Task"] as Task
                     TaskOverlayViewModel(
@@ -193,6 +185,8 @@
                         dispatcherProvider = inject(),
                     )
                 }
+                IsThumbnailValidUseCase::class.java ->
+                    IsThumbnailValidUseCase(rotationStateRepository = inject())
                 GetTaskUseCase::class.java -> GetTaskUseCase(repository = inject())
                 GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
                 GetSysUiStatusNavFlagsUseCase::class.java -> GetSysUiStatusNavFlagsUseCase()
@@ -203,14 +197,6 @@
                         tasksRepository = inject(),
                     )
                 OrganizeDesktopTasksUseCase::class.java -> OrganizeDesktopTasksUseCase()
-                SplashAlphaUseCase::class.java ->
-                    SplashAlphaUseCase(
-                        recentsViewData = inject(),
-                        taskContainerData = inject(scopeId),
-                        taskThumbnailViewData = inject(scopeId),
-                        tasksRepository = inject(),
-                        rotationStateRepository = inject(),
-                    )
                 else -> {
                     log("Factory for ${modelClass.simpleName} not defined!", Log.ERROR)
                     error("Factory for ${modelClass.simpleName} not defined!")
diff --git a/quickstep/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCase.kt
new file mode 100644
index 0000000..02f8329
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCase.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2025 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.recents.domain.usecase
+
+import android.graphics.Bitmap
+import android.view.Surface
+import com.android.quickstep.recents.data.RecentsRotationStateRepository
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import com.android.systemui.shared.recents.utilities.Utilities
+
+/**
+ * Use case responsible for validating the aspect ratio and rotation of a thumbnail against the
+ * expected values based on the view's dimensions and the current rotation state.
+ *
+ * This class checks if the thumbnail's aspect ratio significantly differs from the aspect ratio of
+ * the view it is intended to be displayed in, and if the thumbnail's rotation is consistent with
+ * the device's current rotation state.
+ *
+ * @property rotationStateRepository Repository providing the current rotation state of the device.
+ */
+class IsThumbnailValidUseCase(private val rotationStateRepository: RecentsRotationStateRepository) {
+    operator fun invoke(thumbnailData: ThumbnailData?, viewWidth: Int, viewHeight: Int): Boolean {
+        val thumbnail = thumbnailData?.thumbnail ?: return false
+        return !isInaccurateThumbnail(thumbnail, viewWidth, viewHeight, thumbnailData.rotation)
+    }
+
+    private fun isInaccurateThumbnail(
+        thumbnail: Bitmap,
+        viewWidth: Int,
+        viewHeight: Int,
+        rotation: Int,
+    ): Boolean =
+        isAspectRatioDifferentFromViewAspectRatio(
+            thumbnail = thumbnail,
+            width = viewWidth.toFloat(),
+            height = viewHeight.toFloat(),
+        ) || isRotationDifferentFromTask(rotation)
+
+    private fun isAspectRatioDifferentFromViewAspectRatio(
+        thumbnail: Bitmap,
+        width: Float,
+        height: Float,
+    ): Boolean {
+        return Utilities.isRelativePercentDifferenceGreaterThan(
+            /* first = */ width / height,
+            /* second = */ thumbnail.width / thumbnail.height.toFloat(),
+            /* bound = */ PreviewPositionHelper.MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT,
+        )
+    }
+
+    private fun isRotationDifferentFromTask(thumbnailRotation: Int): Boolean {
+        val rotationState = rotationStateRepository.getRecentsRotationState()
+        return if (rotationState.orientationHandlerRotation == Surface.ROTATION_0) {
+            (rotationState.activityRotation - thumbnailRotation) % 2 != 0
+        } else {
+            rotationState.orientationHandlerRotation != thumbnailRotation
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
index 961446f..0b299ee 100644
--- a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
@@ -24,8 +24,10 @@
 import com.android.quickstep.recents.domain.model.TaskModel
 import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
 import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.views.TaskViewType
+import com.android.systemui.shared.recents.model.ThumbnailData
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -45,6 +47,7 @@
     recentsViewData: RecentsViewData,
     private val getTaskUseCase: GetTaskUseCase,
     private val getSysUiStatusNavFlagsUseCase: GetSysUiStatusNavFlagsUseCase,
+    private val isThumbnailValidUseCase: IsThumbnailValidUseCase,
     dispatcherProvider: DispatcherProvider,
 ) {
     private var taskIds = MutableStateFlow(emptySet<Int>())
@@ -74,10 +77,12 @@
             .flowOn(dispatcherProvider.background)
 
     fun bind(vararg taskId: TaskId) {
-        Log.d(TAG, "bind: $taskId")
-        taskIds.value = taskId.toSet()
+        taskIds.value = taskId.toSet().also { Log.d(TAG, "bind: $it") }
     }
 
+    fun isThumbnailValid(thumbnail: ThumbnailData?, width: Int, height: Int): Boolean =
+        isThumbnailValidUseCase(thumbnail, width, height)
+
     private fun mapToTaskTile(tasks: List<TaskData>, isLiveTile: Boolean): TaskTileUiState {
         val firstThumbnailData = (tasks.firstOrNull() as? TaskData.Data)?.thumbnailData
         return TaskTileUiState(
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
index a1f8454..2465a46 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
@@ -27,8 +27,6 @@
     // The settled set of visible taskIds that is updated after RecentsView scroll settles.
     val settledFullyVisibleTaskIds = MutableStateFlow(emptySet<Int>())
 
-    val thumbnailSplashProgress = MutableStateFlow(0f)
-
     // A list of taskIds that are associated with a RecentsAnimationController. */
     val runningTaskIds = MutableStateFlow(emptySet<Int>())
 
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
index 73332fc..5ff8aaa 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -42,10 +42,6 @@
         recentsViewData.overlayEnabled.value = isOverlayEnabled
     }
 
-    fun updateThumbnailSplashProgress(taskThumbnailSplashAlpha: Float) {
-        recentsViewData.thumbnailSplashProgress.value = taskThumbnailSplashAlpha
-    }
-
     suspend fun waitForThumbnailsToUpdate(updatedThumbnails: Map<Int, ThumbnailData>?) {
         if (updatedThumbnails.isNullOrEmpty()) return
         combine(
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
deleted file mode 100644
index 723df55..0000000
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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.recents.viewmodel
-
-import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.runBlocking
-
-class TaskContainerViewModel(private val splashAlphaUseCase: SplashAlphaUseCase) {
-    fun shouldShowThumbnailSplash(taskId: Int): Boolean =
-        (runBlocking { splashAlphaUseCase.execute(taskId).firstOrNull() } ?: 0f) > 0f
-}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt b/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt
deleted file mode 100644
index 7673c71..0000000
--- a/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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.task.thumbnail
-
-import android.graphics.Bitmap
-import android.view.Surface
-import com.android.quickstep.recents.data.RecentTasksRepository
-import com.android.quickstep.recents.data.RecentsRotationStateRepository
-import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.viewmodel.TaskContainerData
-import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
-import com.android.systemui.shared.recents.utilities.Utilities
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-
-class SplashAlphaUseCase(
-    private val recentsViewData: RecentsViewData,
-    private val taskContainerData: TaskContainerData,
-    private val taskThumbnailViewData: TaskThumbnailViewData,
-    private val tasksRepository: RecentTasksRepository,
-    private val rotationStateRepository: RecentsRotationStateRepository,
-) {
-    fun execute(taskId: Int): Flow<Float> =
-        combine(
-                taskThumbnailViewData.width,
-                taskThumbnailViewData.height,
-                tasksRepository.getThumbnailById(taskId),
-                taskContainerData.thumbnailSplashProgress,
-                recentsViewData.thumbnailSplashProgress
-            ) { width, height, thumbnailData, taskSplashProgress, globalSplashProgress ->
-                val thumbnail = thumbnailData?.thumbnail
-                when {
-                    thumbnail == null -> 0f
-                    taskSplashProgress > 0f -> taskSplashProgress
-                    globalSplashProgress > 0f &&
-                        isInaccurateThumbnail(thumbnail, thumbnailData.rotation, width, height) ->
-                        globalSplashProgress
-                    else -> 0f
-                }
-            }
-            .distinctUntilChanged()
-
-    private fun isInaccurateThumbnail(
-        thumbnail: Bitmap,
-        thumbnailRotation: Int,
-        width: Int,
-        height: Int
-    ): Boolean {
-        return isThumbnailAspectRatioDifferentFromThumbnailData(thumbnail, width, height) ||
-            isThumbnailRotationDifferentFromTask(thumbnailRotation)
-    }
-
-    private fun isThumbnailAspectRatioDifferentFromThumbnailData(
-        thumbnail: Bitmap,
-        viewWidth: Int,
-        viewHeight: Int
-    ): Boolean {
-        val viewAspect: Float = viewWidth / viewHeight.toFloat()
-        val thumbnailAspect: Float = thumbnail.width / thumbnail.height.toFloat()
-        return Utilities.isRelativePercentDifferenceGreaterThan(
-            viewAspect,
-            thumbnailAspect,
-            PreviewPositionHelper.MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT
-        )
-    }
-
-    private fun isThumbnailRotationDifferentFromTask(thumbnailRotation: Int): Boolean {
-        val rotationState = rotationStateRepository.getRecentsRotationState()
-        return if (rotationState.orientationHandlerRotation == Surface.ROTATION_0) {
-            (rotationState.activityRotation - thumbnailRotation) % 2 != 0
-        } else {
-            rotationState.orientationHandlerRotation != thumbnailRotation
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 28152ec..63e93ba 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -50,10 +50,6 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.dropWhile
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
 
 class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
@@ -62,8 +58,6 @@
 
     // This is initialised here and set in onAttachedToWindow because onLayout can be called before
     // onAttachedToWindow so this property needs to be initialised as it is used below.
-    private var viewData: TaskThumbnailViewData = RecentsDependencies.get(this)
-
     private lateinit var viewModel: TaskThumbnailViewModel
 
     private lateinit var viewAttachedScope: CoroutineScope
@@ -110,18 +104,7 @@
             CoroutineScope(
                 SupervisorJob() + Dispatchers.Main.immediate + CoroutineName("TaskThumbnailView")
             )
-        viewData = RecentsDependencies.get(this)
-        updateViewDataValues()
         viewModel = RecentsDependencies.get(this)
-        viewModel.splashAlpha
-            .dropWhile { it == 0f }
-            .flowOn(dispatcherProvider.background)
-            .onEach { splashAlpha ->
-                splashBackground.alpha = splashAlpha
-                splashIcon.alpha = splashAlpha
-            }
-            .launchIn(viewAttachedScope)
-
         clipToOutline = true
         outlineProvider =
             object : ViewOutlineProvider() {
@@ -144,16 +127,9 @@
         resetViews()
     }
 
-    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
-        super.onLayout(changed, left, top, right, bottom)
-        if (changed) {
-            updateViewDataValues()
-        }
-    }
-
     fun setState(state: TaskThumbnailUiState, taskId: Int? = null) {
-        logDebug("taskId: $taskId - uiState changed from: $uiState to: $state")
         if (uiState == state) return
+        logDebug("taskId: $taskId - uiState changed from: $uiState to: $state")
         uiState = state
         resetViews()
         when (state) {
@@ -164,6 +140,12 @@
         }
     }
 
+    /**
+     * Updates the alpha of the dim layer on top of this view. If dimAlpha is 0, no dimming is
+     * applied; if dimAlpha is 1, the thumbnail will be the extracted background color.
+     *
+     * @param tintAmount The amount of alpha that will be applied to the dim layer.
+     */
     fun updateTintAmount(tintAmount: Float) {
         dimAlpha[ScrimViewAlpha.TintAmount.ordinal].value = tintAmount
     }
@@ -172,9 +154,9 @@
         dimAlpha[ScrimViewAlpha.MenuProgress.ordinal].value = progress * MAX_SCRIM_ALPHA
     }
 
-    private fun updateViewDataValues() {
-        viewData.width.value = width
-        viewData.height.value = height
+    fun updateSplashAlpha(value: Float) {
+        splashBackground.alpha = value
+        splashIcon.alpha = value
     }
 
     override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
deleted file mode 100644
index 3502029..0000000
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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.task.thumbnail
-
-import kotlinx.coroutines.flow.MutableStateFlow
-
-class TaskThumbnailViewData {
-    val width = MutableStateFlow(0)
-    val height = MutableStateFlow(0)
-}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
deleted file mode 100644
index 279ce39..0000000
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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.task.viewmodel
-
-import kotlinx.coroutines.flow.MutableStateFlow
-
-class TaskContainerData {
-    val thumbnailSplashProgress = MutableStateFlow(0f)
-}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
index c89bf01..e641737 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
@@ -17,13 +17,9 @@
 package com.android.quickstep.task.viewmodel
 
 import android.graphics.Matrix
-import kotlinx.coroutines.flow.Flow
 
 /** ViewModel for representing TaskThumbnails */
 interface TaskThumbnailViewModel {
-    /** Provides the alpha of the splash icon */
-    val splashAlpha: Flow<Float>
-
     /** Attaches this ViewModel to a specific task id for it to provide data from. */
     fun bind(taskId: Int)
 
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
index 635d08b..94c40d1 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
@@ -19,32 +19,17 @@
 import android.app.ActivityTaskManager.INVALID_TASK_ID
 import android.graphics.Matrix
 import android.util.Log
-import com.android.launcher3.util.coroutines.DispatcherProvider
 import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.usecase.ThumbnailPositionState
-import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.flowOn
 
-@OptIn(ExperimentalCoroutinesApi::class)
 class TaskThumbnailViewModelImpl(
-    dispatcherProvider: DispatcherProvider,
-    private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
-    private val splashAlphaUseCase: SplashAlphaUseCase,
+    private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase
 ) : TaskThumbnailViewModel {
-    private val splashProgress = MutableStateFlow(flowOf(0f))
     private var taskId: Int = INVALID_TASK_ID
 
-    override val splashAlpha =
-        splashProgress.flatMapLatest { it }.flowOn(dispatcherProvider.background)
-
     override fun bind(taskId: Int) {
         Log.d(TAG, "bind taskId: $taskId")
         this.taskId = taskId
-        splashProgress.value = splashAlphaUseCase.execute(taskId)
     }
 
     override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix =
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 0182969..99255e8 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -52,9 +52,9 @@
 import com.android.launcher3.QuickstepTransitionManager
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
+import com.android.launcher3.anim.AnimatedFloat
 import com.android.launcher3.anim.PendingAnimation
 import com.android.launcher3.apppairs.AppPairIcon
-import com.android.launcher3.config.FeatureFlags
 import com.android.launcher3.logging.StatsLogManager.EventEnum
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.statehandlers.DepthController
@@ -94,7 +94,7 @@
             val fadeWithThumbnail: Boolean,
             val isStagedTask: Boolean,
             val iconView: View?,
-            val contentDescription: CharSequence?
+            val contentDescription: CharSequence?,
         )
     }
 
@@ -104,7 +104,7 @@
      */
     fun getFirstAnimInitViews(
         taskViewSupplier: Supplier<TaskView>,
-        splitSelectSourceSupplier: Supplier<SplitSelectSource?>
+        splitSelectSourceSupplier: Supplier<SplitSelectSource?>,
     ): SplitAnimInitProps {
         val splitSelectSource = splitSelectSourceSupplier.get()
         if (!splitSelectStateController.isAnimateCurrentTaskDismissal) {
@@ -116,7 +116,7 @@
                 fadeWithThumbnail = false,
                 isStagedTask = true,
                 iconView = null,
-                splitSelectSource.itemInfo.contentDescription
+                splitSelectSource.itemInfo.contentDescription,
             )
         } else if (splitSelectStateController.isDismissingFromSplitPair) {
             // Initiating split from overview, but on a split pair
@@ -131,7 +131,7 @@
                         fadeWithThumbnail = true,
                         isStagedTask = true,
                         iconView = container.iconView.asView(),
-                        container.task.titleDescription
+                        container.task.titleDescription,
                     )
                 }
             }
@@ -151,7 +151,7 @@
                     fadeWithThumbnail = true,
                     isStagedTask = true,
                     iconView = it.iconView.asView(),
-                    it.task.titleDescription
+                    it.task.titleDescription,
                 )
             }
         }
@@ -189,29 +189,25 @@
         deviceProfile: DeviceProfile,
         taskViewWidth: Int,
         taskViewHeight: Int,
-        isPrimaryTaskSplitting: Boolean
+        isPrimaryTaskSplitting: Boolean,
     ) {
         val snapshot = taskContainer.snapshotView
         val iconView: View = taskContainer.iconView.asView()
-        if (!enableRefactorTaskThumbnail()) {
+        if (enableRefactorTaskThumbnail()) {
+            builder.add(
+                AnimatedFloat { v -> taskContainer.taskView.splitSplashAlpha = v }
+                    .animateToValue(1f)
+            )
+        } else {
             val thumbnailViewDeprecated = taskContainer.thumbnailViewDeprecated
             builder.add(
                 ObjectAnimator.ofFloat(
                     thumbnailViewDeprecated,
                     TaskThumbnailViewDeprecated.SPLASH_ALPHA,
-                    1f
+                    1f,
                 )
             )
             thumbnailViewDeprecated.setShowSplashForSplitSelection(true)
-        } else {
-            builder.add(
-                ValueAnimator.ofFloat(0f, 1f).apply {
-                    addUpdateListener {
-                        taskContainer.taskContainerData.thumbnailSplashProgress.value =
-                            it.animatedFraction
-                    }
-                }
-            )
         }
         // With the new `IconAppChipView`, we always want to keep the chip pinned to the
         // top left of the task / thumbnail.
@@ -220,7 +216,7 @@
                 ObjectAnimator.ofFloat(
                     (iconView as IconAppChipView).splitTranslationX,
                     MULTI_PROPERTY_VALUE,
-                    0f
+                    0f,
                 )
             )
             builder.add(
@@ -306,7 +302,7 @@
     fun addScrimBehindAnim(
         pendingAnimation: PendingAnimation,
         container: RecentsViewContainer,
-        context: Context
+        context: Context,
     ): View {
         val scrim = View(context)
         val recentsView = container.getOverviewPanel<RecentsView<*, *>>()
@@ -334,8 +330,8 @@
             Interpolators.clampToProgress(
                 timings.backingScrimFadeInterpolator,
                 timings.backingScrimFadeInStartOffset,
-                timings.backingScrimFadeInEndOffset
-            )
+                timings.backingScrimFadeInEndOffset,
+            ),
         )
 
         return scrim
@@ -358,7 +354,7 @@
     fun createPlaceholderDismissAnim(
         container: RecentsViewContainer,
         splitDismissEvent: EventEnum,
-        duration: Long?
+        duration: Long?,
     ): AnimatorSet {
         val animatorSet = AnimatorSet()
         duration?.let { animatorSet.duration = it }
@@ -375,7 +371,7 @@
             Rect(0, 0, floatingTask.width, floatingTask.height),
             false,
             null,
-            onScreenRectF
+            onScreenRectF,
         )
         // Get the part of the floatingTask that intersects with the DragLayer (i.e. the
         // on-screen portion)
@@ -383,7 +379,7 @@
             dragLayer.left.toFloat(),
             dragLayer.top.toFloat(),
             dragLayer.right.toFloat(),
-            dragLayer.bottom.toFloat()
+            dragLayer.bottom.toFloat(),
         )
         animatorSet.play(
             ObjectAnimator.ofFloat(
@@ -393,8 +389,8 @@
                     floatingTask,
                     onScreenRectF,
                     floatingTask.stagePosition,
-                    container.deviceProfile
-                )
+                    container.deviceProfile,
+                ),
             )
         )
         animatorSet.addListener(
@@ -403,7 +399,7 @@
                     splitSelectStateController.resetState()
                     safeRemoveViewFromDragLayer(
                         container,
-                        splitSelectStateController.splitInstructionsView
+                        splitSelectStateController.splitInstructionsView,
                     )
                 }
             }
@@ -429,8 +425,8 @@
             Interpolators.clampToProgress(
                 Interpolators.LINEAR,
                 timings.instructionsContainerFadeInStartOffset,
-                timings.instructionsContainerFadeInEndOffset
-            )
+                timings.instructionsContainerFadeInEndOffset,
+            ),
         )
         anim.addFloat(
             splitInstructionsView,
@@ -440,8 +436,8 @@
             Interpolators.clampToProgress(
                 Interpolators.EMPHASIZED_DECELERATE,
                 timings.instructionsUnfoldStartOffset,
-                timings.instructionsUnfoldEndOffset
-            )
+                timings.instructionsUnfoldEndOffset,
+            ),
         )
         return anim
     }
@@ -459,7 +455,7 @@
     fun playAnimPlaceholderToFullscreen(
         container: RecentsViewContainer,
         view: View,
-        resetCallback: Optional<Runnable>
+        resetCallback: Optional<Runnable>,
     ) {
         val stagedTaskView = view as FloatingTaskView
 
@@ -481,7 +477,7 @@
             RectF(firstTaskStartingBounds),
             firstTaskEndingBounds,
             false /* fadeWithThumbnail */,
-            true /* isStagedTask */
+            true, /* isStagedTask */
         )
 
         pendingAnimation.addEndListener {
@@ -511,7 +507,7 @@
         info: TransitionInfo?,
         t: Transaction?,
         finishCallback: Runnable,
-        cornerRadius: Float
+        cornerRadius: Float,
     ) {
         if (info == null && t == null) {
             // (Legacy animation) Tapping a split tile in Overview
@@ -530,7 +526,7 @@
                 nonApps,
                 stateManager,
                 depthController,
-                finishCallback
+                finishCallback,
             )
 
             return
@@ -548,7 +544,7 @@
                 depthController,
                 info,
                 t,
-                finishCallback
+                finishCallback,
             )
         } else if (launchingIconView != null) {
             // Tapping an app pair icon
@@ -563,7 +559,7 @@
                     info,
                     t,
                     finishCallback,
-                    cornerRadius
+                    cornerRadius,
                 )
             } else {
                 composeFullscreenIconSplitLaunchAnimator(
@@ -571,7 +567,7 @@
                     info,
                     t,
                     finishCallback,
-                    appPairLaunchingAppIndex
+                    appPairLaunchingAppIndex,
                 )
             }
         } else {
@@ -587,7 +583,7 @@
                 info,
                 t,
                 finishCallback,
-                cornerRadius
+                cornerRadius,
             )
         }
     }
@@ -603,7 +599,7 @@
         depthController: DepthController?,
         info: TransitionInfo,
         t: Transaction,
-        finishCallback: Runnable
+        finishCallback: Runnable,
     ) {
         TaskViewUtils.composeRecentsSplitLaunchAnimator(
             launchingTaskView,
@@ -611,7 +607,7 @@
             depthController,
             info,
             t,
-            finishCallback
+            finishCallback,
         )
     }
 
@@ -629,7 +625,7 @@
         nonApps: Array<RemoteAnimationTarget>,
         stateManager: StateManager<*, *>,
         depthController: DepthController?,
-        finishCallback: Runnable
+        finishCallback: Runnable,
     ) {
         TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(
             launchingTaskView,
@@ -640,7 +636,7 @@
             nonApps,
             stateManager,
             depthController,
-            finishCallback
+            finishCallback,
         )
     }
 
@@ -651,7 +647,7 @@
      */
     fun hasChangesForBothAppPairs(
         launchingIconView: AppPairIcon,
-        transitionInfo: TransitionInfo
+        transitionInfo: TransitionInfo,
     ): Int {
         val intent1 = launchingIconView.info.getFirstApp().intent.component?.packageName
         val intent2 = launchingIconView.info.getSecondApp().intent.component?.packageName
@@ -712,7 +708,7 @@
         transitionInfo: TransitionInfo,
         t: Transaction,
         finishCallback: Runnable,
-        windowRadius: Float
+        windowRadius: Float,
     ) {
         // If launching an app pair from Taskbar inside of an app context (no access to Launcher),
         // use the scale-up animation
@@ -721,7 +717,7 @@
                 transitionInfo,
                 t,
                 finishCallback,
-                WINDOWING_MODE_MULTI_WINDOW
+                WINDOWING_MODE_MULTI_WINDOW,
             )
             return
         }
@@ -753,8 +749,7 @@
                     (!isLeftRightSplit && change.endAbsBounds.top <= 0)
             }
         val dividerPos =
-            if (isLeftRightSplit) leftTopApp.endAbsBounds.right
-            else leftTopApp.endAbsBounds.bottom
+            if (isLeftRightSplit) leftTopApp.endAbsBounds.right else leftTopApp.endAbsBounds.bottom
 
         // Create a new floating view in Launcher, positioned above the launching icon
         val drawableArea = launchingIconView.iconDrawableArea
@@ -769,7 +764,7 @@
                 drawableArea,
                 appIcon1,
                 appIcon2,
-                dividerPos
+                dividerPos,
             )
         floatingView.bringToFront()
 
@@ -780,7 +775,7 @@
                 finishCallback,
                 launcher,
                 floatingView,
-                mainRootCandidate
+                mainRootCandidate,
             )
         iconLaunchValueAnimator.addListener(
             object : AnimatorListenerAdapter() {
@@ -806,7 +801,7 @@
         transitionInfo: TransitionInfo,
         t: Transaction,
         finishCallback: Runnable,
-        launchFullscreenIndex: Int
+        launchFullscreenIndex: Int,
     ) {
         // If launching an app pair from Taskbar inside of an app context (no access to Launcher),
         // use the scale-up animation
@@ -815,7 +810,7 @@
                 transitionInfo,
                 t,
                 finishCallback,
-                WINDOWING_MODE_FULLSCREEN
+                WINDOWING_MODE_FULLSCREEN,
             )
             return
         }
@@ -867,7 +862,7 @@
                 drawableArea,
                 appIcon,
                 null /*appIcon2*/,
-                0 /*dividerPos*/
+                0, /*dividerPos*/
             )
         floatingView.bringToFront()
         launchAnimation.play(
@@ -882,7 +877,7 @@
         finishCallback: Runnable,
         launcher: QuickstepLauncher,
         floatingView: FloatingAppPairView,
-        rootCandidate: Change
+        rootCandidate: Change,
     ): ValueAnimator {
         val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
         val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet)
@@ -896,7 +891,7 @@
                     Interpolators.LINEAR,
                     valueAnimator.animatedFraction,
                     timings.appRevealStartOffset,
-                    timings.appRevealEndOffset
+                    timings.appRevealEndOffset,
                 )
 
             // Set the alpha of the shell layer (2 apps + divider)
@@ -913,8 +908,8 @@
                         Interpolators.clampToProgress(
                             timings.getStagedRectXInterpolator(),
                             timings.stagedRectSlideStartOffset,
-                            timings.stagedRectSlideEndOffset
-                        )
+                            timings.stagedRectSlideEndOffset,
+                        ),
                     )
                 var mDy =
                     FloatProp(
@@ -923,8 +918,8 @@
                         Interpolators.clampToProgress(
                             Interpolators.EMPHASIZED,
                             timings.stagedRectSlideStartOffset,
-                            timings.stagedRectSlideEndOffset
-                        )
+                            timings.stagedRectSlideEndOffset,
+                        ),
                     )
                 var mScaleX =
                     FloatProp(
@@ -933,8 +928,8 @@
                         Interpolators.clampToProgress(
                             Interpolators.EMPHASIZED,
                             timings.stagedRectSlideStartOffset,
-                            timings.stagedRectSlideEndOffset
-                        )
+                            timings.stagedRectSlideEndOffset,
+                        ),
                     )
                 var mScaleY =
                     FloatProp(
@@ -943,8 +938,8 @@
                         Interpolators.clampToProgress(
                             Interpolators.EMPHASIZED,
                             timings.stagedRectSlideStartOffset,
-                            timings.stagedRectSlideEndOffset
-                        )
+                            timings.stagedRectSlideEndOffset,
+                        ),
                     )
 
                 override fun onUpdate(percent: Float, initOnly: Boolean) {
@@ -979,7 +974,7 @@
         transitionInfo: TransitionInfo,
         t: Transaction,
         finishCallback: Runnable,
-        windowingMode: Int
+        windowingMode: Int,
     ) {
         val launchAnimation = AnimatorSet()
         val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
@@ -1066,7 +1061,7 @@
         transitionInfo: TransitionInfo,
         t: Transaction,
         finishCallback: Runnable,
-        cornerRadius: Float
+        cornerRadius: Float,
     ) {
         var splitRoot1: Change? = null
         var splitRoot2: Change? = null
@@ -1131,7 +1126,7 @@
                     Interpolators.LINEAR,
                     valueAnimator.animatedFraction,
                     0.8f,
-                    1f
+                    1f,
                 )
             for (leash in openingTargets) {
                 animTransaction.setAlpha(leash, progress)
diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
index 498078b..ceffbe4 100644
--- a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
+++ b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
@@ -29,6 +29,7 @@
  */
 public class TaskGridNavHelper {
     public static final int CLEAR_ALL_PLACEHOLDER_ID = -1;
+    public static final int ADD_DESK_PLACEHOLDER_ID = -2;
 
     public static final int DIRECTION_UP = 0;
     public static final int DIRECTION_DOWN = 1;
@@ -41,44 +42,42 @@
     public @interface TASK_NAV_DIRECTION {}
 
     private final IntArray mOriginalTopRowIds;
-    private IntArray mTopRowIds;
-    private IntArray mBottomRowIds;
+    private final IntArray mTopRowIds = new IntArray();
+    private final IntArray mBottomRowIds = new IntArray();
 
     public TaskGridNavHelper(IntArray topIds, IntArray bottomIds,
-            List<Integer> largeTileIds) {
+            List<Integer> largeTileIds, boolean hasAddDesktopButton) {
         mOriginalTopRowIds = topIds.clone();
-        generateTaskViewIdGrid(topIds, bottomIds, largeTileIds);
+        generateTaskViewIdGrid(topIds, bottomIds, largeTileIds, hasAddDesktopButton);
     }
 
     private void generateTaskViewIdGrid(IntArray topRowIdArray, IntArray bottomRowIdArray,
-            List<Integer> largeTileIds) {
-
-        int maxSize = Math.max(topRowIdArray.size(), bottomRowIdArray.size())
-                + largeTileIds.size();
-        int minSize = Math.min(topRowIdArray.size(), bottomRowIdArray.size())
-                + largeTileIds.size();
-
-        // Add Large tile task views first at the beginning
-        for (int i = 0; i < largeTileIds.size(); i++) {
-            topRowIdArray.add(i, largeTileIds.get(i));
-            bottomRowIdArray.add(i, largeTileIds.get(i));
+            List<Integer> largeTileIds, boolean hasAddDesktopButton) {
+        // Add AddDesktopButton and lage tiles to both rows.
+        if (hasAddDesktopButton) {
+            mTopRowIds.add(ADD_DESK_PLACEHOLDER_ID);
+            mBottomRowIds.add(ADD_DESK_PLACEHOLDER_ID);
         }
+        for (Integer tileId : largeTileIds) {
+            mTopRowIds.add(tileId);
+            mBottomRowIds.add(tileId);
+        }
+
+        // Add row ids to their respective rows.
+        mTopRowIds.addAll(topRowIdArray);
+        mBottomRowIds.addAll(bottomRowIdArray);
 
         // Fill in the shorter array with the ids from the longer one.
-        for (int i = minSize; i < maxSize; i++) {
-            if (i >= topRowIdArray.size()) {
-                topRowIdArray.add(bottomRowIdArray.get(i));
-            } else {
-                bottomRowIdArray.add(topRowIdArray.get(i));
-            }
+        while (mTopRowIds.size() > mBottomRowIds.size()) {
+            mBottomRowIds.add(mTopRowIds.get(mBottomRowIds.size()));
+        }
+        while (mBottomRowIds.size() > mTopRowIds.size()) {
+            mTopRowIds.add(mBottomRowIds.get(mTopRowIds.size()));
         }
 
-        // Add the clear all button to the end of both arrays
-        topRowIdArray.add(CLEAR_ALL_PLACEHOLDER_ID);
-        bottomRowIdArray.add(CLEAR_ALL_PLACEHOLDER_ID);
-
-        mTopRowIds = topRowIdArray;
-        mBottomRowIds = bottomRowIdArray;
+        // Add the clear all button to the end of both arrays.
+        mTopRowIds.add(CLEAR_ALL_PLACEHOLDER_ID);
+        mBottomRowIds.add(CLEAR_ALL_PLACEHOLDER_ID);
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
index e353160..9f3c017 100644
--- a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
+++ b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
@@ -17,13 +17,15 @@
 package com.android.quickstep.views
 
 import android.content.Context
-import android.graphics.drawable.ShapeDrawable
-import android.graphics.drawable.shapes.RoundRectShape
+import android.graphics.Canvas
+import android.graphics.Rect
 import android.util.AttributeSet
 import android.widget.ImageButton
 import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
 import com.android.launcher3.R
 import com.android.launcher3.util.MultiPropertyFactory
+import com.android.quickstep.util.BorderAnimator
+import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator
 
 /**
  * Button for supporting multiple desktop sessions. The button will be next to the first TaskView
@@ -55,20 +57,49 @@
             multiTranslationX[TranslationX.OFFSET.ordinal].value = value
         }
 
-    override fun onFinishInflate() {
-        super.onFinishInflate()
+    private val focusBorderAnimator: BorderAnimator =
+        createSimpleBorderAnimator(
+            context.resources.getDimensionPixelSize(R.dimen.add_desktop_button_size),
+            context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width),
+            this::getBorderBounds,
+            this,
+            context
+                .obtainStyledAttributes(attrs, R.styleable.AddDesktopButton)
+                .getColor(
+                    R.styleable.AddDesktopButton_focusBorderColor,
+                    BorderAnimator.DEFAULT_BORDER_COLOR,
+                ),
+        )
 
-        background =
-            ShapeDrawable().apply {
-                shape =
-                    RoundRectShape(
-                        FloatArray(8) { R.dimen.add_desktop_button_size.toFloat() },
-                        null,
-                        null,
-                    )
-                setTint(
-                    resources.getColor(android.R.color.system_surface_bright_light, context.theme)
-                )
+    var borderEnabled = false
+        set(value) {
+            if (field == value) {
+                return
             }
+            field = value
+            focusBorderAnimator.setBorderVisibility(visible = field && isFocused, animated = true)
+        }
+
+    public override fun onFocusChanged(
+        gainFocus: Boolean,
+        direction: Int,
+        previouslyFocusedRect: Rect?,
+    ) {
+        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
+        if (borderEnabled) {
+            focusBorderAnimator.setBorderVisibility(gainFocus, /* animated= */ true)
+        }
+    }
+
+    private fun getBorderBounds(bounds: Rect) {
+        bounds.set(0, 0, width, height)
+        val outlinePadding =
+            context.resources.getDimensionPixelSize(R.dimen.add_desktop_button_outline_padding)
+        bounds.inset(-outlinePadding, -outlinePadding)
+    }
+
+    override fun draw(canvas: Canvas) {
+        focusBorderAnimator.drawBorder(canvas)
+        super.draw(canvas)
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index bb6829a..02be373 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -53,6 +53,7 @@
 import com.android.quickstep.recents.di.get
 import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData
 import com.android.quickstep.recents.ui.viewmodel.DesktopTaskViewModel
+import com.android.quickstep.recents.ui.viewmodel.TaskData
 import com.android.quickstep.task.thumbnail.TaskThumbnailView
 import com.android.quickstep.util.RecentsOrientedState
 import com.android.systemui.shared.recents.model.Task
@@ -364,6 +365,10 @@
         taskContainer.snapshotView.contentDescription = taskContainer.task.titleDescription
     }
 
+    override fun setIconState(container: TaskContainer, state: TaskData?) {
+        container.snapshotView.contentDescription = (state as? TaskData.Data)?.titleDescription
+    }
+
     // Ignoring [onIconUnloaded] as all tasks shares the same Desktop icon
     override fun onIconUnloaded(taskContainer: TaskContainer) {}
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index dec603b..a76ebdb 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1633,6 +1633,9 @@
             taskView.setBorderEnabled(enabled);
         }
         mClearAllButton.setBorderEnabled(enabled);
+        if (mAddDesktopButton != null) {
+            mAddDesktopButton.setBorderEnabled(enabled);
+        }
     }
 
     /**
@@ -3538,11 +3541,6 @@
     }
 
     private void setTaskThumbnailSplashAlpha(float taskThumbnailSplashAlpha) {
-        if (enableRefactorTaskThumbnail()) {
-            mRecentsViewModel.updateThumbnailSplashProgress(taskThumbnailSplashAlpha);
-            return;
-        }
-
         mTaskThumbnailSplashAlpha = taskThumbnailSplashAlpha;
         for (TaskView taskView : getTaskViews()) {
             taskView.setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha);
@@ -4571,24 +4569,32 @@
 
         // Init task grid nav helper with top/bottom id arrays.
         TaskGridNavHelper taskGridNavHelper = new TaskGridNavHelper(getTopRowIdArray(),
-                getBottomRowIdArray(), mUtils.getLargeTaskViewIds());
+                getBottomRowIdArray(), mUtils.getLargeTaskViewIds(), mAddDesktopButton != null);
 
         // Get current page's task view ID.
         TaskView currentPageTaskView = getCurrentPageTaskView();
         int currentPageTaskViewId;
+        final int clearAllButtonIndex = indexOfChild(mClearAllButton);
+        final int addDesktopButtonIndex = indexOfChild(mAddDesktopButton);
         if (currentPageTaskView != null) {
             currentPageTaskViewId = currentPageTaskView.getTaskViewId();
-        } else if (mCurrentPage == indexOfChild(mClearAllButton)) {
+        } else if (mCurrentPage == clearAllButtonIndex) {
             currentPageTaskViewId = TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID;
+        } else if (mCurrentPage == addDesktopButtonIndex) {
+            currentPageTaskViewId = TaskGridNavHelper.ADD_DESK_PLACEHOLDER_ID;
         } else {
             return INVALID_PAGE;
         }
 
-        int nextGridPage =
+        final int nextGridPage =
                 taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
-        return nextGridPage == TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID
-                ? indexOfChild(mClearAllButton)
-                : indexOfChild(getTaskViewFromTaskViewId(nextGridPage));
+        if (nextGridPage == TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID) {
+            return clearAllButtonIndex;
+        }
+        if (nextGridPage == TaskGridNavHelper.ADD_DESK_PLACEHOLDER_ID) {
+            return addDesktopButtonIndex;
+        }
+        return indexOfChild(getTaskViewFromTaskViewId(nextGridPage));
     }
 
     private void runDismissAnimation(PendingAnimation pendingAnim) {
@@ -6126,8 +6132,10 @@
     }
 
     private int getFirstViewIndex() {
-        final TaskView firstView;
-        if (mShowAsGridLastOnLayout) {
+        final View firstView;
+        if (mAddDesktopButton != null) {
+            firstView = mAddDesktopButton;
+        } else if (mShowAsGridLastOnLayout) {
             // For grid Overview, it always start if a large tile (focused task or desktop task) if
             // they exist, otherwise it start with the first task.
             TaskView firstLargeTaskView = mUtils.getFirstLargeTaskView();
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index b6f6bed..bbe1af4 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -25,16 +25,14 @@
 import com.android.quickstep.TaskOverlayFactory
 import com.android.quickstep.ViewUtils.addAccessibleChildToList
 import com.android.quickstep.recents.di.RecentsDependencies
-import com.android.quickstep.recents.di.get
 import com.android.quickstep.recents.di.getScope
 import com.android.quickstep.recents.di.inject
 import com.android.quickstep.recents.ui.mapper.TaskUiStateMapper
 import com.android.quickstep.recents.ui.viewmodel.TaskData
-import com.android.quickstep.recents.viewmodel.TaskContainerViewModel
 import com.android.quickstep.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.viewmodel.TaskContainerData
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
 import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
 
 /** Holder for all Task dependent information. */
 class TaskContainer(
@@ -56,20 +54,14 @@
     taskOverlayFactory: TaskOverlayFactory,
 ) {
     val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
-    lateinit var taskContainerData: TaskContainerData
 
+    // TODO(b/390581380): Remove this after this bug is fixed
     private val taskThumbnailViewModel: TaskThumbnailViewModel by
         RecentsDependencies.inject(snapshotView)
 
-    // TODO(b/335649589): Ideally create and obtain this from DI.
-    private val taskContainerViewModel: TaskContainerViewModel by lazy {
-        TaskContainerViewModel(splashAlphaUseCase = RecentsDependencies.get())
-    }
-
     init {
         if (enableRefactorTaskThumbnail()) {
             require(snapshotView is TaskThumbnailView)
-            taskContainerData = RecentsDependencies.get(this)
             RecentsDependencies.getScope(snapshotView).apply {
                 val taskViewScope = RecentsDependencies.getScope(taskView)
                 linkTo(taskViewScope)
@@ -82,9 +74,11 @@
         }
     }
 
-    var splitAnimationThumbnail: Bitmap? = null
-        get() = if (enableRefactorTaskThumbnail()) field else thumbnailViewDeprecated.thumbnail
-        private set
+    internal var thumbnailData: ThumbnailData? = null
+    val splitAnimationThumbnail: Bitmap?
+        get() =
+            if (enableRefactorTaskThumbnail()) thumbnailData?.thumbnail
+            else thumbnailViewDeprecated.thumbnail
 
     val thumbnailView: TaskThumbnailView
         get() {
@@ -98,10 +92,12 @@
             return snapshotView as TaskThumbnailViewDeprecated
         }
 
+    var isThumbnailValid: Boolean = false
+        internal set
+
     val shouldShowSplashView: Boolean
         get() =
-            if (enableRefactorTaskThumbnail())
-                taskContainerViewModel.shouldShowThumbnailSplash(task.key.id)
+            if (enableRefactorTaskThumbnail()) taskView.shouldShowSplash()
             else thumbnailViewDeprecated.shouldShowSplashView()
 
     /** Builds proto for logging */
@@ -111,7 +107,7 @@
     fun bind() {
         digitalWellBeingToast?.bind(task, taskView, snapshotView, stagePosition)
         if (enableRefactorTaskThumbnail()) {
-            bindThumbnailView()
+            taskThumbnailViewModel.bind(task.key.id)
         } else {
             thumbnailViewDeprecated.bind(task, overlay, taskView)
         }
@@ -126,6 +122,9 @@
         if (enableRefactorTaskThumbnail()) {
             RecentsDependencies.getInstance().removeScope(snapshotView)
             RecentsDependencies.getInstance().removeScope(this)
+            isThumbnailValid = false
+        } else {
+            thumbnailViewDeprecated.setShowSplashForSplitSelection(false)
         }
     }
 
@@ -134,10 +133,6 @@
         thumbnailView.destroyScopes()
     }
 
-    private fun bindThumbnailView() {
-        taskThumbnailViewModel.bind(task.key.id)
-    }
-
     fun setOverlayEnabled(enabled: Boolean) {
         if (!enableRefactorTaskThumbnail()) {
             thumbnailViewDeprecated.setOverlayEnabled(enabled)
@@ -157,15 +152,41 @@
             TaskUiStateMapper.toTaskThumbnailUiState(state, liveTile, hasHeader),
             state?.taskId,
         )
-        splitAnimationThumbnail =
-            if (state is TaskData.Data) state.thumbnailData?.thumbnail else null
+        thumbnailData = if (state is TaskData.Data) state.thumbnailData else null
     }
 
     fun updateTintAmount(tintAmount: Float) {
         thumbnailView.updateTintAmount(tintAmount)
     }
 
+    /**
+     * Updates the progress of the menu opening animation.
+     *
+     * This function propagates the given `progress` value to the `thumbnailView` allowing the
+     * thumbnail view to animate its visual state in sync with the menu's opening/closing
+     * transition.
+     *
+     * @param progress The progress of the menu opening animation (from closed=0 to fully open=1)
+     */
     fun updateMenuOpenProgress(progress: Float) {
         thumbnailView.updateMenuOpenProgress(progress)
     }
+
+    /**
+     * Updates the thumbnail splash progress for a given task.
+     *
+     * This function manages the visual feedback of a "splash" effect that can be displayed over a
+     * thumbnail image, typically during loading or updating. It calculates the alpha (transparency)
+     * of the splash based on the provided progress and then applies this alpha to the thumbnail
+     * view if it should be displayed.
+     *
+     * @param progress The progress of the operation, ranging from 0.0 to 1.0
+     */
+    fun updateThumbnailSplashProgress(progress: Float) {
+        if (enableRefactorTaskThumbnail()) {
+            thumbnailView.updateSplashAlpha(progress)
+        } else {
+            thumbnailViewDeprecated.setSplashAlpha(progress)
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 4b1b8dc..609262f 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -41,6 +41,7 @@
 import android.widget.Toast
 import androidx.annotation.IntDef
 import androidx.annotation.VisibleForTesting
+import androidx.core.view.doOnLayout
 import androidx.core.view.updateLayoutParams
 import com.android.app.animation.Interpolators
 import com.android.launcher3.Flags.enableCursorHoverStates
@@ -80,6 +81,7 @@
 import com.android.quickstep.recents.di.RecentsDependencies
 import com.android.quickstep.recents.di.get
 import com.android.quickstep.recents.di.inject
+import com.android.quickstep.recents.ui.viewmodel.TaskData
 import com.android.quickstep.recents.ui.viewmodel.TaskTileUiState
 import com.android.quickstep.recents.ui.viewmodel.TaskViewModel
 import com.android.quickstep.util.ActiveGestureErrorDetector
@@ -248,8 +250,43 @@
             )
 
     private val tempCoordinates = FloatArray(2)
-    private val focusBorderAnimator: BorderAnimator?
-    private val hoverBorderAnimator: BorderAnimator?
+    private val focusBorderAnimator: BorderAnimator? =
+        focusBorderAnimator
+            ?: createSimpleBorderAnimator(
+                TaskCornerRadius.get(context).toInt(),
+                context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width),
+                this::getThumbnailBounds,
+                this,
+                context
+                    .obtainStyledAttributes(attrs, R.styleable.TaskView, defStyleAttr, defStyleRes)
+                    .getColor(
+                        R.styleable.TaskView_focusBorderColor,
+                        BorderAnimator.DEFAULT_BORDER_COLOR,
+                    ),
+            )
+
+    private val hoverBorderAnimator: BorderAnimator? =
+        hoverBorderAnimator
+            ?: if (enableCursorHoverStates())
+                createSimpleBorderAnimator(
+                    TaskCornerRadius.get(context).toInt(),
+                    context.resources.getDimensionPixelSize(R.dimen.task_hover_border_width),
+                    this::getThumbnailBounds,
+                    this,
+                    context
+                        .obtainStyledAttributes(
+                            attrs,
+                            R.styleable.TaskView,
+                            defStyleAttr,
+                            defStyleRes,
+                        )
+                        .getColor(
+                            R.styleable.TaskView_hoverBorderColor,
+                            BorderAnimator.DEFAULT_BORDER_COLOR,
+                        ),
+                )
+            else null
+
     private val rootViewDisplayId: Int
         get() = rootView.display?.displayId ?: Display.DEFAULT_DISPLAY
 
@@ -295,6 +332,12 @@
             onModalnessUpdated(field)
         }
 
+    var splitSplashAlpha = 0f
+        set(value) {
+            field = value
+            applyThumbnailSplashAlpha()
+        }
+
     protected var taskThumbnailSplashAlpha = 0f
         set(value) {
             field = value
@@ -519,40 +562,7 @@
     init {
         setOnClickListener { _ -> onClick() }
 
-        val cursorHoverStatesEnabled = enableCursorHoverStates()
-        setWillNotDraw(!cursorHoverStatesEnabled)
-        context.obtainStyledAttributes(attrs, R.styleable.TaskView, defStyleAttr, defStyleRes).use {
-            this.focusBorderAnimator =
-                focusBorderAnimator
-                    ?: createSimpleBorderAnimator(
-                        TaskCornerRadius.get(context).toInt(),
-                        context.resources.getDimensionPixelSize(
-                            R.dimen.keyboard_quick_switch_border_width
-                        ),
-                        { bounds: Rect -> getThumbnailBounds(bounds) },
-                        this,
-                        it.getColor(
-                            R.styleable.TaskView_focusBorderColor,
-                            BorderAnimator.DEFAULT_BORDER_COLOR,
-                        ),
-                    )
-            this.hoverBorderAnimator =
-                hoverBorderAnimator
-                    ?: if (cursorHoverStatesEnabled)
-                        createSimpleBorderAnimator(
-                            TaskCornerRadius.get(context).toInt(),
-                            context.resources.getDimensionPixelSize(
-                                R.dimen.task_hover_border_width
-                            ),
-                            { bounds: Rect -> getThumbnailBounds(bounds) },
-                            this,
-                            it.getColor(
-                                R.styleable.TaskView_hoverBorderColor,
-                                BorderAnimator.DEFAULT_BORDER_COLOR,
-                            ),
-                        )
-                    else null
-        }
+        setWillNotDraw(!enableCursorHoverStates())
     }
 
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
@@ -645,6 +655,8 @@
         viewModel = null
         attachAlpha = 1f
         splitAlpha = 1f
+        splitSplashAlpha = 0f
+        taskThumbnailSplashAlpha = 0f
         // Clear any references to the thumbnail (it will be re-read either from the cache or the
         // system on next bind)
         if (!enableRefactorTaskThumbnail()) {
@@ -762,14 +774,30 @@
         // Updating containers
         val mapOfTasks = state.tasks.associateBy { it.taskId }
         taskContainers.forEach { container ->
+            val containerState = mapOfTasks[container.task.key.id]
             container.setState(
-                state = mapOfTasks[container.task.key.id],
+                state = containerState,
                 liveTile = state.isLiveTile,
                 hasHeader = type == TaskViewType.DESKTOP,
             )
+            updateThumbnailValidity(container)
+
+            if (enableOverviewIconMenu()) {
+                setIconState(container, containerState)
+            }
         }
     }
 
+    private fun updateThumbnailValidity(container: TaskContainer) {
+        container.isThumbnailValid =
+            viewModel!!.isThumbnailValid(
+                thumbnail = container.thumbnailData,
+                width = container.thumbnailView.width,
+                height = container.thumbnailView.height,
+            )
+        applyThumbnailSplashAlpha()
+    }
+
     override fun onDetachedFromWindow() {
         super.onDetachedFromWindow()
         if (enableRefactorTaskThumbnail()) {
@@ -806,7 +834,7 @@
         onBind(orientedState)
     }
 
-    open fun onBind(orientedState: RecentsOrientedState) {
+    protected open fun onBind(orientedState: RecentsOrientedState) {
         if (enableRefactorTaskThumbnail()) {
             viewModel =
                 TaskViewModel(
@@ -814,20 +842,37 @@
                         recentsViewData = RecentsDependencies.get(),
                         getTaskUseCase = RecentsDependencies.get(),
                         getSysUiStatusNavFlagsUseCase = RecentsDependencies.get(),
+                        isThumbnailValidUseCase = RecentsDependencies.get(),
                         dispatcherProvider = RecentsDependencies.get(),
                     )
                     .apply { bind(*taskIds) }
         }
 
-        taskContainers.forEach {
-            it.bind()
+        taskContainers.forEach { container ->
+            container.bind()
             if (enableRefactorTaskThumbnail()) {
-                it.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
+                container.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
+                container.thumbnailView.doOnLayout { updateThumbnailValidity(container) }
             }
         }
         setOrientationState(orientedState)
     }
 
+    private fun applyThumbnailSplashAlpha() {
+        val alpha = getSplashAlphaProgress()
+        taskContainers.forEach { it.updateThumbnailSplashProgress(alpha) }
+    }
+
+    private fun getSplashAlphaProgress(): Float =
+        when {
+            !enableRefactorTaskThumbnail() -> taskThumbnailSplashAlpha
+            splitSplashAlpha > 0f -> splitSplashAlpha
+            shouldShowSplash() -> taskThumbnailSplashAlpha
+            else -> 0f
+        }
+
+    internal fun shouldShowSplash(): Boolean = taskContainers.any { !it.isThumbnailValid }
+
     protected fun createTaskContainer(
         task: Task,
         @IdRes thumbnailViewId: Int,
@@ -989,7 +1034,7 @@
                 }
             }
         }
-        if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
+        if (needsUpdate(changes, FLAG_UPDATE_ICON) && !enableOverviewIconMenu()) {
             taskContainers.forEach {
                 if (visible) {
                     recentsModel.iconCache
@@ -1020,10 +1065,23 @@
         pendingIconLoadRequests.clear()
     }
 
+    protected open fun setIconState(container: TaskContainer, state: TaskData?) {
+        if (enableOverviewIconMenu()) {
+            if (state is TaskData.Data) {
+                setIcon(container.iconView, state.icon)
+                container.iconView.setText(state.title)
+                container.digitalWellBeingToast?.initialize()
+            } else {
+                setIcon(container.iconView, null)
+                container.iconView.setText(null)
+            }
+        }
+    }
+
     protected open fun onIconLoaded(taskContainer: TaskContainer) {
         setIcon(taskContainer.iconView, taskContainer.task.icon)
         if (enableOverviewIconMenu()) {
-            setText(taskContainer.iconView, taskContainer.task.title)
+            taskContainer.iconView.setText(taskContainer.task.title)
         }
         taskContainer.digitalWellBeingToast?.initialize()
     }
@@ -1031,7 +1089,7 @@
     protected open fun onIconUnloaded(taskContainer: TaskContainer) {
         setIcon(taskContainer.iconView, null)
         if (enableOverviewIconMenu()) {
-            setText(taskContainer.iconView, null)
+            taskContainer.iconView.setText(null)
         }
     }
 
@@ -1056,10 +1114,6 @@
         }
     }
 
-    protected fun setText(iconView: TaskViewIcon, text: CharSequence?) {
-        iconView.setText(text)
-    }
-
     @JvmOverloads
     open fun setShouldShowScreenshot(
         shouldShowScreenshot: Boolean,
@@ -1293,6 +1347,7 @@
                     if (isQuickSwitch) {
                         setFreezeRecentTasksReordering()
                     }
+                    // TODO(b/331754864): Update this to use TV.shouldShowSplash
                     disableStartingWindow = firstTaskContainer.shouldShowSplashView
                 }
         Executors.UI_HELPER_EXECUTOR.execute {
@@ -1583,14 +1638,6 @@
         updateFullscreenParams()
     }
 
-    protected open fun applyThumbnailSplashAlpha() {
-        if (!enableRefactorTaskThumbnail()) {
-            taskContainers.forEach {
-                it.thumbnailViewDeprecated.setSplashAlpha(taskThumbnailSplashAlpha)
-            }
-        }
-    }
-
     private fun applyTranslationX() {
         translationX =
             dismissTranslationX +
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
index 3e0c186..1a2b1c3 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
@@ -18,11 +18,8 @@
 
 import android.graphics.Matrix
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
-import kotlinx.coroutines.flow.MutableStateFlow
 
 class FakeTaskThumbnailViewModel : TaskThumbnailViewModel {
-    override val splashAlpha = MutableStateFlow(0f)
-
     override fun bind(taskId: Int) {
         // no-op
     }
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
index 356080a..232a08a 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
@@ -125,7 +125,6 @@
                 as TaskThumbnailView
         taskThumbnailView.cornerRadius = CORNER_RADIUS
         val ttvDiScopeId = di.getScope(taskThumbnailView).scopeId
-        di.provide(TaskThumbnailViewData::class.java, ttvDiScopeId) { TaskThumbnailViewData() }
         di.provide(TaskThumbnailViewModel::class.java, ttvDiScopeId) { taskThumbnailViewModel }
 
         return taskThumbnailView
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
index 90c9553..0204b2d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -33,6 +33,7 @@
 import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
 import com.android.launcher3.util.TestUtil
 import com.android.quickstep.AllAppsActionManager
+import com.android.quickstep.fallback.window.RecentsDisplayModel
 import java.lang.reflect.Field
 import java.lang.reflect.ParameterizedType
 import java.util.Locale
@@ -110,6 +111,7 @@
                                     PendingIntent(IIntentSender.Default())
                                 },
                                 object : TaskbarNavButtonCallbacks {},
+                                RecentsDisplayModel.INSTANCE.get(context),
                             ) {
                             override fun recreateTaskbar() {
                                 super.recreateTaskbar()
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
index 0ae710f..56c01f9 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
@@ -68,7 +68,10 @@
                     touchInteractionService = mock(),
                     overviewComponentObserver = mock(),
                     taskAnimationManager = mock(),
-                    dispatcherProvider = TestDispatcherProvider(dispatcher)
+                    dispatcherProvider = TestDispatcherProvider(dispatcher),
+                    recentsDisplayModel = mock(),
+                    focusState = mock(),
+                    taskbarManager = mock(),
                 )
             )
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
index c399bdb..8b17958 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
@@ -97,6 +97,8 @@
         // Set desktop mode supported
         when(mContext.getResources()).thenReturn(mResources);
         when(mResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true);
+        when(mResources.getBoolean(R.bool.config_canInternalDisplayHostDesktops))
+                .thenReturn(true);
 
         mRecentTasksList = new RecentTasksList(mContext, mockMainThreadExecutor,
                 mockKeyguardManager, mSystemUiProxy, mTopTaskTracker);
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt
new file mode 100644
index 0000000..e8bca93
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2025 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.recents.domain.usecase
+
+import android.graphics.Bitmap
+import android.view.Surface
+import android.view.Surface.ROTATION_90
+import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class IsThumbnailValidUseCaseTest {
+    private val recentsRotationStateRepository = FakeRecentsRotationStateRepository()
+    private val systemUnderTest = IsThumbnailValidUseCase(recentsRotationStateRepository)
+
+    @Test
+    fun withNullThumbnail_returnsInvalid() = runTest {
+        val isThumbnailValid = systemUnderTest(thumbnailData = null, viewWidth = 0, viewHeight = 0)
+        assertThat(isThumbnailValid).isEqualTo(false)
+    }
+
+    @Test
+    fun sameAspectRatio_sameRotation_returnsValid() = runTest {
+        val isThumbnailValid =
+            systemUnderTest.invoke(
+                thumbnailData = createThumbnailData(),
+                viewWidth = THUMBNAIL_WIDTH * 2,
+                viewHeight = THUMBNAIL_HEIGHT * 2,
+            )
+        assertThat(isThumbnailValid).isEqualTo(true)
+    }
+
+    @Test
+    fun differentAspectRatio_sameRotation_returnsInvalid() = runTest {
+        val isThumbnailValid =
+            systemUnderTest.invoke(
+                thumbnailData = createThumbnailData(),
+                viewWidth = THUMBNAIL_WIDTH,
+                viewHeight = THUMBNAIL_HEIGHT * 2,
+            )
+        assertThat(isThumbnailValid).isEqualTo(false)
+    }
+
+    @Test
+    fun sameAspectRatio_differentRotation_returnsInvalid() = runTest {
+        val isThumbnailValid =
+            systemUnderTest.invoke(
+                thumbnailData = createThumbnailData(rotation = ROTATION_90),
+                viewWidth = THUMBNAIL_WIDTH * 2,
+                viewHeight = THUMBNAIL_HEIGHT * 2,
+            )
+        assertThat(isThumbnailValid).isEqualTo(false)
+    }
+
+    @Test
+    fun differentAspectRatio_differentRotation_returnsInvalid() = runTest {
+        val isThumbnailValid =
+            systemUnderTest.invoke(
+                thumbnailData = createThumbnailData(rotation = ROTATION_90),
+                viewWidth = THUMBNAIL_WIDTH,
+                viewHeight = THUMBNAIL_HEIGHT * 2,
+            )
+        assertThat(isThumbnailValid).isEqualTo(false)
+    }
+
+    private fun createThumbnailData(
+        rotation: Int = Surface.ROTATION_0,
+        width: Int = THUMBNAIL_WIDTH,
+        height: Int = THUMBNAIL_HEIGHT,
+    ): ThumbnailData {
+        val bitmap = mock<Bitmap>()
+        whenever(bitmap.width).thenReturn(width)
+        whenever(bitmap.height).thenReturn(height)
+        return ThumbnailData(thumbnail = bitmap, rotation = rotation)
+    }
+
+    companion object {
+        const val THUMBNAIL_WIDTH = 100
+        const val THUMBNAIL_HEIGHT = 200
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
index c031150..08e459b 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
@@ -25,9 +25,11 @@
 import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV
 import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS
 import com.android.launcher3.util.TestDispatcherProvider
+import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
 import com.android.quickstep.recents.domain.model.TaskModel
 import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
 import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.views.TaskViewType
 import com.android.systemui.shared.recents.model.ThumbnailData
@@ -41,6 +43,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
@@ -52,6 +58,8 @@
 
     private val recentsViewData = RecentsViewData()
     private val getTaskUseCase = mock<GetTaskUseCase>()
+    private val isThumbnailValidUseCase =
+        spy(IsThumbnailValidUseCase(FakeRecentsRotationStateRepository()))
     private lateinit var sut: TaskViewModel
 
     @Before
@@ -62,6 +70,7 @@
                 recentsViewData = recentsViewData,
                 getTaskUseCase = getTaskUseCase,
                 getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(),
+                isThumbnailValidUseCase = isThumbnailValidUseCase,
                 dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
             )
         whenever(getTaskUseCase.invoke(TASK_MODEL_1.id)).thenReturn(flow { emit(TASK_MODEL_1) })
@@ -102,6 +111,7 @@
                         recentsViewData = recentsViewData,
                         getTaskUseCase = getTaskUseCase,
                         getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(),
+                        isThumbnailValidUseCase = isThumbnailValidUseCase,
                         dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
                     )
                 sut.bind(TASK_MODEL_1.id)
@@ -225,6 +235,12 @@
             assertThat(sut.state.first()).isEqualTo(expectedResult)
         }
 
+    @Test
+    fun shouldShowSplash_calls_useCase() {
+        sut.isThumbnailValid(null, 0, 0)
+        verify(isThumbnailValidUseCase).invoke(anyOrNull(), anyInt(), anyInt())
+    }
+
     private fun TaskModel.toUiState() =
         TaskData.Data(
             taskId = id,
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
deleted file mode 100644
index 0767fb9..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * 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.task.thumbnail
-
-import android.content.ComponentName
-import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.Color
-import android.graphics.drawable.Drawable
-import android.view.Surface
-import android.view.Surface.ROTATION_90
-import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
-import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.viewmodel.TaskContainerData
-import com.android.systemui.shared.recents.model.Task
-import com.android.systemui.shared.recents.model.ThumbnailData
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-class SplashAlphaUseCaseTest {
-    private val recentsViewData = RecentsViewData()
-    private val taskContainerData = TaskContainerData()
-    private val taskThumbnailViewData = TaskThumbnailViewData()
-    private val recentTasksRepository = FakeTasksRepository()
-    private val recentsRotationStateRepository = FakeRecentsRotationStateRepository()
-    private val systemUnderTest =
-        SplashAlphaUseCase(
-            recentsViewData,
-            taskContainerData,
-            taskThumbnailViewData,
-            recentTasksRepository,
-            recentsRotationStateRepository,
-        )
-
-    @Test
-    fun execute_withNullThumbnail_showsSplash() = runTest {
-        assertThat(systemUnderTest.execute(0).first()).isEqualTo(SPLASH_HIDDEN)
-    }
-
-    @Test
-    fun execute_withTaskSpecificSplashAlpha_showsSplash() = runTest {
-        setupTask(2)
-        taskContainerData.thumbnailSplashProgress.value = 0.7f
-
-        assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.7f)
-    }
-
-    @Test
-    fun execute_withNoGlobalSplashEnabled_doesntShowSplash() = runTest {
-        setupTask(2)
-
-        assertThat(systemUnderTest.execute(2).first()).isEqualTo(SPLASH_HIDDEN)
-    }
-
-    @Test
-    fun execute_withSameAspectRatioAndRotation_withGlobalSplashEnabled_doesntShowSplash() =
-        runTest {
-            setupTask(2)
-            recentsViewData.thumbnailSplashProgress.value = 0.5f
-            taskThumbnailViewData.width.value = THUMBNAIL_WIDTH * 2
-            taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
-
-            assertThat(systemUnderTest.execute(2).first()).isEqualTo(SPLASH_HIDDEN)
-        }
-
-    @Test
-    fun execute_withDifferentAspectRatioAndSameRotation_showsSplash() = runTest {
-        setupTask(2)
-        recentsViewData.thumbnailSplashProgress.value = 0.5f
-        taskThumbnailViewData.width.value = THUMBNAIL_WIDTH
-        taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
-
-        assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
-    }
-
-    @Test
-    fun execute_withSameAspectRatioAndDifferentRotation_showsSplash() = runTest {
-        setupTask(2, createThumbnailData(rotation = ROTATION_90))
-        recentsViewData.thumbnailSplashProgress.value = 0.5f
-        taskThumbnailViewData.width.value = THUMBNAIL_WIDTH * 2
-        taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
-
-        assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
-    }
-
-    @Test
-    fun execute_withDifferentAspectRatioAndRotation_showsSplash() = runTest {
-        setupTask(2, createThumbnailData(rotation = ROTATION_90))
-        recentsViewData.thumbnailSplashProgress.value = 0.5f
-        taskThumbnailViewData.width.value = THUMBNAIL_WIDTH
-        taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
-
-        assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
-    }
-
-    private val tasks = (0..5).map(::createTaskWithId)
-
-    private fun setupTask(taskId: Int, thumbnailData: ThumbnailData = createThumbnailData()) {
-        recentTasksRepository.seedThumbnailData(mapOf(taskId to thumbnailData))
-        val expectedIconData = mock<Drawable>()
-        recentTasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
-        recentTasksRepository.seedTasks(tasks)
-        recentTasksRepository.setVisibleTasks(setOf(taskId))
-    }
-
-    private fun createThumbnailData(
-        rotation: Int = Surface.ROTATION_0,
-        width: Int = THUMBNAIL_WIDTH,
-        height: Int = THUMBNAIL_HEIGHT,
-    ): ThumbnailData {
-        val bitmap = mock<Bitmap>()
-        whenever(bitmap.width).thenReturn(width)
-        whenever(bitmap.height).thenReturn(height)
-
-        return ThumbnailData(thumbnail = bitmap, rotation = rotation)
-    }
-
-    private fun createTaskWithId(taskId: Int) =
-        Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
-            colorBackground = Color.argb(taskId, taskId, taskId, taskId)
-        }
-
-    companion object {
-        const val THUMBNAIL_WIDTH = 100
-        const val THUMBNAIL_HEIGHT = 200
-
-        const val SPLASH_HIDDEN = 0f
-        const val SPLASH_SHOWN = 1f
-    }
-}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
index aec586d..4b4e2eb 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
@@ -19,7 +19,6 @@
 import android.graphics.Matrix
 import android.platform.test.flag.junit.SetFlagsRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.launcher3.util.TestDispatcherProvider
 import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
 import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
@@ -42,17 +41,9 @@
     private val dispatcher = StandardTestDispatcher()
     private val testScope = TestScope(dispatcher)
 
-    private val dispatcherProvider = TestDispatcherProvider(dispatcher)
     private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
-    private val splashAlphaUseCase: SplashAlphaUseCase = mock()
 
-    private val systemUnderTest by lazy {
-        TaskThumbnailViewModelImpl(
-            dispatcherProvider,
-            mGetThumbnailPositionUseCase,
-            splashAlphaUseCase,
-        )
-    }
+    private val systemUnderTest by lazy { TaskThumbnailViewModelImpl(mGetThumbnailPositionUseCase) }
 
     @Test
     fun getSnapshotMatrix_MissingThumbnail() =
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
index 7aab75f..7066d21 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
@@ -16,6 +16,7 @@
 package com.android.quickstep.util
 
 import com.android.launcher3.util.IntArray
+import com.android.quickstep.util.TaskGridNavHelper.ADD_DESK_PLACEHOLDER_ID
 import com.android.quickstep.util.TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID
 import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN
 import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT
@@ -619,6 +620,161 @@
             .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
     }
 
+    /*
+                        5   3   1→----|
+                                      ↓
+         CLEAR_ALL                   ADD_DESKTOP
+                        6   4   2
+    */
+    @Test
+    fun withAddDesktopButton_pressRightFromTop_goesToAddDesktopButton() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = 1,
+                    DIRECTION_RIGHT,
+                    delta = -1,
+                    hasAddDesktopButton = true,
+                )
+            )
+            .isEqualTo(ADD_DESK_PLACEHOLDER_ID)
+    }
+
+    /*
+                    5   3   1
+       CLEAR_ALL                 ADD_DESKTOP
+                                  ↑
+                    6   4   2→----↑
+    */
+    @Test
+    fun withAddDesktopButton_pressRightFromBottom_goesToAddDesktopButton() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = 2,
+                    DIRECTION_RIGHT,
+                    delta = -1,
+                    hasAddDesktopButton = true,
+                )
+            )
+            .isEqualTo(ADD_DESK_PLACEHOLDER_ID)
+    }
+
+    /*
+         ↓-------------------------------←|
+         |                                ↑
+         ↓      5   3   1                 |
+    CLEAR_ALL               ADD_DESKTOP--→
+                6   4   2
+    */
+    @Test
+    fun withAddDesktopButton_pressRightFromAddDesktopButton_goesToClearAllButton() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
+                    DIRECTION_RIGHT,
+                    delta = -1,
+                    hasAddDesktopButton = true,
+                )
+            )
+            .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
+    }
+
+    /*
+           |→--------------------------------|
+           |                                 |
+           ↑                5   3   1        ↓
+           ←------CLEAR_ALL             ADD_DESKTOP
+
+                            6   4   2
+    */
+    @Test
+    fun withAddDesktopButton_pressLeftFromClearAllButton_goesToAddDesktopButton() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
+                    DIRECTION_LEFT,
+                    delta = 1,
+                    hasAddDesktopButton = true,
+                )
+            )
+            .isEqualTo(ADD_DESK_PLACEHOLDER_ID)
+    }
+
+    /*
+                        5   3   1
+                                   ←--↑
+        CLEAR_ALL                  ↓-→ADD_DESKTOP
+                        6   4   2
+    */
+    @Test
+    fun withAddDesktopButton_pressUpOnAddDesktop_stayOnAddDesktopButton() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
+                    DIRECTION_UP,
+                    delta = 1,
+                    hasAddDesktopButton = true,
+                )
+            )
+            .isEqualTo(ADD_DESK_PLACEHOLDER_ID)
+    }
+
+    /*
+                        5   3   1
+        CLEAR_ALL                  ↑--→ADD_DESKTOP
+                                   ↑←--↓
+                        6   4   2
+    */
+    @Test
+    fun withAddDesktopButton_pressDownOnAddDesktop_stayOnAddDesktopButton() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
+                    DIRECTION_DOWN,
+                    delta = 1,
+                    hasAddDesktopButton = true,
+                )
+            )
+            .isEqualTo(ADD_DESK_PLACEHOLDER_ID)
+    }
+
+    /*
+                        5   3   1
+           CLEAR_ALL                DESKTOP--→ADD_DESKTOP
+                        6   4   2
+    */
+    @Test
+    fun withAddDesktopButton_pressRightFromDesktopTask_goesToAddDesktopButton() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
+                    DIRECTION_LEFT,
+                    delta = 1,
+                    largeTileIds = listOf(DESKTOP_TASK_ID),
+                    hasAddDesktopButton = true,
+                )
+            )
+            .isEqualTo(DESKTOP_TASK_ID)
+    }
+
+    /*
+                        5   3   1
+           CLEAR_ALL                DESKTOP←--ADD_DESKTOP
+                        6   4   2
+    */
+    @Test
+    fun withAddDesktopButton_pressLeftFromAddDesktopButton_goesToDesktopTask() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = DESKTOP_TASK_ID,
+                    DIRECTION_RIGHT,
+                    delta = -1,
+                    largeTileIds = listOf(DESKTOP_TASK_ID),
+                    hasAddDesktopButton = true,
+                )
+            )
+            .isEqualTo(ADD_DESK_PLACEHOLDER_ID)
+    }
+
     private fun getNextGridPage(
         currentPageTaskViewId: Int,
         direction: Int,
@@ -626,8 +782,10 @@
         topIds: IntArray = IntArray.wrap(1, 3, 5),
         bottomIds: IntArray = IntArray.wrap(2, 4, 6),
         largeTileIds: List<Int> = emptyList(),
+        hasAddDesktopButton: Boolean = false,
     ): Int {
-        val taskGridNavHelper = TaskGridNavHelper(topIds, bottomIds, largeTileIds)
+        val taskGridNavHelper =
+            TaskGridNavHelper(topIds, bottomIds, largeTileIds, hasAddDesktopButton)
         return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, true)
     }
 
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index e47a44a..add8a05 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE;
 import static com.android.launcher3.LauncherPrefs.FIXED_LANDSCAPE_MODE;
 import static com.android.launcher3.LauncherPrefs.GRID_NAME;
+import static com.android.launcher3.LauncherPrefs.NON_FIXED_LANDSCAPE_GRID_NAME;
 import static com.android.launcher3.Utilities.dpiFromPx;
 import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
@@ -93,6 +94,8 @@
             new MainThreadInitializedObject<>(InvariantDeviceProfile::new);
 
     public static final String GRID_NAME_PREFS_KEY = "idp_grid_name";
+    public static final String NON_FIXED_LANDSCAPE_GRID_NAME_PREFS_KEY =
+            "idp_non_fixed_landscape_grid_name";
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({TYPE_PHONE, TYPE_MULTI_DISPLAY, TYPE_TABLET})
@@ -268,7 +271,14 @@
             if (FIXED_LANDSCAPE_MODE.getSharedPrefKey().equals(key)
                     && isFixedLandscape != FIXED_LANDSCAPE_MODE.get(context)) {
                 Trace.beginSection("InvariantDeviceProfile#setFixedLandscape");
-                onConfigChanged(context);
+                if (isFixedLandscape) {
+                    setCurrentGrid(
+                            context, LauncherPrefs.get(context).get(NON_FIXED_LANDSCAPE_GRID_NAME));
+                } else {
+                    LauncherPrefs.get(context)
+                            .put(NON_FIXED_LANDSCAPE_GRID_NAME, getCurrentGridName(context));
+                    onConfigChanged(context);
+                }
                 Trace.endSection();
             } else if (ENABLE_TWOLINE_ALLAPPS_TOGGLE.getSharedPrefKey().equals(key)
                     && enableTwoLinesInAllApps != ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(context)) {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 30ef24b..58fd154 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -29,6 +29,7 @@
 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
 import static com.android.launcher3.Flags.enableAddAppWidgetViaConfigActivityV2;
 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
+import static com.android.launcher3.Flags.enableStrictMode;
 import static com.android.launcher3.Flags.enableWorkspaceInflation;
 import static com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WIDGET_TRANSITION;
@@ -459,7 +460,8 @@
         Trace.beginAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME,
                 DISPLAY_ALL_APPS_TRACE_COOKIE);
         TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT);
-        if (DEBUG_STRICT_MODE) {
+        if (DEBUG_STRICT_MODE
+                || (FeatureFlags.IS_STUDIO_BUILD && enableStrictMode())) {
             StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                     .detectDiskReads()
                     .detectDiskWrites()
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index 1120ec8..2a5cd63 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -21,6 +21,7 @@
 import androidx.annotation.VisibleForTesting
 import com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN
 import com.android.launcher3.InvariantDeviceProfile.GRID_NAME_PREFS_KEY
+import com.android.launcher3.InvariantDeviceProfile.NON_FIXED_LANDSCAPE_GRID_NAME_PREFS_KEY
 import com.android.launcher3.LauncherFiles.DEVICE_PREFERENCES_KEY
 import com.android.launcher3.LauncherFiles.SHARED_PREFERENCES_KEY
 import com.android.launcher3.dagger.ApplicationContext
@@ -304,6 +305,16 @@
         @JvmField
         val FIXED_LANDSCAPE_MODE = backedUpItem(SettingsActivity.FIXED_LANDSCAPE_MODE, false)
 
+        @JvmField
+        val NON_FIXED_LANDSCAPE_GRID_NAME =
+            ConstantItem(
+                NON_FIXED_LANDSCAPE_GRID_NAME_PREFS_KEY,
+                isBackedUp = true,
+                defaultValue = null,
+                encryptionType = EncryptionType.ENCRYPTED,
+                type = String::class.java,
+            )
+
         // Preferences for widget configurations
         @JvmField
         val RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN =
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index 80743af..12c65c7 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -55,6 +55,8 @@
 
 import java.lang.ref.WeakReference;
 import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -124,6 +126,8 @@
     private static final int MESSAGE_ID_UPDATE_GRID = 7414;
     private static final int MESSAGE_ID_UPDATE_COLOR = 856;
 
+    private static final String DEFAULT_SHAPE_KEY = "circle";
+
     // Set of all active previews used to track duplicate memory allocations
     private final Set<PreviewLifecycleObserver> mActivePreviews =
             Collections.newSetFromMap(new ConcurrentHashMap<>());
@@ -157,7 +161,7 @@
                     // Handle default for when current shape doesn't match new shapes.
                     if (selectedShape.isEmpty()) {
                         selectedShape = Optional.ofNullable(ShapesProvider.INSTANCE.getIconShapes()
-                                .get("circle"));
+                                .get(DEFAULT_SHAPE_KEY));
                     }
 
                     for (IconShapeModel shape : ShapesProvider.INSTANCE.getIconShapes().values()) {
@@ -177,7 +181,13 @@
                         KEY_NAME, KEY_GRID_TITLE, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT,
                         KEY_IS_DEFAULT, KEY_GRID_ICON_ID});
                 InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(getContext());
-                for (GridOption gridOption : idp.parseAllGridOptions(getContext())) {
+                List<GridOption> gridOptionList = idp.parseAllGridOptions(getContext());
+                if (com.android.launcher3.Flags.oneGridSpecs()) {
+                    gridOptionList.sort(Comparator
+                            .comparingInt((GridOption option) -> option.numColumns)
+                            .reversed());
+                }
+                for (GridOption gridOption : gridOptionList) {
                     cursor.newRow()
                             .add(KEY_NAME, gridOption.name)
                             .add(KEY_GRID_TITLE, gridOption.gridTitle)
diff --git a/src/com/android/launcher3/shapes/ShapesProvider.kt b/src/com/android/launcher3/shapes/ShapesProvider.kt
index f1ea3a0..7e1f640 100644
--- a/src/com/android/launcher3/shapes/ShapesProvider.kt
+++ b/src/com/android/launcher3/shapes/ShapesProvider.kt
@@ -152,13 +152,20 @@
     val iconShapes =
         if (Flags.newCustomizationPickerUi() && LauncherFlags.enableLauncherIconShapes()) {
             mapOf(
-                "arch" to
+                "circle" to
                     IconShapeModel(
-                        key = "arch",
-                        title = "arch",
+                        key = "circle",
+                        title = "circle",
+                        pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
+                        folderPathString = folderShapes["clover"]!!,
+                    ),
+                "square" to
+                    IconShapeModel(
+                        key = "square",
+                        title = "square",
                         pathString =
-                            "M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 .1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z",
-                        folderPathString = folderShapes["arch"]!!,
+                            "M53.689 0.82 L53.689 .82 C67.434 .82 74.306 .82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311 V53.689 C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18 H46.311 C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758 .82 74.306 .82 67.434 .82 53.689 L.82 46.311 C.82 32.566 .82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694 .82 32.566 .82 46.311 .82Z",
+                        folderShapes["square"]!!,
                     ),
                 "four_sided_cookie" to
                     IconShapeModel(
@@ -176,6 +183,14 @@
                             "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82 -2.742 55.18 -2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24 .273 66.266 -2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z",
                         folderPathString = folderShapes["clover"]!!,
                     ),
+                "arch" to
+                    IconShapeModel(
+                        key = "arch",
+                        title = "arch",
+                        pathString =
+                            "M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 .1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z",
+                        folderPathString = folderShapes["arch"]!!,
+                    ),
                 "sunny" to
                     IconShapeModel(
                         key = "sunny",
@@ -184,21 +199,6 @@
                             "M42.846 4.873C46.084 -.531 53.916 -.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C -.531 53.916 -.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
                         folderPathString = folderShapes["clover"]!!,
                     ),
-                "circle" to
-                    IconShapeModel(
-                        key = "circle",
-                        title = "circle",
-                        pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
-                        folderPathString = folderShapes["clover"]!!,
-                    ),
-                "square" to
-                    IconShapeModel(
-                        key = "square",
-                        title = "square",
-                        pathString =
-                            "M53.689 0.82 L53.689 .82 C67.434 .82 74.306 .82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311 V53.689 C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18 H46.311 C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758 .82 74.306 .82 67.434 .82 53.689 L.82 46.311 C.82 32.566 .82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694 .82 32.566 .82 46.311 .82Z",
-                        folderShapes["square"]!!,
-                    ),
             )
         } else {
             mapOf(
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index afc0dd5..16faf14 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1272,8 +1272,6 @@
         if (getNavigationModel() == NavigationModel.ZERO_BUTTON
                 || isThreeFingerTrackpadGesture) {
             final Point displaySize = getRealDisplaySize();
-            // TODO(b/225505986): change startY and endY back to displaySize.y / 2 once the
-            //  issue is solved.
             int startX = isThreeFingerTrackpadGesture ? displaySize.x / 4 : 0;
             int endX = isThreeFingerTrackpadGesture ? displaySize.x * 3 / 4 : displaySize.x / 2;
             linearGesture(startX, displaySize.y / 4, endX, displaySize.y / 4,