Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ecede62c authored by Bartosz Chominski's avatar Bartosz Chominski
Browse files

Handle freeform task move transitions in Shell

Bug: 400407139
Bug: 413579853
Flag: com.android.window.flags.enable_window_repositioning_api
Test: CTS in another CL, WMShellUnitTests:DesktopTasksControllerTest
Change-Id: Id5b871d7147757f04f2ff7dd34cd165cdfad585b
parent 7789e677
Loading
Loading
Loading
Loading
+125 −0
Original line number Diff line number Diff line
@@ -18,8 +18,10 @@

package com.android.wm.shell.desktopmode

import android.app.ActivityManager.RecentTaskInfo
import android.app.ActivityManager.RunningTaskInfo
import android.app.TaskInfo
import android.content.Context
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
import android.content.pm.ActivityInfo.LAUNCH_MULTIPLE
@@ -40,7 +42,10 @@ import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.desktopmode.data.DesktopRepository
import com.android.wm.shell.recents.RecentTasksController
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min

val DESKTOP_MODE_INITIAL_BOUNDS_SCALE: Float =
    SystemProperties.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f
@@ -316,6 +321,126 @@ fun getInheritedExistingTaskBounds(
    }
}

/**
 * Returns new or initial bounds of a desktop task that is being placed based on its current bounds,
 * possible inherited bounds, and bounds maybe requested in transition request.
 */
fun decideDesktopTaskPlacementBounds(
    context: Context,
    recentTasksController: RecentTasksController?,
    taskRepository: DesktopRepository,
    shellTaskOrganizer: ShellTaskOrganizer,
    displayController: DisplayController,
    task: RunningTaskInfo,
    requestedDisplayId: Int,
    deskId: Int,
    requestedTaskBounds: Rect?,
): Rect? {
    // If the caller requested specific bounds, they should take priority.
    if (requestedTaskBounds != null && !requestedTaskBounds.isEmpty) {
        val displayLayout = displayController.getDisplayLayout(requestedDisplayId)
        if (displayLayout == null) {
            return requestedTaskBounds
        }
        val stableBounds = Rect().also { displayLayout.getStableBounds(it) }
        val finalBounds =
            Rect(requestedTaskBounds).apply {
                // 1. Try to fit |requestedTaskBounds| in |stableBounds| without changing size
                offset(max(stableBounds.left - left, 0), 0)
                offset(min(stableBounds.right - right, 0), 0)
                offset(0, max(stableBounds.top - top, 0))
                offset(0, min(stableBounds.bottom - bottom, 0))

                // 2. Ensure that |requestedTaskBounds| fit inside |stableBounds| even if that
                // requires size changes.
                intersect(stableBounds)
            }

        return finalBounds
    }

    // Inherit bounds from closing task instance to prevent application jumping different
    // cascading positions.
    val inheritedTaskBounds =
        getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId)
    if (!taskRepository.isActiveTask(task.taskId) && inheritedTaskBounds != null) {
        return inheritedTaskBounds
    }

    // TODO: b/365723620 - Handle non running tasks that were launched after reboot.
    // If task is already visible, it must have been handled already and added to desktop mode.
    // Cascade task only if it's not visible yet.
    if (
        DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() &&
            !taskRepository.isVisibleTask(task.taskId)
    ) {
        val displayLayout = displayController.getDisplayLayout(requestedDisplayId)
        if (displayLayout != null) {
            val stableBounds = Rect().also { displayLayout.getStableBounds(it) }
            val initialBounds = Rect(task.configuration.windowConfiguration.bounds)
            cascadeWindow(
                context,
                recentTasksController,
                taskRepository,
                shellTaskOrganizer,
                initialBounds,
                displayLayout,
                deskId,
                stableBounds,
            )
            return initialBounds
        }
    }

    // No strategy has overridden bounds provided initially in TaskInfo.
    return null
}

/**
 * Finds the topmost active non-closing task on the given desk, calculates new bounds of this task
 * according to position cascading logic, and writes them to |bounds| provided.
 */
