From c96b9adf79f497f1ed28e42d8bdff60bba2ecc5e Mon Sep 17 00:00:00 2001 From: Ajinkya Chalke Date: Wed, 16 Apr 2025 17:07:05 +0000 Subject: [PATCH] Add unit tests for KQS CD changes Bug: 382762871 Bug: 382769617 Test: KeyboardQuickSwitchControllerTest and TaskbarOverflowTest Flag: EXEMPT adding tests Change-Id: I053af97774230a9bc3d21ff8e1e328344519f728 --- .../KeyboardQuickSwitchViewController.java | 20 +- .../KeyboardQuickSwitchControllerTest.kt | 391 ++++++++++++++++++ .../launcher3/taskbar/TaskbarOverflowTest.kt | 3 + 3 files changed, 398 insertions(+), 16 deletions(-) create mode 100644 quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/KeyboardQuickSwitchControllerTest.kt diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java index 47791b79fb..fdd0887a5a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java @@ -313,11 +313,11 @@ public class KeyboardQuickSwitchViewController { // All DesktopTasks, irrespective of whether desktop mode is active, are launched here as // the class DesktopTask is used in a special way by KQS view for showing thumbnails of // freeform tasks. - if (task instanceof DesktopTask) { + if (task instanceof DesktopTask desktopTask) { boolean canUnminimizeDesktopTask = context.canUnminimizeDesktopTask(taskId); - runOnUiWithJankMonitoring(() -> { + UI_HELPER_EXECUTOR.execute(() -> { if (!mOnDesktop) { - systemUiProxy.showDesktopApps(context.getDisplayId(), slideInTransition); + systemUiProxy.activateDesk(desktopTask.getDeskId(), slideInTransition); } systemUiProxy.showDesktopApp(taskId, @@ -327,7 +327,7 @@ public class KeyboardQuickSwitchViewController { return true; } else if (mOnDesktop && task instanceof SingleTask) { // Use the special API if user wants to switch to a fullscreen app while in desktop. - runOnUiWithJankMonitoring( + UI_HELPER_EXECUTOR.execute( () -> systemUiProxy.moveToFullscreen(taskId, DesktopModeTransitionSource.KEYBOARD_SHORTCUT, slideInTransition)); return true; @@ -337,18 +337,6 @@ public class KeyboardQuickSwitchViewController { return false; } - private void runOnUiWithJankMonitoring(Runnable runnable) { - Runnable onStartCallback = () -> InteractionJankMonitorWrapper.begin( - mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH); - Runnable onFinishCallback = () -> InteractionJankMonitorWrapper.end( - Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH); - UI_HELPER_EXECUTOR.execute(() -> { - onStartCallback.run(); - runnable.run(); - onFinishCallback.run(); - }); - } - private RemoteTransition getUnminimizeTransition() { return new RemoteTransition( new DesktopAppLaunchTransition( diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/KeyboardQuickSwitchControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/KeyboardQuickSwitchControllerTest.kt new file mode 100644 index 0000000000..75dfbdaa08 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/KeyboardQuickSwitchControllerTest.kt @@ -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 + 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) = + 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, 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) { + 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 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt index 9966bef7b0..61d7c77f2f 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt @@ -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