Merge "Apply cutout offset when applicable" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index b647a3e..0fa3fbc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -247,7 +247,9 @@
                 ? context.getColor(R.color.taskbar_nav_icon_light_color)
                 : context.getColor(R.color.taskbar_nav_icon_dark_color);
 
-        mTaskbarTransitions = new TaskbarTransitions(mContext, mNavButtonsView);
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions = new TaskbarTransitions(mContext, mNavButtonsView);
+        }
     }
 
     /**
@@ -359,7 +361,9 @@
                 R.bool.floating_rotation_button_position_left);
         mControllers.rotationButtonController.setRotationButton(mFloatingRotationButton,
                 mRotationButtonListener);
-        mTaskbarTransitions.init();
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions.init();
+        }
 
         applyState();
         mPropertyHolders.forEach(StatePropertyHolder::endAnimation);
@@ -621,7 +625,9 @@
     }
 
     public void setWallpaperVisible(boolean isVisible) {
-        mTaskbarTransitions.setWallpaperVisibility(isVisible);
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions.setWallpaperVisibility(isVisible);
+        }
     }
 
     public void onTransitionModeUpdated(int barMode, boolean checkBarModes) {
@@ -632,25 +638,32 @@
     }
 
     public void checkNavBarModes() {
-        boolean isBarHidden = (mSysuiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0;
-        mTaskbarTransitions.transitionTo(mTransitionMode, !isBarHidden);
-    }
-
-    public void finishBarAnimations() {
-        mTaskbarTransitions.finishAnimations();
-    }
-
-    public void touchAutoDim(boolean reset) {
-        mTaskbarTransitions.setAutoDim(false);
-        mHandler.removeCallbacks(mAutoDim);
-        if (reset) {
-            mHandler.postDelayed(mAutoDim, AUTODIM_TIMEOUT_MS);
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            boolean isBarHidden = (mSysuiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0;
+            mTaskbarTransitions.transitionTo(mTransitionMode, !isBarHidden);
         }
     }
 
-    public void transitionTo(@BarTransitions.TransitionMode int barMode,
-            boolean animate) {
-        mTaskbarTransitions.transitionTo(barMode, animate);
+    public void finishBarAnimations() {
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions.finishAnimations();
+        }
+    }
+
+    public void touchAutoDim(boolean reset) {
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions.setAutoDim(false);
+            mHandler.removeCallbacks(mAutoDim);
+            if (reset) {
+                mHandler.postDelayed(mAutoDim, AUTODIM_TIMEOUT_MS);
+            }
+        }
+    }
+
+    public void transitionTo(@BarTransitions.TransitionMode int barMode, boolean animate) {
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions.transitionTo(barMode, animate);
+        }
     }
 
     /** Use to set the translationY for the all nav+contextual buttons */
@@ -752,7 +765,9 @@
 
     private void onDarkIntensityChanged() {
         updateNavButtonColor();
-        mTaskbarTransitions.onDarkIntensityChanged(mTaskbarNavButtonDarkIntensity.value);
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions.onDarkIntensityChanged(mTaskbarNavButtonDarkIntensity.value);
+        }
     }
 
     protected ImageView addButton(@DrawableRes int drawableId, @TaskbarButton int buttonType,
@@ -1100,7 +1115,9 @@
                 + mOnBackgroundNavButtonColorOverrideMultiplier.value);
 
         mNavButtonsView.dumpLogs(prefix + "\t", pw);
-        mTaskbarTransitions.dumpLogs(prefix + "\t", pw);
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions.dumpLogs(prefix + "\t", pw);
+        }
     }
 
     private static String getStateString(int flags) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index fc3b4c7..0a81f78 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -149,20 +149,39 @@
             taskListChangeId =
                 recentsModel.getTasks { tasks ->
                     allRecentTasks = tasks
+                    val oldRunningPackages = runningAppPackages
+                    val oldMinimizedPackages = minimizedAppPackages
                     desktopTask = allRecentTasks.filterIsInstance<DesktopTask>().firstOrNull()
-                    onRecentsOrHotseatChanged()
-                    controllers.taskbarViewController.commitRunningAppsToUI()
+                    val runningPackagesChanged = oldRunningPackages != runningAppPackages
+                    val minimizedPackagessChanged = oldMinimizedPackages != minimizedAppPackages
+                    if (
+                        onRecentsOrHotseatChanged() ||
+                            runningPackagesChanged ||
+                            minimizedPackagessChanged
+                    ) {
+                        controllers.taskbarViewController.commitRunningAppsToUI()
+                    }
                 }
         }
     }
 
