Add unit tests for KQS CD changes

Bug: 382762871
Bug: 382769617
Test: KeyboardQuickSwitchControllerTest and TaskbarOverflowTest
Flag: EXEMPT adding tests
Change-Id: I053af97774230a9bc3d21ff8e1e328344519f728
This commit is contained in:
Ajinkya Chalke
2025-04-16 17:07:05 +00:00
parent c496829044
commit c96b9adf79
3 changed files with 398 additions and 16 deletions

View File

@@ -0,0 +1,391 @@
/*
* Copyright (C) 2025 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.graphics.Rect
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 android.window.RemoteTransition
import androidx.test.core.app.ApplicationProvider
import com.android.launcher3.Flags.FLAG_ENABLE_ALT_TAB_KQS_FLATENNING
import com.android.launcher3.Flags.FLAG_ENABLE_ALT_TAB_KQS_ON_CONNECTED_DISPLAYS
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
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.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.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.SlideInRemoteTransition
import com.android.quickstep.util.SplitTask
import com.android.systemui.shared.recents.model.Task
import com.android.wm.shell.desktopmode.IDesktopTaskListener
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
import com.android.wm.shell.shared.desktopmode.DesktopTaskToFrontReason
import com.android.wm.shell.shared.split.SplitBounds
import com.android.wm.shell.shared.split.SplitScreenConstants
import com.google.common.truth.Truth.assertThat
import dagger.BindsInstance
import dagger.Component
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@RunWith(LauncherMultivalentJUnit::class)
@EmulatedDevices(["pixelTablet2023"])
class KeyboardQuickSwitchControllerTest {
private var systemUiProxySpy: SystemUiProxy? = null
private var desktopTaskListener: IDesktopTaskListener? = null
private val mockRecentsModelHelper: MockedRecentsModelHelper = MockedRecentsModelHelper()
private val desktopVisibilityController: DesktopVisibilityController = mock()
private val taskIdCaptor = ArgumentCaptor.forClass(Int::class.java)
private val transitionCaptor = ArgumentCaptor.forClass(RemoteTransition::class.java)
@get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
@get:Rule(order = 1)
val context =
TaskbarWindowSandboxContext.create(
SandboxParams(
{
spy(SystemUiProxy(ApplicationProvider.getApplicationContext())) { proxy ->
systemUiProxySpy = proxy
doAnswer { desktopTaskListener = it.getArgument(0) }
.whenever(proxy)
.setDesktopTaskListener(anyOrNull())
}
},
builderBase =
DaggerKeyboardQuickSwitchControllerComponent.builder()
.bindRecentsModel(mockRecentsModelHelper.mockRecentsModel)
.bindDesktopVisibilityController(desktopVisibilityController),
)
)
@get:Rule(order = 2) val recentsModel = MockedRecentsModelTestRule(mockRecentsModelHelper)
@get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
@InjectController lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController
private val isKqsShown: Boolean
get() = getOnUiThread { keyboardQuickSwitchController.isShown }
private val shownTaskIds: List<Int>
get() = getOnUiThread { keyboardQuickSwitchController.shownTaskIds() }
@Test
fun noRecentTasks_noShownTaskIds() {
triggerAltTab()
assertThat(isKqsShown).isTrue()
assertThat(shownTaskIds).isEmpty()
}
@Test
fun onlySingleTasksPresent_shouldShowAllTaskIds() {
updateRecentsModel(
listOf(createSingleTask(PREVIOUS_TASK_ID), createSingleTask(RUNNING_TASK_ID))
)
triggerAltTab()
assertThat(isKqsShown).isTrue()
assertThat(shownTaskIds).containsExactly(RUNNING_TASK_ID, PREVIOUS_TASK_ID).inOrder()
}
@Test
fun onlyDesktopTasksPresent_shouldShowAllTaskIds() {
updateRecentsModel(listOf(createDesktopTask(listOf(RUNNING_TASK_ID, PREVIOUS_TASK_ID))))
enableDesktopMode()
triggerAltTab()
assertThat(isKqsShown).isTrue()
assertThat(shownTaskIds).containsExactly(RUNNING_TASK_ID, PREVIOUS_TASK_ID).inOrder()
}
@Test
@DisableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING)
fun singleAndDesktopTasksPresent_notOnDesktopWithFlatenningOff_onlyShowSingleTaskIds() {
updateRecentsModel(
listOf(
createDesktopTask(listOf(PREVIOUS_TASK_ID, OLDEST_TASK_ID)),
createSingleTask(RUNNING_TASK_ID),
)
)
triggerAltTab()
assertThat(isKqsShown).isTrue()
assertThat(shownTaskIds).containsExactly(RUNNING_TASK_ID)
}
@Test
@DisableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING)
fun singleAndDesktopTasksPresent_onDesktopWithFlatenningOff_onlyShowDesktopTaskIds() {
updateRecentsModel(
listOf(
createDesktopTask(listOf(RUNNING_TASK_ID, OLDEST_TASK_ID)),
createSingleTask(PREVIOUS_TASK_ID),
)
)
enableDesktopMode()
triggerAltTab()
assertThat(isKqsShown).isTrue()
assertThat(shownTaskIds).containsExactly(RUNNING_TASK_ID, OLDEST_TASK_ID).inOrder()
}
@Test
@EnableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING)
fun singleAndDesktopTasksPresent_onDesktopWithFlatenningOn_showAllTaskIds() {
updateRecentsModel(
listOf(
createDesktopTask(listOf(RUNNING_TASK_ID, OLDEST_TASK_ID)),
createSingleTask(PREVIOUS_TASK_ID),
)
)
enableDesktopMode()
triggerAltTab()
assertThat(isKqsShown).isTrue()
assertThat(shownTaskIds)
.containsExactly(RUNNING_TASK_ID, PREVIOUS_TASK_ID, OLDEST_TASK_ID)
.inOrder()
}
@Test
@EnableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING)
fun singleAndDesktopTasksPresent_notOnDesktopWithFlatenningOn_showAllTaskIds() {
updateRecentsModel(
listOf(
createDesktopTask(listOf(PREVIOUS_TASK_ID, OLDEST_TASK_ID)),
createSingleTask(RUNNING_TASK_ID),
)
)
triggerAltTab()
assertThat(isKqsShown).isTrue()
assertThat(shownTaskIds)
.containsExactly(RUNNING_TASK_ID, PREVIOUS_TASK_ID, OLDEST_TASK_ID)
.inOrder()
}
@Test
@DisableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING, FLAG_ENABLE_ALT_TAB_KQS_ON_CONNECTED_DISPLAYS)
fun multipleDesktopTasksPresent_onDesktopWithCdFlagOff_onlyShowCurrentDesktopTasks() {
updateRecentsModel(
listOf(
createDesktopTask(listOf(RUNNING_TASK_ID)),
createDesktopTask(listOf(PREVIOUS_TASK_ID)),
)
)
enableDesktopMode()
triggerAltTab()
assertThat(isKqsShown).isTrue()
assertThat(shownTaskIds).containsExactly(RUNNING_TASK_ID)
}
@Test
@DisableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING)
@EnableFlags(FLAG_ENABLE_ALT_TAB_KQS_ON_CONNECTED_DISPLAYS)
fun multipleDesktopTasksPresent_onDesktopWithCdFlagON_showAllDesktopTasks() {
updateRecentsModel(
listOf(
createDesktopTask(listOf(RUNNING_TASK_ID)),
createDesktopTask(listOf(PREVIOUS_TASK_ID)),
)
)
enableDesktopMode()
triggerAltTab()
assertThat(isKqsShown).isTrue()
assertThat(shownTaskIds).containsExactly(RUNNING_TASK_ID, PREVIOUS_TASK_ID).inOrder()
}
@Test
@EnableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING)
fun splitAndSingleTaskPresent_withFlatenningOn_shouldSortTaskIds() {
updateRecentsModel(
listOf(
createSplitTask(OLDEST_TASK_ID to RUNNING_TASK_ID),
createSingleTask(PREVIOUS_TASK_ID),
)
)
triggerAltTab()
// Although single task is more recent than one of the split tasks, the split tasks should
// be together. Furthermore, the shownTaskIds returns left split task first.
assertThat(shownTaskIds)
.containsExactly(OLDEST_TASK_ID, RUNNING_TASK_ID, PREVIOUS_TASK_ID)
.inOrder()
}
@Test
@EnableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING)
fun launchDesktopApp_notOnDesktop_shouldCallSysUIProxyToStartSpecificApp() {
val deskId = 1
updateRecentsModel(listOf(createDesktopTask(listOf(PREVIOUS_TASK_ID), deskId)))
triggerAltTabAndLaunchFocusedTask()
val deskIdCaptor = ArgumentCaptor.forClass(Int::class.java)
verify(systemUiProxySpy)?.activateDesk(deskIdCaptor.capture(), transitionCaptor.capture())
assertThat(deskIdCaptor.value).isEqualTo(deskId)
assertThat(transitionCaptor.value.remoteTransition)
.isInstanceOf(SlideInRemoteTransition::class.java)
verify(systemUiProxySpy)
?.showDesktopApp(taskIdCaptor.capture(), eq(null), eq(DesktopTaskToFrontReason.ALT_TAB))
assertThat(taskIdCaptor.value).isEqualTo(PREVIOUS_TASK_ID)
}
@Test
@EnableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING)
fun launchSingleApp_onDesktop_shouldCallSysUIProxyToMoveToFullscreen() {
updateRecentsModel(listOf(createSingleTask(PREVIOUS_TASK_ID)))
enableDesktopMode()
triggerAltTabAndLaunchFocusedTask()
verify(systemUiProxySpy)
?.moveToFullscreen(
taskIdCaptor.capture(),
eq(DesktopModeTransitionSource.KEYBOARD_SHORTCUT),
transitionCaptor.capture(),
)
assertThat(taskIdCaptor.value).isEqualTo(PREVIOUS_TASK_ID)
assertThat(transitionCaptor.value.remoteTransition)
.isInstanceOf(SlideInRemoteTransition::class.java)
}
private fun createSingleTask(taskId: Int) = SingleTask(createTask(taskId))
private fun createSplitTask(taskIds: Pair<Int, Int>) =
SplitTask(
createTask(taskIds.first),
createTask(taskIds.second),
SplitBounds(
/* leftTopBounds = */ Rect(),
/* rightBottomBounds = */ Rect(),
/* leftTopTaskId = */ -1,
/* rightBottomTaskId = */ -1,
/* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50,
),
)
private fun createDesktopTask(taskIds: List<Int>, deskId: Int = 0) =
DesktopTask(deskId, DEFAULT_DISPLAY, taskIds.map { createTask(it) })
private fun enableDesktopMode() {
whenever(desktopVisibilityController.isInDesktopMode(anyInt())).thenReturn(true)
}
/*
* Returns a task with the given ID and a fake package name.
*
* Note: the task ID is added to last active time, thus higher task ID indicates a more recent
* active task.
*/
private fun createTask(taskId: Int): Task {
return Task(
Task.TaskKey(
taskId,
0,
Intent().apply { `package` = "Fake${taskId}" },
ComponentName("Fake${taskId}", ""),
Process.myUserHandle().identifier,
2000L + taskId,
)
)
}
private fun updateRecentsModel(tasks: List<GroupTask>) {
recentsModel.updateRecentTasks(tasks)
runOnMainSync { recentsModel.resolvePendingTaskRequests() }
}
private fun triggerAltTab() = runOnMainSync {
keyboardQuickSwitchController.openQuickSwitchView()
recentsModel.resolvePendingTaskRequests()
}
private fun triggerAltTabAndLaunchFocusedTask() {
triggerAltTab()
runOnMainSync { keyboardQuickSwitchController.launchFocusedTask() }
}
private companion object {
const val OLDEST_TASK_ID = 1
const val PREVIOUS_TASK_ID = 2
const val RUNNING_TASK_ID = 3
}
}
/** KeyboardQuickSwitchControllerComponent used to bind the RecentsModel. */
@LauncherAppSingleton
@Component(
modules = [AllModulesForTest::class, FakePrefsModule::class, DisplayControllerModule::class]
)
interface KeyboardQuickSwitchControllerComponent : TaskbarSandboxComponent {
@Component.Builder
interface Builder : TaskbarSandboxComponent.Builder {
@BindsInstance fun bindRecentsModel(model: RecentsModel): Builder
@BindsInstance
fun bindDesktopVisibilityController(
desktopVisibilityController: DesktopVisibilityController
): Builder
override fun build(): KeyboardQuickSwitchControllerComponent
}
}

View File

@@ -26,6 +26,7 @@ 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
@@ -354,6 +355,7 @@ class TaskbarOverflowTest {
@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
@@ -413,6 +415,7 @@ class TaskbarOverflowTest {
@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