diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java index f20d7a551d..8385485253 100644 --- a/quickstep/src/com/android/quickstep/util/AppPairsController.java +++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java @@ -26,6 +26,7 @@ import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; +import static com.android.systemui.shared.recents.utilities.Utilities.isFreeformTask; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE; import static com.android.wm.shell.shared.split.SplitScreenConstants.getIndex; @@ -69,6 +70,7 @@ import com.android.quickstep.views.TaskContainer; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; import java.util.Arrays; @@ -337,10 +339,12 @@ public class AppPairsController { * c) App B is on-screen, but App A isn't. * d) Neither is on-screen. * - * If the user tapped an app pair while inside a single app, there are 3 cases: - * a) The on-screen app is App A of the app pair. - * b) The on-screen app is App B of the app pair. - * c) It is neither. + * If the user tapped an app pair while a fullscreen or freeform app is visible on screen, + * there are 4 cases: + * a) At least one of the apps in the app pair is in freeform windowing mode. + * b) The on-screen app is App A of the app pair. + * c) The on-screen app is App B of the app pair. + * d) It is neither. * * For each case, we call the appropriate animation and split launch type. */ @@ -422,6 +426,14 @@ public class AppPairsController { foundTasks -> { Task foundTask1 = foundTasks[0]; Task foundTask2 = foundTasks[1]; + + if (DesktopModeStatus.canEnterDesktopMode(context) && (isFreeformTask( + foundTask1) || isFreeformTask(foundTask2))) { + launchAppPair(launchingIconView, + CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR); + return; + } + boolean task1IsOnScreen; boolean task2IsOnScreen; if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt index ee70e0a9d3..76d36d3658 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt @@ -16,7 +16,9 @@ package com.android.quickstep.util +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.Context +import android.content.res.Resources import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.launcher3.apppairs.AppPairIcon import com.android.launcher3.logging.StatsLogManager @@ -28,6 +30,7 @@ import com.android.quickstep.TopTaskTracker import com.android.quickstep.TopTaskTracker.CachedTaskInfo import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.Task.TaskKey +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66 import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50 import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33 @@ -54,6 +57,7 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class AppPairsControllerTest { @Mock lateinit var context: Context + @Mock lateinit var resources: Resources @Mock lateinit var splitSelectStateController: SplitSelectStateController @Mock lateinit var statsLogManager: StatsLogManager @@ -109,6 +113,8 @@ class AppPairsControllerTest { doNothing() .whenever(spyAppPairsController) .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + whenever(mockAppPairIcon.context.resources).thenReturn(resources) + whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(false) } @Test @@ -391,6 +397,68 @@ class AppPairsControllerTest { .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT)) } + @Test + fun handleAppPairLaunchInApp_freeformTask1IsOnScreen_shouldLaunchAppPair() { + whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(true) + /// Test launching apps 1 and 2 from app pair + whenever(mockTaskKey1.getId()).thenReturn(1) + whenever(mockTaskKey2.getId()).thenReturn(2) + // Task 1 is in freeform windowing mode + mockTaskKey1.windowingMode = WINDOWING_MODE_FREEFORM + // ... and app 1 is already on screen + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(1))).thenReturn(true) + } else { + whenever(mockCachedTaskInfo.taskId).thenReturn(1) + } + + // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback + spyAppPairsController.handleAppPairLaunchInApp( + mockAppPairIcon, + listOf(mockItemInfo1, mockItemInfo2), + ) + verify(splitSelectStateController) + .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) + val callback: Consumer> = callbackCaptor.value + callback.accept(arrayOf(mockTask1, mockTask2)) + + // Verify that launchAppPair was called + verify(spyAppPairsController, times(1)).launchAppPair(any(), any()) + verify(spyAppPairsController, never()) + .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + } + + @Test + fun handleAppPairLaunchInApp_freeformTask2IsOnScreen_shouldLaunchAppPair() { + whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(true) + /// Test launching apps 1 and 2 from app pair + whenever(mockTaskKey1.getId()).thenReturn(1) + whenever(mockTaskKey2.getId()).thenReturn(2) + // Task 2 is in freeform windowing mode + mockTaskKey1.windowingMode = WINDOWING_MODE_FREEFORM + // ... and app 2 is already on screen + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(2))).thenReturn(true) + } else { + whenever(mockCachedTaskInfo.taskId).thenReturn(2) + } + + // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback + spyAppPairsController.handleAppPairLaunchInApp( + mockAppPairIcon, + listOf(mockItemInfo1, mockItemInfo2), + ) + verify(splitSelectStateController) + .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) + val callback: Consumer> = callbackCaptor.value + callback.accept(arrayOf(mockTask1, mockTask2)) + + // Verify that launchAppPair was called + verify(spyAppPairsController, times(1)).launchAppPair(any(), any()) + verify(spyAppPairsController, never()) + .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + } + @Test fun handleAppPairLaunchInApp_shouldLaunchAppPairNormallyWhenUnrelatedSingleAppIsFullscreen() { // Test launching apps 1 and 2 from app pair