-    private fun onRecentsOrHotseatChanged() {
+    /**
+     * Updates [shownTasks] when Recents or Hotseat changes.
+     *
+     * @return Whether [shownTasks] changed.
+     */
+    private fun onRecentsOrHotseatChanged(): Boolean {
+        val oldShownTasks = shownTasks
         shownTasks =
             if (isInDesktopMode) {
                 computeShownRunningTasks()
             } else {
                 computeShownRecentTasks()
             }
+        val shownTasksChanged = oldShownTasks != shownTasks
+        if (!shownTasksChanged) {
+            return shownTasksChanged
+        }
 
         for (groupTask in shownTasks) {
             for (task in groupTask.tasks) {
@@ -174,6 +193,7 @@
                 }
             }
         }
+        return shownTasksChanged
     }
 
     private fun computeShownRunningTasks(): List<GroupTask> {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
index dc6365b..181cba0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
@@ -79,6 +79,7 @@
 import com.android.launcher3.util.OnboardingPrefs.HOTSEAT_DISCOVERY_TIP_COUNT
 import com.android.launcher3.util.OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN
 import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP
+import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN
 import com.android.launcher3.util.PluginManagerWrapper
 import com.android.launcher3.util.StartActivityParams
 import com.android.launcher3.util.UserIconInfo
@@ -394,6 +395,7 @@
                 HOTSEAT_LONGPRESS_TIP_SEEN.sharedPrefKey
             )
             addOnboardPref("Taskbar Education", TASKBAR_EDU_TOOLTIP_STEP.sharedPrefKey)
+            addOnboardPref("Taskbar Search Education", TASKBAR_SEARCH_EDU_SEEN.sharedPrefKey)
             addOnboardPref("All Apps Visited Count", ALL_APPS_VISITED_COUNT.sharedPrefKey)
         }
     }
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index bdbe826..b6d83d3 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -2445,16 +2445,15 @@
             finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
             return;
         }
-        Optional<RemoteAnimationTarget> taskTargetOptional =
-                Arrays.stream(appearedTaskTargets)
-                        .filter(mGestureState.mLastStartedTaskIdPredicate)
-                        .findFirst();
-        if (!taskTargetOptional.isPresent()) {
+        RemoteAnimationTarget[] taskTargets = Arrays.stream(appearedTaskTargets)
+                .filter(mGestureState.mLastStartedTaskIdPredicate)
+                .toArray(RemoteAnimationTarget[]::new);
+        if (taskTargets.length == 0) {
             ActiveGestureLog.INSTANCE.addLog("No appeared task matching started task id");
             finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
             return;
         }
-        RemoteAnimationTarget taskTarget = taskTargetOptional.get();
+        RemoteAnimationTarget taskTarget = taskTargets[0];
         TaskView taskView = mRecentsView == null
                 ? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId);
         if (taskView == null
@@ -2468,13 +2467,13 @@
             finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
             return;
         }
-        animateSplashScreenExit(mContainer, appearedTaskTargets, taskTarget.leash);
+        animateSplashScreenExit(mContainer, appearedTaskTargets, taskTargets);
     }
 
     private void animateSplashScreenExit(
             @NonNull T activity,
             @NonNull RemoteAnimationTarget[] appearedTaskTargets,
-            @NonNull SurfaceControl leash) {
+            @NonNull RemoteAnimationTarget[] animatingTargets) {
         ViewGroup splashView = activity.getDragLayer();
         final QuickstepLauncher quickstepLauncher = activity instanceof QuickstepLauncher
                 ? (QuickstepLauncher) activity : null;
@@ -2492,26 +2491,28 @@
         }
         surfaceApplier.scheduleApply(transaction);
 
