/* * 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.animation.AnimatorTestRule import android.content.ComponentName import android.content.Intent import android.os.Process 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.core.app.ApplicationProvider import com.android.launcher3.BubbleTextView import com.android.launcher3.Flags.FLAG_ENABLE_ALT_TAB_KQS_FLATENNING import com.android.launcher3.Flags.FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR import com.android.launcher3.Flags.FLAG_TASKBAR_OVERFLOW import com.android.launcher3.R import com.android.launcher3.dagger.LauncherAppSingleton import com.android.launcher3.dagger.LauncherComponentProvider.appComponent import com.android.launcher3.model.BgDataModel import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.model.data.TaskItemInfo import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.popup.SystemShortcut import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems import com.android.launcher3.taskbar.bubbles.BubbleBarViewController import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController import com.android.launcher3.taskbar.rules.DisplayControllerModule import com.android.launcher3.taskbar.rules.MockedRecentsModelHelper import com.android.launcher3.taskbar.rules.MockedRecentsModelTestRule import com.android.launcher3.taskbar.rules.SandboxParams 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.TaskbarSandboxComponent 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.AllModulesForTest import com.android.launcher3.util.FakePrefsModule import com.android.launcher3.util.LauncherMultivalentJUnit import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices import com.android.launcher3.util.Preconditions.assertNotNull import com.android.launcher3.util.TestUtil.getOnUiThread import com.android.quickstep.RecentsModel import com.android.quickstep.SystemUiProxy import com.android.quickstep.util.DesktopTask import com.android.quickstep.util.GroupTask import com.android.quickstep.util.SingleTask import com.android.quickstep.util.SingleTask.Companion.createTaskItemInfo 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.window.flags.Flags.FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU 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 dagger.BindsInstance import dagger.Component import java.util.function.Predicate import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doAnswer import org.mockito.kotlin.spy import org.mockito.kotlin.whenever @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, ) @DisableFlags(FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR) class TaskbarOverflowTest { @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() val mockRecentsModelHelper: MockedRecentsModelHelper = MockedRecentsModelHelper() @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create( SandboxParams( { spy(SystemUiProxy(ApplicationProvider.getApplicationContext())) { proxy -> doAnswer { desktopTaskListener = it.getArgument(0) } .whenever(proxy) .setDesktopTaskListener(anyOrNull()) } }, DaggerTaskbarOverflowComponent.builder() .bindRecentsModel(mockRecentsModelHelper.mockRecentsModel), ) ) @get:Rule(order = 2) val recentsModel = MockedRecentsModelTestRule(mockRecentsModelHelper) @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context) @get:Rule(order = 4) val animatorTestRule = AnimatorTestRule(this) @get:Rule(order = 5) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context, this::onControllersInitialized) @InjectController lateinit var taskbarViewController: TaskbarViewController @InjectController lateinit var recentAppsController: TaskbarRecentAppsController @InjectController lateinit var bubbleBarViewController: BubbleBarViewController @InjectController lateinit var bubbleStashController: BubbleStashController @InjectController lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController private var desktopTaskListener: IDesktopTaskListener? = null private val modelCallback = ModelCallbacks() private val taskbarContext: TaskbarActivityContext get() = taskbarUnitTestRule.activityContext private var currentControllerInitCallback: () -> Unit = {} set(value) { runOnMainSync { value.invoke() } field = value } private fun onControllersInitialized() { runOnMainSync { if (!recentAppsController.canShowRunningApps) { recentAppsController.onDestroy() recentAppsController.canShowRunningApps = true recentAppsController.init( taskbarUnitTestRule.activityContext.controllers, emptyList(), ) } currentControllerInitCallback.invoke() } } @Before fun ensureRunningAppsShowing() { runOnMainSync { 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 testOverflownTaskbarWithNoSpaceForRecentApps_pinned() { val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) // Create two "recent" desktop tasks, and then add enough hotseat items so the taskbar // reaches max number of items with hotseat item icons, all apps and divider icons only. // I.e. so all desktop tasks are in taskbar overflow. createDesktopTask(2) runOnMainSync { val taskbarView: TaskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) taskbarView.updateItems( createHotseatItems(maxNumberOfTaskbarIcons - initialIconCount), recentAppsController.shownTasks, ) } // Verify that taskbar overflow view is shown (eventhough it exceeds max taskbar icons). assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumberOfTaskbarIcons + 1) assertThat(taskbarOverflowIconIndex).isEqualTo(maxNumberOfTaskbarIcons) assertThat(overflowItems).containsExactlyElementsIn(0..1) } @Test @TaskbarMode(PINNED) fun testOverflownTaskbarWithNoSpaceForRecentApps_singleRecent_pinned() { val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) // Create a "recent" desktop task, and then add enough hotseat items so the taskbar // reaches max number of items with hotseat item icons, all apps and divider icons only. // I.e. so the single desktop tasks is in taskbar overflow. createDesktopTask(1) runOnMainSync { val taskbarView: TaskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) val hotseatItems = createHotseatItems(maxNumberOfTaskbarIcons - initialIconCount) taskbarView.updateItems( recentAppsController.updateHotseatItemInfos(hotseatItems as Array), recentAppsController.shownTasks, ) } // Verify that recent task is shown (eventhough it exceeds max taskbar icons), and that // the taskbar overflow view is not added for the single recent app. assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumberOfTaskbarIcons + 1) assertThat(taskbarOverflowIconIndex).isEqualTo(-1) } @Test @TaskbarMode(PINNED) fun testBubbleBarReducesTaskbarMaxNumIcons_pinned() { var initialMaxNumIconViews = maxNumberOfTaskbarIcons assertThat(initialMaxNumIconViews).isGreaterThan(0) currentControllerInitCallback = { 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) currentControllerInitCallback = { 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) currentControllerInitCallback = { 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() { currentControllerInitCallback = { 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() { currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) } val initialNumIcons = currentNumberOfTaskbarIcons val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5) currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(true) } runOnMainSync { animatorTestRule.advanceTimeBy(150) } 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() { currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) } val initialNumIcons = currentNumberOfTaskbarIcons val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5) currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(true) } runOnMainSync { animatorTestRule.advanceTimeBy(150) } val maxNumIconViews = maxNumberOfTaskbarIcons assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews) assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2)) assertThat(taskbarIconsCentered).isTrue() } @Test @TaskbarMode(PINNED) @DisableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING) fun testPressingOverflowButtonOpensKeyboardQuickSwitch() { val maxNumIconViews = maxNumberOfTaskbarIcons // Assume there are at least all apps and divider icon, as they would appear once running // apps are added, even if not present initially. val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) val targetOverflowSize = 5 val createdTasks = maxNumIconViews - initialIconCount + targetOverflowSize createDesktopTask(createdTasks) assertThat(taskbarOverflowIconIndex).isEqualTo(initialIconCount) tapOverflowIcon() // Keyboard quick switch view is shown only after list of recent task is asynchronously // retrieved from the recents model. runOnMainSync { recentsModel.resolvePendingTaskRequests() } assertThat(getOnUiThread { keyboardQuickSwitchController.isShownFromTaskbar }).isTrue() assertThat(getOnUiThread { keyboardQuickSwitchController.shownTaskIds() }) .containsExactlyElementsIn(0..), recentAppsController.shownTasks, ) } assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) assertThat(taskbarOverflowIconIndex).isEqualTo(initialIconCount + hotseatItems.size) assertThat(overflowItems) .containsExactlyElementsIn(listOf(0) + (2..targetOverflowSize + 1).toList()) } @Test @TaskbarMode(PINNED) @DisableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING) fun testHotseatItemTasksNotShownInKQS() { val maxNumIconViews = maxNumberOfTaskbarIcons // Assume there are at least all apps and divider icon, as they would appear once running // apps are added, even if not present initially. val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) val hotseatItems = createHotseatItems(1) val targetOverflowSize = 5 val createdTasks = maxNumIconViews - initialIconCount + targetOverflowSize createDesktopTaskWithTasksFromPackages( listOf("fake") + listOf(hotseatItems[0]?.targetPackage ?: "") + List(createdTasks - 2) { "fake" } ) runOnMainSync { val taskbarView: TaskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) taskbarView.updateItems( recentAppsController.updateHotseatItemInfos(hotseatItems as Array), recentAppsController.shownTasks, ) } tapOverflowIcon() // Keyboard quick switch view is shown only after list of recent task is asynchronously // retrieved from the recents model. runOnMainSync { recentsModel.resolvePendingTaskRequests() } assertThat(getOnUiThread { keyboardQuickSwitchController.isShownFromTaskbar }).isTrue() assertThat(getOnUiThread { keyboardQuickSwitchController.shownTaskIds() }) .containsExactlyElementsIn(listOf(0) + (2..? = null var hotseatIcon: BubbleTextView? = null runOnMainSync { val taskbarView = setUpTaskbarAndModelCallback(hotseatItems) hotseatIcon = taskbarView.iconViews.filterIsInstance().first { it.tag is WorkspaceItemInfo } shortcut = taskbarContext.controllers.taskbarPopupController.createPinShortcut( taskbarContext, hotseatIcon!!.tag as ItemInfo, hotseatIcon, ) as SystemShortcut<*> } assertNotNull(shortcut) runOnMainSync { shortcut?.onClick(hotseatIcon) } // After unpinning the first item, only the second app is left. assertThat(modelCallback.hotseatItems.map { info -> info.title }) .isEqualTo(listOf("Test App 1")) // The unpinned app doesn't have a task so the shown tasks won't change. assertThat(recentAppsController.shownTasks.map { it.tasks[0].key.id }) .isEqualTo(listOf(0, 1)) } @Test @TaskbarMode(PINNED) @EnableFlags(FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU) fun pinToTaskbarShortcut_unpinPinnedItemWithTask() { // Create two hotseat items with a task for both of them respectively. var hotseatItems = createHotseatItems(2).mapIndexed { idx, item -> TaskItemInfo(idx, item) }.toTypedArray() createDesktopTaskWithTasksFromPackages(hotseatItems.mapNotNull { it.targetPackage }) var shortcut: SystemShortcut<*>? = null var hotseatIcon: BubbleTextView? = null runOnMainSync { val taskbarView = setUpTaskbarAndModelCallback(hotseatItems.map { it }.toTypedArray()) hotseatIcon = taskbarView.iconViews.filterIsInstance().first { it.tag is WorkspaceItemInfo } shortcut = taskbarContext.controllers.taskbarPopupController.createPinShortcut( taskbarContext, hotseatIcon!!.tag as ItemInfo, hotseatIcon, ) as SystemShortcut<*> } // Before unpinning the app, both of the apps should be pinned and no shown task available. assertThat(modelCallback.hotseatItems.map { info -> info.title }) .isEqualTo(listOf("Test App 0", "Test App 1")) assertThat(recentAppsController.shownTasks.map { it.tasks[0].key.id }) .isEqualTo(emptyList()) assertNotNull(shortcut) runOnMainSync { shortcut?.onClick(hotseatIcon) } // After unpinning the app, app 0 is removed and its task is shown as a recent task. assertThat(modelCallback.hotseatItems.map { info -> info.title }) .isEqualTo(listOf("Test App 1")) assertThat(recentAppsController.shownTasks.map { it.tasks[0].key.id }).isEqualTo(listOf(0)) } @Test @TaskbarMode(PINNED) @EnableFlags(FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU) fun pinToTaskbarShortcut_pinRecentTask() { // Create two tasks and two pinned items. createDesktopTask(2) val hotseatItems = createHotseatItems(2) var shortcut: SystemShortcut<*>? = null var recentTaskIcon: BubbleTextView? = null runOnMainSync { val taskbarView = setUpTaskbarAndModelCallback(hotseatItems) // Get the first recent task icon recentTaskIcon = taskbarView.iconViews.filterIsInstance().first { it.tag is GroupTask } val recentTaskInfo = createTaskItemInfo( recentTaskIcon!!.tag as SingleTask, WorkspaceItemInfo().apply { title = "Test App 2" intent = Intent().apply { `package` = "fake" } }, ) shortcut = taskbarContext.controllers.taskbarPopupController.createPinShortcut( taskbarContext, recentTaskInfo, recentTaskIcon, ) as SystemShortcut<*> } assertNotNull(shortcut) runOnMainSync { shortcut?.onClick(recentTaskIcon) } // After pinning the recent task, it should be included in the hotseat items. assertThat(modelCallback.hotseatItems.map { info -> info.title }) .isEqualTo(listOf("Test App 0", "Test App 1", "Test App 2")) // As the task is pinned, the shown tasks should remove it from the list assertThat(recentAppsController.shownTasks.map { it.tasks[0].key.id }).isEqualTo(listOf(1)) } private fun setUpTaskbarAndModelCallback(hotseatItems: Array): TaskbarView { val taskbarView: TaskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) taskbarView.updateItems(hotseatItems, recentAppsController.shownTasks) modelCallback.recentAppsController = recentAppsController context.baseContext.appComponent.launcherAppState.model.addCallbacksAndLoad(modelCallback) modelCallback.bindItems(hotseatItems.toList(), false) return taskbarView } private fun createDesktopTask(tasksToAdd: Int) { createDesktopTaskWithTasksFromPackages((0..) { val tasks = packages.mapIndexed({ index, p -> Task( Task.TaskKey( index, 0, Intent().apply { `package` = p }, ComponentName(p, ""), Process.myUserHandle().identifier, 2000, ) ) }) recentsModel.updateRecentTasks(listOf(DesktopTask(deskId = 0, DEFAULT_DISPLAY, tasks))) for (task in 1..tasks.size) { desktopTaskListener?.onTasksVisibilityChanged( context.virtualDisplay.display.displayId, task, ) } runOnMainSync { recentsModel.resolvePendingTaskRequests() } } private val navButtonEndSpacing: Int get() { return taskbarUnitTestRule.activityContext.resources.getDimensionPixelSize( taskbarUnitTestRule.activityContext.deviceProfile.inv.inlineNavButtonsEndSpacing ) } private val taskbarOverflowIconIndex: Int get() { return getOnUiThread { taskbarViewController.iconViews.indexOfFirst { it is TaskbarOverflowView } } } private val maxNumberOfTaskbarIcons: Int get() = getOnUiThread { taskbarViewController.maxNumIconViews } private val currentNumberOfTaskbarIcons: Int get() = getOnUiThread { taskbarViewController.iconViews.size } private val taskbarIconsCentered: Boolean get() { return getOnUiThread { val iconLayoutBounds = taskbarViewController.transientTaskbarIconLayoutBoundsInParent val availableWidth = taskbarUnitTestRule.activityContext.deviceProfile.widthPx iconLayoutBounds.left - (availableWidth - iconLayoutBounds.right) < 2 } } private val taskbarEndMargin: Int get() { return getOnUiThread { taskbarUnitTestRule.activityContext.deviceProfile.widthPx - taskbarViewController.transientTaskbarIconLayoutBoundsInParent.right } } private val overflowItems: List get() { return getOnUiThread { val overflowIcon = taskbarViewController.iconViews.firstOrNull { it is TaskbarOverflowView } if (overflowIcon is TaskbarOverflowView) { overflowIcon.itemIds } else { emptyList() } } } private fun tapOverflowIcon() { runOnMainSync { val overflowIcon = taskbarViewController.iconViews.firstOrNull { it is TaskbarOverflowView } assertThat(overflowIcon?.callOnClick()).isTrue() } } /** * Adds enough running apps for taskbar to enter overflow of `targetOverflowSize`, and verifies * * max number of icons in the taskbar remains unchanged * * number of icons in the taskbar is at most max number of icons * * whether the taskbar overflow icon is shown, and its position in taskbar. * * Returns max number of icons. */ private fun addRunningAppsAndVerifyOverflowState(targetOverflowSize: Int): Int { val maxNumIconViews = maxNumberOfTaskbarIcons assertThat(maxNumIconViews).isGreaterThan(0) // Assume there are at least all apps and divider icon, as they would appear once running // apps are added, even if not present initially. val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) assertThat(initialIconCount).isLessThan(maxNumIconViews) createDesktopTask(maxNumIconViews - initialIconCount + targetOverflowSize) assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) assertThat(taskbarOverflowIconIndex) .isEqualTo(if (targetOverflowSize > 0) initialIconCount else -1) if (targetOverflowSize > 0) { assertThat(overflowItems).containsExactlyElementsIn(0..targetOverflowSize) } return maxNumIconViews } private class ModelCallbacks : BgDataModel.Callbacks { var hotseatItems = mutableListOf() var recentAppsController: TaskbarRecentAppsController? = null override fun bindItems(shortcuts: List, forceAnimateIcons: Boolean) { runOnMainSync { shortcuts .filter { item -> item is WorkspaceItemInfo && !hotseatItems.any { it.targetPackage == item.targetPackage } } .forEach { item -> hotseatItems.add(item as WorkspaceItemInfo) } recentAppsController?.updateHotseatItemInfos(hotseatItems.toTypedArray()) } } override fun bindWorkspaceComponentsRemoved(matcher: Predicate) { runOnMainSync { for (i in hotseatItems.size - 1 downTo 0) { if (matcher.test(hotseatItems[i])) { hotseatItems.removeAt(i) } } recentAppsController?.updateHotseatItemInfos(hotseatItems.toTypedArray()) } } } } /** TaskbarOverflowComponent used to bind the RecentsModel. */ @LauncherAppSingleton @Component( modules = [AllModulesForTest::class, FakePrefsModule::class, DisplayControllerModule::class] ) interface TaskbarOverflowComponent : TaskbarSandboxComponent { @Component.Builder interface Builder : TaskbarSandboxComponent.Builder { @BindsInstance fun bindRecentsModel(model: RecentsModel): Builder override fun build(): TaskbarOverflowComponent } }