diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt index 541cec7728..bc3dc53aa0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt @@ -16,6 +16,8 @@ package com.android.launcher3.taskbar import android.content.Context +import android.util.Log +import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags import androidx.annotation.VisibleForTesting import com.android.launcher3.BubbleTextView.RunningAppState @@ -57,6 +59,9 @@ class TaskbarRecentAppsController( } } + val enableRecentTasksThrottle = + DesktopExperienceFlags.ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX.isTrue + // TODO(b/343532825): Add a setting to disable Recents even when the flag is on. var canShowRecentApps = enableRecentsInTaskbar() @VisibleForTesting @@ -175,6 +180,11 @@ class TaskbarRecentAppsController( // tasks again if we have already requested it and the task list has not changed private var taskListChangeId = -1 + // Whether we're currently loading recents tasks + private var loadingRecentsTasks = false + // Whether we need to reload recents tasks when the current loading operation is finished + private var needsRecentsTasksReload = false + // Whether we've loaded recents tasks at least once private var recentTasksLoaded = false fun init(taskbarControllers: TaskbarControllers, previousShownTasks: List) { @@ -240,24 +250,34 @@ class TaskbarRecentAppsController( } private fun reloadRecentTasksIfNeeded() { - if (!recentsModel.isTaskListValid(taskListChangeId)) { - taskListChangeId = - recentsModel.getTasks(RecentsFilterState.EMPTY_FILTER) { tasks -> - recentTasksLoaded = true - allRecentTasks = tasks - val oldRunningTaskdIds = runningTaskIds - val oldMinimizedTaskIds = minimizedTaskIds - desktopTasks = - allRecentTasks.filterIsInstance().flatMap { it.tasks } - val runningTasksChanged = oldRunningTaskdIds != runningTaskIds - val minimizedTasksChanged = oldMinimizedTaskIds != minimizedTaskIds - if ( - onRecentsOrHotseatChanged() || runningTasksChanged || minimizedTasksChanged - ) { - controllers.taskbarViewController.commitRunningAppsToUI() - } - } + if (recentsModel.isTaskListValid(taskListChangeId)) return + if (enableRecentTasksThrottle && loadingRecentsTasks) { + Log.v(TAG, "reloadRecentTasksIfNeeded: tried to reload while loading recents tasks") + needsRecentsTasksReload = true + return } + Log.v(TAG, "reloadRecentTasksIfNeeded: load recents tasks") + // Only indicate that recents tasks are loading if the enableRecentTasksThrottle flag is on + loadingRecentsTasks = enableRecentTasksThrottle + taskListChangeId = + recentsModel.getTasks(RecentsFilterState.EMPTY_FILTER) { tasks -> + loadingRecentsTasks = false + recentTasksLoaded = true + allRecentTasks = tasks + val oldRunningTaskdIds = runningTaskIds + val oldMinimizedTaskIds = minimizedTaskIds + desktopTasks = allRecentTasks.filterIsInstance().flatMap { it.tasks } + val runningTasksChanged = oldRunningTaskdIds != runningTaskIds + val minimizedTasksChanged = oldMinimizedTaskIds != minimizedTaskIds + if (onRecentsOrHotseatChanged() || runningTasksChanged || minimizedTasksChanged) { + controllers.taskbarViewController.commitRunningAppsToUI() + } + if (needsRecentsTasksReload) { + Log.v(TAG, "reloadRecentTasksIfNeeded: reload recents tasks") + needsRecentsTasksReload = false + reloadRecentTasksIfNeeded() + } + } } /** @@ -462,6 +482,8 @@ class TaskbarRecentAppsController( get() = tasks.map { task -> task.key.packageName } private companion object { + private val TAG = "TaskbarRecentAppsController" + const val MAX_RECENT_TASKS = 2 } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt index 329d977fc7..da9cacedeb 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt @@ -24,7 +24,9 @@ import android.content.res.Resources import android.graphics.Rect import android.os.Process import android.os.UserHandle +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.view.Display.DEFAULT_DISPLAY import androidx.test.annotation.UiThreadTest import com.android.internal.R @@ -60,6 +62,7 @@ import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.never import org.mockito.kotlin.times @@ -72,6 +75,7 @@ import org.mockito.kotlin.whenever class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { @get:Rule val mockitoRule = MockitoJUnit.rule() + @get:Rule val setFlagsRule = SetFlagsRule() @get:Rule val disableControllerForCertainTestsWatcher = object : TestWatcher() { @@ -118,6 +122,16 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { recentAppsController = TaskbarRecentAppsController(mockContext, mockRecentsModel) recentAppsController.canShowRunningApps = canShowRunningAndRecentAppsAtInit recentAppsController.canShowRecentApps = canShowRunningAndRecentAppsAtInit + + // To ensure the initial getTasks() call is not seen as "loading" for the rest of the test, + // execute its callback. + doAnswer { + val callback: Consumer> = it.getArgument(1) + callback.accept(arrayListOf()) + taskListChangeId + } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) recentAppsController.init(taskbarControllers, emptyList()) taskbarControllers.onPostInit() @@ -148,7 +162,7 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { runningTasks = listOf(createTask(1, RUNNING_APP_PACKAGE_1)), recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), ) - verify(mockRecentsModel, never()).getTasks(any>>()) + verify(mockRecentsModel, never()).getTasks(any(), any>>()) } @Test @@ -166,6 +180,82 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { verify(mockRecentsModel, times(1)).getTasks(any(), any>>()) } + @Test + @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX) + fun recentTasksChanged_duringGetTasksLoading_dontCallGetTasks() { + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(any(), any>>()) + // Override the mock answer for getTasks() so it doesn't call the callback immediately. + doAnswer { taskListChangeId } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) + recentTasksChangedListener?.onRecentTasksChanged() + // By not invoking the callback passed to getTasks() we here emulate getTasks() loading. + + recentTasksChangedListener?.onRecentTasksChanged() + + // getTasks() is only called two times overall (init + once more). + verify(mockRecentsModel, times(2)).getTasks(any(), any>>()) + } + + @Test + @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX) + fun recentTasksChanged_duringGetTasksLoading_getTasksCalledWhenLoadingDone() { + val callbackCaptor = argumentCaptor>>() + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(any(), callbackCaptor.capture()) + // Override the mock answer for getTasks() so it doesn't call the callback immediately. + doAnswer { taskListChangeId } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) + recentTasksChangedListener?.onRecentTasksChanged() + // By not invoking the callback passed to getTasks() we here emulate getTasks() loading. + + recentTasksChangedListener?.onRecentTasksChanged() + callbackCaptor.lastValue.accept(emptyList()) + + // getTasks() is called again now that the first getTasks() call finished. + verify(mockRecentsModel, times(3)).getTasks(any(), any>>()) + } + + @Test + @DisableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX) + fun recentTasksChanged_duringGetTasksLoading_flagDisabled_callGetTasks() { + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(any(), any>>()) + // Override the mock answer for getTasks() so it doesn't call the callback immediately. + doAnswer { taskListChangeId } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) + recentTasksChangedListener?.onRecentTasksChanged() + // By not invoking the callback passed to getTasks() we here emulate getTasks() loading. + + recentTasksChangedListener?.onRecentTasksChanged() + + // getTasks() is called once per onRecentTasksChanged() invocation (and once at init) + verify(mockRecentsModel, times(3)).getTasks(any(), any>>()) + } + + @Test + @DisableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX) + fun recentTasksChanged_duringGetTasksLoading_flagDisabled_getTasksNotCalledWhenLoadingDone() { + val callbackCaptor = argumentCaptor>>() + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(any(), callbackCaptor.capture()) + // Override the mock answer for getTasks() so it doesn't call the callback immediately. + doAnswer { taskListChangeId } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) + recentTasksChangedListener?.onRecentTasksChanged() + recentTasksChangedListener?.onRecentTasksChanged() + verify(mockRecentsModel, times(3)).getTasks(any(), any>>()) + + callbackCaptor.lastValue.accept(emptyList()) + + // getTasks() is called once per onRecentTasksChanged() invocation (and once at init) + verify(mockRecentsModel, times(3)).getTasks(any(), any>>()) + } + @Test fun getDesktopItemState_nullItemInfo_returnsNotRunning() { setInDesktopMode(true)