Files
lawnchair/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
Sunny Goyal aef6c7932e Moving predicted items to ItemIdMap instead of extraItems
> This allows the whole workspace model to be contained in itemIdMap instead of spread across multiple data containers
> Deprecating extraItems, all non-workspace data should use separate repositories
> Design: http://go/launchermodel-re-arch?#heading=h.5y8n4r164d5s

Bug: 390572144
Test: atest PredictionUpdateTaskTest
Flag: EXEMPT refactor
Change-Id: I886d0a8d5bbbf9f5c2054d4a2442bf03f9c4eab5
2025-05-05 15:52:37 -07:00

748 lines
31 KiB
Kotlin

/*
* 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 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.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.statehandlers.DesktopVisibilityController
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.DesktopVisibilityControllerModule
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.IntSparseArrayMap
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.window.flags.Flags.FLAG_ENABLE_TASKBAR_OVERFLOW
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_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS,
FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
FLAG_ENABLE_BUBBLE_BAR,
FLAG_ENABLE_TASKBAR_OVERFLOW,
)
@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 val desktopVisibilityController: DesktopVisibilityController
get() = DesktopVisibilityController.INSTANCE[context]
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() {
whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true)
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<ItemInfo?>),
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..<createdTasks)
tapOverflowIcon()
assertThat(keyboardQuickSwitchController.isShown).isFalse()
}
@Test
@TaskbarMode(PINNED)
fun testHotseatItemTasksNotShownInRecents() {
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<ItemInfo?>),
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<ItemInfo?>),
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..<createdTasks).toList())
}
@Test
@TaskbarMode(PINNED)
@EnableFlags(FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU)
fun pinToTaskbarShortcut_unpinPinnedItem() {
// Create two tasks and two pinned items.
createDesktopTask(2)
val hotseatItems = createHotseatItems(2)
var shortcut: SystemShortcut<*>? = null
var hotseatIcon: BubbleTextView? = null
runOnMainSync {
val taskbarView = setUpTaskbarAndModelCallback(hotseatItems)
hotseatIcon =
taskbarView.iconViews.filterIsInstance<BubbleTextView>().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<BubbleTextView>().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<Int>())
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<BubbleTextView>().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<WorkspaceItemInfo>): 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.bindItemsAdded(hotseatItems.toList())
return taskbarView
}
private fun createDesktopTask(tasksToAdd: Int) {
createDesktopTaskWithTasksFromPackages((0..<tasksToAdd).map { "fake" })
}
private fun createDesktopTaskWithTasksFromPackages(packages: List<String>) {
val tasks =
packages.mapIndexed({ index, p ->
Task(
Task.TaskKey(
index,
0,
Intent().apply { `package` = p },
ComponentName(p, ""),
Process.myUserHandle().identifier,
2000,
)
)
})
val displayId = context.virtualDisplay.display.displayId
recentsModel.updateRecentTasks(listOf(DesktopTask(deskId = 0, displayId, tasks)))
for (task in 1..tasks.size) {
desktopTaskListener?.onTasksVisibilityChanged(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<Int>
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<WorkspaceItemInfo>()
var recentAppsController: TaskbarRecentAppsController? = null
override fun bindCompleteModel(
itemIdMap: IntSparseArrayMap<ItemInfo>,
isBindingSync: Boolean,
) = bindItemsAdded(itemIdMap.toList())
override fun bindItemsAdded(items: List<ItemInfo>) {
runOnMainSync {
items
.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<ItemInfo?>) {
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,
DesktopVisibilityControllerModule::class,
]
)
interface TaskbarOverflowComponent : TaskbarSandboxComponent {
@Component.Builder
interface Builder : TaskbarSandboxComponent.Builder {
@BindsInstance fun bindRecentsModel(model: RecentsModel): Builder
override fun build(): TaskbarOverflowComponent
}
}