fun cascadeWindow(
    context: Context,
    recentTasksController: RecentTasksController?,
    taskRepository: DesktopRepository,
    shellTaskOrganizer: ShellTaskOrganizer,
    bounds: Rect,
    displayLayout: DisplayLayout,
    deskId: Int,
    stableBounds: Rect = Rect(),
) {
    if (stableBounds.isEmpty) {
        displayLayout.getStableBoundsForDesktopMode(stableBounds)
    }

    val expandedTasks = taskRepository.getExpandedTasksIdsInDeskOrdered(deskId)
    expandedTasks
        .firstOrNull { !taskRepository.isClosingTask(it) }
        ?.let { taskId: Int ->
            val taskInfo =
                shellTaskOrganizer.getRunningTaskInfo(taskId)
                    ?: recentTasksController?.findTaskInBackground(taskId)
            taskInfo?.let {
                val taskBounds = it.configuration.windowConfiguration.bounds
                if (!taskBounds.isEmpty()) {
                    cascadeWindow(context.resources, stableBounds, taskBounds, bounds)
                    return@let
                }
                // RecentsTaskInfo might not have configuration bounds populated yet so use
                // task lastNonFullscreenBounds if available. If null or empty bounds are found
                // do not cascade.
                if (it is RecentTaskInfo) {
                    it.lastNonFullscreenBounds?.let {
                        if (!it.isEmpty()) {
                            cascadeWindow(context.resources, stableBounds, it, bounds)
                        }
                    }
                }
            }
        }
}

/**
 * Returns true if the launch mode will result in a single new task being created for the activity.
 */
+258 −138

File changed.

Preview size limit exceeded, changes collapsed.

+265 −3
Original line number Diff line number Diff line
@@ -124,7 +124,6 @@ import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.Unminim
import com.android.wm.shell.desktopmode.DesktopTasksController.DesktopModeEntryExitTransitionListener
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
import com.android.wm.shell.desktopmode.DesktopTasksController.TaskbarDesktopTaskListener
import com.android.wm.shell.desktopmode.DesktopTestHelpers.DEFAULT_USER_ID
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTask
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createHomeTask
@@ -1292,7 +1291,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
    @EnableFlags(Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES)
    fun addMoveToDeskTaskChanges_newTaskInstance_inheritsClosingInstanceBounds() {
        // Setup existing task.
        val existingTask = setUpFreeformTask(active = true).apply { userId = DEFAULT_USER_ID }
        val existingTask = setUpFreeformTask(active = true)
        val testComponent = ComponentName(/* package */ "test.package", /* class */ "test.class")
        existingTask.topActivity = testComponent
        existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500))
@@ -1300,7 +1299,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
        val launchingTask =
            setUpFullscreenTask().apply {
                topActivityInfo = ActivityInfo().apply { launchMode = LAUNCH_SINGLE_INSTANCE }
                userId = DEFAULT_USER_ID
            }
        launchingTask.topActivity = testComponent