-        SplashScreenExitAnimationUtils.startAnimations(splashView, leash,
-                mSplashMainWindowShiftLength, new TransactionPool(), new Rect(),
-                SPLASH_ANIMATION_DURATION, SPLASH_FADE_OUT_DURATION,
-                /* iconStartAlpha= */ 0, /* brandingStartAlpha= */ 0,
-                SPLASH_APP_REVEAL_DELAY, SPLASH_APP_REVEAL_DURATION,
-                new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        // Hiding launcher which shows the app surface behind, then
-                        // finishing recents to the app. After transition finish, showing
-                        // the views on launcher again, so it can be visible when next
-                        // animation starts.
-                        splashView.setAlpha(0);
-                        if (quickstepLauncher != null) {
-                            quickstepLauncher.getDepthController()
-                                    .pauseBlursOnWindows(false);
+        for (RemoteAnimationTarget target : animatingTargets) {
+            SplashScreenExitAnimationUtils.startAnimations(splashView, target.leash,
+                    mSplashMainWindowShiftLength, new TransactionPool(), target.screenSpaceBounds,
+                    SPLASH_ANIMATION_DURATION, SPLASH_FADE_OUT_DURATION,
+                    /* iconStartAlpha= */ 0, /* brandingStartAlpha= */ 0,
+                    SPLASH_APP_REVEAL_DELAY, SPLASH_APP_REVEAL_DURATION,
+                    new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            // Hiding launcher which shows the app surface behind, then
+                            // finishing recents to the app. After transition finish, showing
+                            // the views on launcher again, so it can be visible when next
+                            // animation starts.
+                            splashView.setAlpha(0);
+                            if (quickstepLauncher != null) {
+                                quickstepLauncher.getDepthController()
+                                        .pauseBlursOnWindows(false);
+                            }
+                            finishRecentsAnimationOnTasksAppeared(() -> splashView.setAlpha(1));
                         }
-                        finishRecentsAnimationOnTasksAppeared(() -> splashView.setAlpha(1));
-                    }
-                });
+                    });
+        }
     }
 
     private void finishRecentsAnimationOnTasksAppeared(Runnable onFinishComplete) {
diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java
index 8d99069..307b2fa 100644
--- a/quickstep/src/com/android/quickstep/util/DesktopTask.java
+++ b/quickstep/src/com/android/quickstep/util/DesktopTask.java
@@ -22,6 +22,7 @@
 import com.android.systemui.shared.recents.model.Task;
 
 import java.util.List;
+import java.util.Objects;
 
 /**
  * A {@link Task} container that can contain N number of tasks that are part of the desktop in
@@ -68,4 +69,16 @@
         return "type=" + taskViewType + " tasks=" + tasks;
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof DesktopTask that)) return false;
+        if (!super.equals(o)) return false;
+        return Objects.equals(tasks, that.tasks);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), tasks);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java
index 945ffe3..e8b611c 100644
--- a/quickstep/src/com/android/quickstep/util/GroupTask.java
+++ b/quickstep/src/com/android/quickstep/util/GroupTask.java
@@ -26,6 +26,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * A {@link Task} container that can contain one or two tasks, depending on if the two tasks
@@ -91,4 +92,17 @@
         return "type=" + taskViewType + " task1=" + task1 + " task2=" + task2;
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof GroupTask that)) return false;
+        return taskViewType == that.taskViewType && Objects.equals(task1,
+                that.task1) && Objects.equals(task2, that.task2)
+                && Objects.equals(mSplitBounds, that.mSplitBounds);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(task1, task2, mSplitBounds, taskViewType);
+    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt
new file mode 100644
index 0000000..7aed579
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.util
+
+import android.content.ComponentName
+import android.content.Intent
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.systemui.shared.recents.model.Task
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+class DesktopTaskTest {
+
+    @Test
+    fun testDesktopTask_sameInstance_isEqual() {
+        val task = DesktopTask(createTasks(1))
+        assertThat(task).isEqualTo(task)
+    }
+
+    @Test
+    fun testDesktopTask_identicalConstructor_isEqual() {
+        val task1 = DesktopTask(createTasks(1))
+        val task2 = DesktopTask(createTasks(1))
+        assertThat(task1).isEqualTo(task2)
+    }
+
+    @Test
+    fun testDesktopTask_copy_isEqual() {
+        val task1 = DesktopTask(createTasks(1))
+        val task2 = task1.copy()
+        assertThat(task1).isEqualTo(task2)
+    }
+
+    @Test
+    fun testDesktopTask_differentId_isNotEqual() {
+        val task1 = DesktopTask(createTasks(1))
+        val task2 = DesktopTask(createTasks(2))
+        assertThat(task1).isNotEqualTo(task2)
+    }
+
+    @Test
+    fun testDesktopTask_differentLength_isNotEqual() {
+        val task1 = DesktopTask(createTasks(1))
+        val task2 = DesktopTask(createTasks(1, 2))
+        assertThat(task1).isNotEqualTo(task2)
+    }
+
+    private fun createTasks(vararg ids: Int): List<Task> {
+        return ids.map { Task(Task.TaskKey(it, 0, Intent(), ComponentName("", ""), 0, 0)) }
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
new file mode 100644
index 0000000..a6d3887
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.util
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Rect
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.quickstep.views.TaskView
+import com.android.systemui.shared.recents.model.Task
+import com.android.wm.shell.common.split.SplitScreenConstants
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+class GroupTaskTest {
+
+    @Test
+    fun testGroupTask_sameInstance_isEqual() {
+        val task = GroupTask(createTask(1))
+        assertThat(task).isEqualTo(task)
+    }
+
+    @Test
+    fun testGroupTask_identicalConstructor_isEqual() {
+        val task1 = GroupTask(createTask(1))
+        val task2 = GroupTask(createTask(1))
+        assertThat(task1).isEqualTo(task2)
+    }
+
+    @Test
+    fun testGroupTask_copy_isEqual() {
+        val task1 = GroupTask(createTask(1))
+        val task2 = task1.copy()
+        assertThat(task1).isEqualTo(task2)
+    }
+
+    @Test
+    fun testGroupTask_differentId_isNotEqual() {
+        val task1 = GroupTask(createTask(1))
+        val task2 = GroupTask(createTask(2))
+        assertThat(task1).isNotEqualTo(task2)
+    }
+
+    @Test
+    fun testGroupTask_equalSplitTasks_isEqual() {
+        val splitBounds =
+            SplitConfigurationOptions.SplitBounds(
+                Rect(),
+                Rect(),
+                1,
+                2,
+                SplitScreenConstants.SNAP_TO_50_50
+            )
+        val task1 = GroupTask(createTask(1), createTask(2), splitBounds, TaskView.Type.GROUPED)
+        val task2 = GroupTask(createTask(1), createTask(2), splitBounds, TaskView.Type.GROUPED)
+        assertThat(task1).isEqualTo(task2)
+    }
+
+    @Test
+    fun testGroupTask_differentSplitTasks_isNotEqual() {
+        val splitBounds1 =
+            SplitConfigurationOptions.SplitBounds(
+                Rect(),
+                Rect(),
+                1,
+                2,
+                SplitScreenConstants.SNAP_TO_50_50
+            )
+        val splitBounds2 =
+            SplitConfigurationOptions.SplitBounds(
+                Rect(),
+                Rect(),
+                1,
+                2,
+                SplitScreenConstants.SNAP_TO_30_70
+            )
+        val task1 = GroupTask(createTask(1), createTask(2), splitBounds1, TaskView.Type.GROUPED)
+        val task2 = GroupTask(createTask(1), createTask(2), splitBounds2, TaskView.Type.GROUPED)
+        assertThat(task1).isNotEqualTo(task2)
+    }
+
+    @Test
+    fun testGroupTask_differentType_isNotEqual() {
+        val task1 = GroupTask(createTask(1), null, null, TaskView.Type.SINGLE)
+        val task2 = GroupTask(createTask(1), null, null, TaskView.Type.DESKTOP)
+        assertThat(task1).isNotEqualTo(task2)
+    }
+
+    private fun createTask(id: Int): Task {
+        return Task(Task.TaskKey(id, 0, Intent(), ComponentName("", ""), 0, 0))
+    }
+}
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 486dc68..13c4f72 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -44,6 +44,7 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.kotlin.any
 import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
@@ -56,7 +57,6 @@
     @Mock private lateinit var mockRecentsModel: RecentsModel
     @Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController
 
-    private var nextTaskId: Int = 500
     private var taskListChangeId: Int = 1
 
     private lateinit var recentAppsController: TaskbarRecentAppsController
@@ -478,6 +478,82 @@
         assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
     }
 
+    @Test
+    fun onRecentTasksChanged_notInDesktopMode_noActualChangeToRecents_commitRunningAppsToUI_notCalled() {
+        setInDesktopMode(false)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = emptyList(),
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+        )
+        verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+        // Call onRecentTasksChanged() again with the same tasks, verify it's a no-op.
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = emptyList(),
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+        )
+        verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+    }
+
+    @Test
+    fun onRecentTasksChanged_inDesktopMode_noActualChangeToRunning_commitRunningAppsToUI_notCalled() {
+        setInDesktopMode(true)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+            recentTaskPackages = emptyList()
+        )
+        verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+        // Call onRecentTasksChanged() again with the same tasks, verify it's a no-op.
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+            recentTaskPackages = emptyList()
+        )
+        verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+    }
+
+    @Test
+    fun onRecentTasksChanged_onlyMinimizedChanges_commitRunningAppsToUI_isCalled() {
+        setInDesktopMode(true)
+        val runningTasks = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = runningTasks,
+            minimizedTaskIndices = setOf(0),
+            recentTaskPackages = emptyList()
+        )
+        verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+        // Call onRecentTasksChanged() again with a new minimized app, verify we update UI.
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTaskPackages = runningTasks,
+            minimizedTaskIndices = setOf(0, 1),
+            recentTaskPackages = emptyList()
+        )
+        verify(taskbarViewController, times(2)).commitRunningAppsToUI()
+    }
+
+    @Test
+    fun onRecentTasksChanged_hotseatAppStartsRunning_commitRunningAppsToUI_isCalled() {
+        setInDesktopMode(true)
+        val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = hotseatPackages,
+            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1),
+            recentTaskPackages = emptyList()
+        )
+        verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+        // Call onRecentTasksChanged() again with a new running app, verify we update UI.
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = hotseatPackages,
+            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, HOTSEAT_PACKAGE_1),
+            recentTaskPackages = emptyList()
+        )
+        verify(taskbarViewController, times(2)).commitRunningAppsToUI()
+    }
+
     private fun prepareHotseatAndRunningAndRecentApps(
         hotseatPackages: List<String>,
         runningTaskPackages: List<String>,
@@ -556,9 +632,11 @@
     }
 
     private fun createTask(packageName: String, isVisible: Boolean = true): Task {
+        // Use the number at the end of the test packageName as the id.
+        val id = packageName[packageName.length - 1].code
         return Task(
                 Task.TaskKey(
-                    nextTaskId++,
+                    id,
                     WINDOWING_MODE_FREEFORM,
                     Intent().apply { `package` = packageName },
                     ComponentName(packageName, "TestActivity"),
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
index 28d1faa..d40d3bc 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
@@ -23,8 +23,6 @@
 import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.WidgetUtils.createWidgetInfo;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -56,7 +54,6 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.rule.ShellCommandRule;
-import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
@@ -143,7 +140,6 @@
     }
 
     @Test
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/310242894
     public void testPendingWidget_withConfigScreen() {
         // A non-restored widget with config screen get bound and shows a 'Click to setup' UI.
         // Do not bind the widget
@@ -193,7 +189,6 @@
     }
 
     @Test
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/310242894
     public void testPendingWidget_notRestored_brokenInstall() {
         // A widget which is was being installed once, even if its not being
         // installed at the moment is not removed.
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
index ae24a57..bc26c00 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
@@ -352,8 +352,11 @@
     }
 
     private void assertPagesExist(Launcher launcher, int... pageIds) {
+        waitForLauncherCondition("Existing page count does NOT match. "
+                + "Expected: " + pageIds.length
+                + ". Actual: " + launcher.getWorkspace().getPageCount(),
+                l -> pageIds.length == l.getWorkspace().getPageCount());
         int pageCount = launcher.getWorkspace().getPageCount();
-        assertEquals("Existing page count does NOT match.", pageIds.length, pageCount);
         for (int i = 0; i < pageCount; i++) {
             CellLayout page = (CellLayout) launcher.getWorkspace().getPageAt(i);
             int pageId = launcher.getWorkspace().getCellLayoutId(page);