diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index 8816a6dd00..3c5d71e1e8 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -847,6 +847,13 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar return icons; } + /** + * The max number of icon views the taskbar can have when taskbar overflow is enabled. + */ + int getMaxNumIconViews() { + return mMaxNumIcons; + } + /** * Returns the all apps button in the taskbar. */ diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index bb4f07a72f..bc5f9a3ca9 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -351,6 +351,11 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar OneShotPreDrawListener.add(mTaskbarView, listener); } + @VisibleForTesting + int getMaxNumIconViews() { + return mTaskbarView.getMaxNumIconViews(); + } + public Rect getIconLayoutVisualBounds() { return mTaskbarView.getIconLayoutVisualBounds(); } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt new file mode 100644 index 0000000000..cc8582c957 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.content.ComponentName +import android.content.Intent +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import com.android.launcher3.Flags.FLAG_TASKBAR_OVERFLOW +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.launcher3.taskbar.rules.MockedRecentsModelTestRule +import com.android.launcher3.taskbar.rules.TaskbarModeRule +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.launcher3.util.TestUtil.getOnUiThread +import com.android.quickstep.SystemUiProxy +import com.android.quickstep.util.DesktopTask +import com.android.systemui.shared.recents.model.Task +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS +import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR +import com.android.wm.shell.desktopmode.IDesktopTaskListener +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelTablet2023"]) +@EnableFlags( + FLAG_TASKBAR_OVERFLOW, + FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS, + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_BUBBLE_BAR, +) +class TaskbarOverflowTest { + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + + @get:Rule(order = 1) + val context = + TaskbarWindowSandboxContext.create { builder -> + builder.bindSystemUiProxy( + object : SystemUiProxy(this) { + override fun setDesktopTaskListener(listener: IDesktopTaskListener?) { + desktopTaskListener = listener + } + } + ) + } + + @get:Rule(order = 2) val recentsModel = MockedRecentsModelTestRule(context) + + @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context) + + @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var taskbarViewController: TaskbarViewController + @InjectController lateinit var recentAppsController: TaskbarRecentAppsController + @InjectController lateinit var bubbleBarViewController: BubbleBarViewController + @InjectController lateinit var bubbleStashController: BubbleStashController + + private var desktopTaskListener: IDesktopTaskListener? = null + + @Before + fun ensureRunningAppsShowing() { + runOnMainSync { + if (!recentAppsController.canShowRunningApps) { + recentAppsController.onDestroy() + recentAppsController.canShowRunningApps = true + recentAppsController.init(taskbarUnitTestRule.activityContext.controllers) + } + recentsModel.resolvePendingTaskRequests() + } + } + + @Test + @TaskbarMode(PINNED) + fun testTaskbarWithMaxNumIcons_pinned() { + addRunningAppsAndVerifyOverflowState(0) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testTaskbarWithMaxNumIcons_transient() { + addRunningAppsAndVerifyOverflowState(0) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing) + } + + @Test + @TaskbarMode(PINNED) + fun testOverflownTaskbar_pinned() { + addRunningAppsAndVerifyOverflowState(5) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testOverflownTaskbar_transient() { + addRunningAppsAndVerifyOverflowState(5) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing) + } + + @Test + @TaskbarMode(PINNED) + fun testBubbleBarReducesTaskbarMaxNumIcons_pinned() { + var initialMaxNumIconViews = maxNumberOfTaskbarIcons + assertThat(initialMaxNumIconViews).isGreaterThan(0) + + runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) } + + val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2) + assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews) + + assertThat(taskbarIconsCentered).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testBubbleBarReducesTaskbarMaxNumIcons_transient() { + var initialMaxNumIconViews = maxNumberOfTaskbarIcons + assertThat(initialMaxNumIconViews).isGreaterThan(0) + + runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) } + + val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2) + assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin) + .isAtLeast( + navButtonEndSpacing + + bubbleBarViewController.collapsedWidthWithMaxVisibleBubbles.toInt() + ) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testBubbleBarReducesTaskbarMaxNumIcons_transientBubbleInitiallyStashed() { + var initialMaxNumIconViews = maxNumberOfTaskbarIcons + assertThat(initialMaxNumIconViews).isGreaterThan(0) + runOnMainSync { + bubbleStashController.stashBubbleBarImmediate() + bubbleBarViewController.setHiddenForBubbles(false) + } + + val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2) + assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin) + .isAtLeast( + navButtonEndSpacing + + bubbleBarViewController.collapsedWidthWithMaxVisibleBubbles.toInt() + ) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testStashingBubbleBarMaintainsMaxNumIcons_transient() { + runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) } + + val initialNumIcons = currentNumberOfTaskbarIcons + val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2) + + runOnMainSync { bubbleStashController.stashBubbleBarImmediate() } + assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2)) + } + + @Test + @TaskbarMode(PINNED) + fun testHidingBubbleBarIncreasesMaxNumIcons_pinned() { + runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) } + + val initialNumIcons = currentNumberOfTaskbarIcons + val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5) + + runOnMainSync { bubbleBarViewController.setHiddenForBubbles(true) } + + val maxNumIconViews = maxNumberOfTaskbarIcons + assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews) + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2)) + + assertThat(taskbarIconsCentered).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testHidingBubbleBarIncreasesMaxNumIcons_transient() { + runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) } + + val initialNumIcons = currentNumberOfTaskbarIcons + val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5) + + runOnMainSync { bubbleBarViewController.setHiddenForBubbles(true) } + + val maxNumIconViews = maxNumberOfTaskbarIcons + assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews) + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2)) + + assertThat(taskbarIconsCentered).isTrue() + } + + private fun createDesktopTask(tasksToAdd: Int) { + val tasks = + (0.. 0) initialIconCount else -1) + return maxNumIconViews + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt new file mode 100644 index 0000000000..ed1443d39a --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.rules + +import com.android.quickstep.RecentsModel +import com.android.quickstep.RecentsModel.RecentTasksChangedListener +import com.android.quickstep.TaskIconCache +import com.android.quickstep.util.GroupTask +import java.util.function.Consumer +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock + +class MockedRecentsModelTestRule(private val context: TaskbarWindowSandboxContext) : TestRule { + + private val mockIconCache: TaskIconCache = mock() + + private val mockRecentsModel: RecentsModel = mock { + on { iconCache } doReturn mockIconCache + + on { unregisterRecentTasksChangedListener() } doAnswer { recentTasksChangedListener = null } + + on { registerRecentTasksChangedListener(any()) } doAnswer + { + recentTasksChangedListener = it.getArgument(0) + } + + on { getTasks(anyOrNull(), anyOrNull()) } doAnswer + { + val request = it.getArgument>?>(0) + if (request != null) { + taskRequests.add { response -> request.accept(response) } + } + taskListId + } + + on { getTasks(anyOrNull()) } doAnswer + { + val request = it.getArgument>?>(0) + if (request != null) { + taskRequests.add { response -> request.accept(response) } + } + taskListId + } + + on { isTaskListValid(any()) } doAnswer { taskListId == it.getArgument(0) } + } + + private var recentTasks: List = emptyList() + private var taskListId = 0 + private var recentTasksChangedListener: RecentTasksChangedListener? = null + private var taskRequests: MutableList<(List) -> Unit> = mutableListOf() + + override fun apply(base: Statement?, description: Description?): Statement { + return object : Statement() { + override fun evaluate() { + context.putObject(RecentsModel.INSTANCE, mockRecentsModel) + base?.evaluate() + } + } + } + + // NOTE: For the update to take effect, `resolvePendingTaskRequests()` needs to be called, so + // calbacks to any pending `RecentsModel.getTasks()` get called with the updated task list. + fun updateRecentTasks(tasks: List) { + ++taskListId + recentTasks = tasks + recentTasksChangedListener?.onRecentTasksChanged() + } + + fun resolvePendingTaskRequests() { + val requests = mutableListOf<(List) -> Unit>() + requests.addAll(taskRequests) + taskRequests.clear() + + requests.forEach { it(recentTasks) } + } +}