@@ -10854,6 +10852,258 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
        finishWct.assertWithoutHop { hop -> hop.type == HIERARCHY_OP_TYPE_PENDING_INTENT }
    }

    @Test
    fun handleRequest_freeformTaskMove_addsBoundsToWct() {
        val stableBounds =
            Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT - TASKBAR_FRAME_HEIGHT)
        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
            (i.arguments.first() as Rect).set(stableBounds)
        }

        val task = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
        val requestedBounds = Rect(DEFAULT_LANDSCAPE_BOUNDS).apply { inset(10, 20, 30, 40) }

        val wct =
            controller.handleRequest(
                Binder(),
                createTaskMoveTransition(task, task.displayId, requestedBounds),
            )

        assertNotNull(wct, "should handle request")
        val finalBounds = findBoundsChange(wct, task)
        assertThat(requestedBounds).isEqualTo(finalBounds)
    }

    @Test
    fun handleRequest_freeformTaskMove_sameTargetDisplay_noReorder() {
        val stableBounds =
            Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT - TASKBAR_FRAME_HEIGHT)
        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
            (i.arguments.first() as Rect).set(stableBounds)
        }

        val task = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
        val requestedBounds = Rect(DEFAULT_LANDSCAPE_BOUNDS).apply { inset(10, 20, 30, 40) }

        val wct =
            controller.handleRequest(
                Binder(),
                createTaskMoveTransition(task, task.displayId, requestedBounds),
            )

        assertNotNull(wct, "should handle request")
        wct.assertWithoutHop(ReorderPredicate(token = task.token))
    }

    @Test
    fun handleRequest_freeformTaskMove_doesNotAdjustStableBounds() {
        val stableBounds =
            Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT - TASKBAR_FRAME_HEIGHT)
        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
            (i.arguments.first() as Rect).set(stableBounds)
        }

        val task = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)

        val wct =
            controller.handleRequest(
                Binder(),
                createTaskMoveTransition(task, task.displayId, stableBounds),
            )

        assertNotNull(wct, "should handle request")
        val finalBounds = findBoundsChange(wct, task)
        assertThat(finalBounds).isEqualTo(stableBounds)
    }

    @Test
    fun handleRequest_freeformTaskMove_doesNotAdjustBoundsInsideStableBounds() {
        val stableBounds =
            Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT - TASKBAR_FRAME_HEIGHT)
        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
            (i.arguments.first() as Rect).set(stableBounds)
        }

        val task = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
        // For this test make sure that |requestedBounds| are fully contained in |stableBounds|.
        val requestedBounds = Rect(stableBounds).apply { inset(10, 20, 30, 40) }

        val wct =
            controller.handleRequest(
                Binder(),
                createTaskMoveTransition(task, task.displayId, requestedBounds),
            )

        assertNotNull(wct, "should handle request")
        val finalBounds = findBoundsChange(wct, task)
        assertThat(finalBounds).isEqualTo(requestedBounds)
    }

    @Test
    fun handleRequest_freeformTaskMove_adjustsToFitInsideStableBounds() {
        val stableBounds =
            Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT).apply {
                inset(10, 20, 30, 40)
            }
        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
            (i.arguments.first() as Rect).set(stableBounds)
        }

        val task = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
        // For this test make sure that |requestedBounds| fully contain |stableBounds|.
        val requestedBounds = Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT)

        val wct =
            controller.handleRequest(
                Binder(),
                createTaskMoveTransition(task, task.displayId, requestedBounds),
            )

        assertNotNull(wct, "should handle request")
        val finalBounds = findBoundsChange(wct, task)
        assertThat(finalBounds).isEqualTo(stableBounds)
    }

    @Test
    fun handleRequest_freeformTaskMove_adjustsToFitInsideStableBounds_preservesSizeIfPossible() {
        val stableBounds =
            Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT).apply {
                inset(10, 20, 30, 40)
            }
        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
            (i.arguments.first() as Rect).set(stableBounds)
        }

        val task = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
        // For this test make sure that |requestedBounds| is inside display bounds, not fully inside
        // |stableBounds| and no bigger than |stableBounds|.
        val requestedBounds = Rect(0, 0, DISPLAY_DIMENSION_LONG / 2, DISPLAY_DIMENSION_SHORT / 2)

        val wct =
            controller.handleRequest(
                Binder(),
                createTaskMoveTransition(task, task.displayId, requestedBounds),
            )

        assertNotNull(wct, "should handle request")
        val finalBounds = findBoundsChange(wct, task)
        assertThat(finalBounds?.width()).isEqualTo(requestedBounds.width())
        assertThat(finalBounds?.height()).isEqualTo(requestedBounds.height())
    }

    @Test
    fun handleRequest_freeformTaskMove_toDisplayWithoutDesks_reparentsToTda() {
        val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT)
        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
            (i.arguments.first() as Rect).set(stableBounds)
        }
        whenever(rootTaskDisplayAreaOrganizer.displayIds)
            .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))

        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY, bounds = DEFAULT_LANDSCAPE_BOUNDS)
        val requestedBounds = Rect(DEFAULT_LANDSCAPE_BOUNDS).apply { inset(10, 20, 30, 40) }

        val wct =
            controller.handleRequest(
                Binder(),
                createTaskMoveTransition(task, SECOND_DISPLAY, requestedBounds),
            )
        assertNotNull(wct, "should handle request")

        wct.assertHop(
            ReparentPredicate(
                token = task.token,
                parentToken = secondDisplayArea.token,
                toTop = true,
            )
        )
    }

    @Test
    @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
    fun handleRequest_freeformTaskMove_toDisplayWithDesk_multiDesksDisabled() {
        val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT)
        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
            (i.arguments.first() as Rect).set(stableBounds)
        }
        taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
        taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
        whenever(rootTaskDisplayAreaOrganizer.displayIds)
            .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
        desktopState.enterDesktopByDefaultOnFreeformDisplay = true
        rootTaskDisplayAreaOrganizer.setDesktopFirst(SECOND_DISPLAY)

        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY, bounds = DEFAULT_LANDSCAPE_BOUNDS)
        val requestedBounds = Rect(DEFAULT_LANDSCAPE_BOUNDS).apply { inset(10, 20, 30, 40) }

        val wct =
            controller.handleRequest(
                Binder(),
                createTaskMoveTransition(task, SECOND_DISPLAY, requestedBounds),
            )
        assertNotNull(wct, "should handle request")
        val finalBounds = findBoundsChange(wct, task)
        assertThat(finalBounds).isEqualTo(requestedBounds)
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
    fun handleRequest_freeformTaskMove_toDisplayWithDesk_multiDesksEnabled_reparentsToActiveDesk() {
        val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT)
        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
            (i.arguments.first() as Rect).set(stableBounds)
        }
        val targetDeskId = 2
        taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId)
        taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId)
        whenever(rootTaskDisplayAreaOrganizer.displayIds)
            .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
        desktopState.enterDesktopByDefaultOnFreeformDisplay = true
        rootTaskDisplayAreaOrganizer.setDesktopFirst(SECOND_DISPLAY)

        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY, bounds = DEFAULT_LANDSCAPE_BOUNDS)
        val requestedBounds = Rect(DEFAULT_LANDSCAPE_BOUNDS).apply { inset(10, 20, 30, 40) }

        val wct =
            controller.handleRequest(
                Binder(),
                createTaskMoveTransition(task, SECOND_DISPLAY, requestedBounds),
            )
        assertNotNull(wct, "should handle request")
        val finalBounds = findBoundsChange(wct, task)
        assertThat(finalBounds).isEqualTo(requestedBounds)
        verify(desksOrganizer).moveTaskToDesk(any(), eq(targetDeskId), eq(task), eq(false))
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
    fun handleRequest_freeformTaskMove_toDisplayWithDesk_multiDesksEnabled_activatesDeskIfNoActive() {
        val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT)
        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
            (i.arguments.first() as Rect).set(stableBounds)
        }
        val targetDeskId = 2
        taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId)
        taskRepository.setDeskInactive(targetDeskId)
        whenever(rootTaskDisplayAreaOrganizer.displayIds)
            .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
        desktopState.enterDesktopByDefaultOnFreeformDisplay = true
        rootTaskDisplayAreaOrganizer.setDesktopFirst(SECOND_DISPLAY)

        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY, bounds = DEFAULT_LANDSCAPE_BOUNDS)
        val requestedBounds = Rect(DEFAULT_LANDSCAPE_BOUNDS).apply { inset(10, 20, 30, 40) }

        val wct =
            controller.handleRequest(
                Binder(),
                createTaskMoveTransition(task, SECOND_DISPLAY, requestedBounds),
            )
        assertNotNull(wct, "should handle request")
        val finalBounds = findBoundsChange(wct, task)
        assertThat(finalBounds).isEqualTo(requestedBounds)
        verify(desksOrganizer).activateDesk(wct, targetDeskId)
    }

    private class RunOnStartTransitionCallback : ((IBinder) -> Unit) {
        var invocations = 0
            private set
@@ -11249,6 +11499,18 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
        return TransitionRequestInfo(type, task, /* remoteTransition= */ null)
    }

    private fun createTaskMoveTransition(
        task: RunningTaskInfo?,
        requestedDisplayId: Int,
        requestedBounds: Rect,
    ): TransitionRequestInfo {
        return createTransition(task, TRANSIT_CHANGE).apply {
            setRequestedLocation(
                TransitionRequestInfo.RequestedLocation(requestedDisplayId, requestedBounds)
            )
        }
    }

    private companion object {
        const val SECOND_DISPLAY = 2
        const val SECOND_DISPLAY_ON_RECONNECT = 3
+5 −6
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.wm.shell.desktopmode

import android.app.ActivityManager
import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
@@ -34,7 +35,7 @@ object DesktopTestHelpers {
    fun createFreeformTask(
        displayId: Int = DEFAULT_DISPLAY,
        bounds: Rect? = null,
        userId: Int = DEFAULT_USER_ID,
        userId: Int = ActivityManager.getCurrentUser(),
    ): RunningTaskInfo =
        TestRunningTaskInfoBuilder()
            .setDisplayId(displayId)
@@ -64,7 +65,7 @@ object DesktopTestHelpers {
            .setToken(MockToken().token())
            .setActivityType(ACTIVITY_TYPE_STANDARD)
            .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
            .setUserId(DEFAULT_USER_ID)
            .setUserId(ActivityManager.getCurrentUser())
            .setLastActiveTime(100)

    /** Create a task that has windowing mode set to [WINDOWING_MODE_FULLSCREEN] */
@@ -78,13 +79,13 @@ object DesktopTestHelpers {
            .setToken(MockToken().token())
            .setActivityType(ACTIVITY_TYPE_STANDARD)
            .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
            .setUserId(DEFAULT_USER_ID)
            .setUserId(ActivityManager.getCurrentUser())
            .setLastActiveTime(100)
            .build()

    fun createHomeTask(
        displayId: Int = DEFAULT_DISPLAY,
        userId: Int = DEFAULT_USER_ID,
        userId: Int = ActivityManager.getCurrentUser(),
    ): RunningTaskInfo =
        TestRunningTaskInfoBuilder()
            .setDisplayId(displayId)
@@ -111,6 +112,4 @@ object DesktopTestHelpers {
        createSystemModalTask().apply {
            baseActivity = ComponentName("com.test.dummypackage", "TestClass")
        }

    const val DEFAULT_USER_ID = 10
}