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

Commit 8356ba21 authored by Jorge Gil's avatar Jorge Gil Committed by Android (Google) Code Review
Browse files

Merge "Exit desktop immersive if applicable on other transitions" into main

parents 4100e22e 0a7aa275
Loading
Loading
Loading
Loading
+8 −2
Original line number Diff line number Diff line
@@ -695,10 +695,16 @@ public abstract class WMShellModule {
    static Optional<DesktopFullImmersiveTransitionHandler> provideDesktopImmersiveHandler(
            Context context,
            Transitions transitions,
            @DynamicOverride DesktopRepository desktopRepository) {
            @DynamicOverride DesktopRepository desktopRepository,
            DisplayController displayController,
            ShellTaskOrganizer shellTaskOrganizer) {
        if (DesktopModeStatus.canEnterDesktopMode(context)) {
            return Optional.of(
                    new DesktopFullImmersiveTransitionHandler(transitions, desktopRepository));
                    new DesktopFullImmersiveTransitionHandler(
                            transitions,
                            desktopRepository,
                            displayController,
                            shellTaskOrganizer));
        }
        return Optional.empty();
    }
+151 −20
Original line number Diff line number Diff line
@@ -27,8 +27,12 @@ import android.window.TransitionInfo
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import androidx.core.animation.addListener
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.protolog.ShellProtoLogGroup
import com.android.window.flags.Flags
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TransitionHandler
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
@@ -41,16 +45,29 @@ import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
class DesktopFullImmersiveTransitionHandler(
    private val transitions: Transitions,
    private val desktopRepository: DesktopRepository,
    private val displayController: DisplayController,
    private val shellTaskOrganizer: ShellTaskOrganizer,
    private val transactionSupplier: () -> SurfaceControl.Transaction,
) : TransitionHandler {

    constructor(
        transitions: Transitions,
        desktopRepository: DesktopRepository,
    ) : this(transitions, desktopRepository, { SurfaceControl.Transaction() })
        displayController: DisplayController,
        shellTaskOrganizer: ShellTaskOrganizer,
    ) : this(
        transitions,
        desktopRepository,
        displayController,
        shellTaskOrganizer,
        { SurfaceControl.Transaction() }
    )

    private var state: TransitionState? = null

    @VisibleForTesting
    val pendingExternalExitTransitions = mutableSetOf<ExternalPendingExit>()

    /** Whether there is an immersive transition that hasn't completed yet. */
    private val inProgress: Boolean
        get() = state != null
@@ -61,15 +78,15 @@ class DesktopFullImmersiveTransitionHandler(
    var onTaskResizeAnimationListener: OnTaskResizeAnimationListener? = null

    /** Starts a transition to enter full immersive state inside the desktop. */
    fun enterImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
    fun moveTaskToImmersive(taskInfo: RunningTaskInfo) {
        if (inProgress) {
            ProtoLog.v(
                ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                "FullImmersive: cannot start entry because transition already in progress."
            )
            logV("Cannot start entry because transition already in progress.")
            return
        }

        val wct = WindowContainerTransaction().apply {
            setBounds(taskInfo.token, Rect())
        }
        logV("Moving task ${taskInfo.taskId} into immersive mode")
        val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
        state = TransitionState(
            transition = transition,
@@ -79,15 +96,18 @@ class DesktopFullImmersiveTransitionHandler(
        )
    }

    fun exitImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
    fun moveTaskToNonImmersive(taskInfo: RunningTaskInfo) {
        if (inProgress) {
            ProtoLog.v(
                ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                "$TAG: cannot start exit because transition already in progress."
            )
            logV("Cannot start exit because transition already in progress.")
            return
        }

        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
        val destinationBounds = calculateMaximizeBounds(displayLayout, taskInfo)
        val wct = WindowContainerTransaction().apply {
            setBounds(taskInfo.token, destinationBounds)
        }
        logV("Moving task ${taskInfo.taskId} out of immersive mode")
        val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
        state = TransitionState(
            transition = transition,
@@ -97,6 +117,82 @@ class DesktopFullImmersiveTransitionHandler(
        )
    }

    /**
     * Bring the immersive app of the given [displayId] out of immersive mode, if applicable.
     *
     * @param transition that will apply this transaction
     * @param wct that will apply these changes
     * @param displayId of the display that should exit immersive mode
     */
    fun exitImmersiveIfApplicable(
        transition: IBinder,
        wct: WindowContainerTransaction,
        displayId: Int
    ) {
        if (!Flags.enableFullyImmersiveInDesktop()) return
        exitImmersiveIfApplicable(wct, displayId)?.invoke(transition)
    }

    /**
     * Bring the immersive app of the given [displayId] out of immersive mode, if applicable.
     *
     * @param wct that will apply these changes
     * @param displayId of the display that should exit immersive mode
     * @return a function to apply once the transition that will apply these changes is started
     */
    fun exitImmersiveIfApplicable(
        wct: WindowContainerTransaction,
        displayId: Int
    ): ((IBinder) -> Unit)? {
        if (!Flags.enableFullyImmersiveInDesktop()) return null
        val displayLayout = displayController.getDisplayLayout(displayId) ?: return null
        val immersiveTask = desktopRepository.getTaskInFullImmersiveState(displayId) ?: return null
        val taskInfo = shellTaskOrganizer.getRunningTaskInfo(immersiveTask) ?: return null
        logV("Appending immersive exit for task: $immersiveTask in display: $displayId")
        wct.setBounds(taskInfo.token, calculateMaximizeBounds(displayLayout, taskInfo))
        return { transition -> addPendingImmersiveExit(immersiveTask, displayId, transition) }
    }

    /**
     * Bring the given [taskInfo] out of immersive mode, if applicable.
     *
     * @param wct that will apply these changes
     * @param taskInfo of the task that should exit immersive mode
     * @return a function to apply once the transition that will apply these changes is started
     */
    fun exitImmersiveIfApplicable(
        wct: WindowContainerTransaction,
        taskInfo: RunningTaskInfo
    ): ((IBinder) -> Unit)? {
        if (!Flags.enableFullyImmersiveInDesktop()) return null
        if (desktopRepository.isTaskInFullImmersiveState(taskInfo.taskId)) {
            // A full immersive task is being minimized, make sure the immersive state is broken
            // (i.e. resize back to max bounds).
            displayController.getDisplayLayout(taskInfo.displayId)?.let { displayLayout ->
                wct.setBounds(taskInfo.token, calculateMaximizeBounds(displayLayout, taskInfo))
                logV("Appending immersive exit for task: ${taskInfo.taskId}")
                return { transition ->
                    addPendingImmersiveExit(
                        taskId = taskInfo.taskId,
                        displayId = taskInfo.displayId,
                        transition = transition
                    )
                }
            }
        }
        return null
    }

    private fun addPendingImmersiveExit(taskId: Int, displayId: Int, transition: IBinder) {
        pendingExternalExitTransitions.add(
            ExternalPendingExit(
                taskId = taskId,
                displayId = displayId,
                transition = transition
            )
        )
    }

    override fun startAnimation(
        transition: IBinder,
        info: TransitionInfo,
@@ -190,15 +286,31 @@ class DesktopFullImmersiveTransitionHandler(
     * Called when any transition in the system is ready to play. This is needed to update the
     * repository state before window decorations are drawn (which happens immediately after
     * |onTransitionReady|, before this transition actually animates) because drawing decorations
     * depends in whether the task is in full immersive state or not.
     * depends on whether the task is in full immersive state or not.
     */
    fun onTransitionReady(transition: IBinder) {
    fun onTransitionReady(transition: IBinder, info: TransitionInfo) {
        // Check if this is a pending external exit transition.
        val pendingExit = pendingExternalExitTransitions
            .firstOrNull { pendingExit -> pendingExit.transition == transition }
        if (pendingExit != null) {
            pendingExternalExitTransitions.remove(pendingExit)
            if (info.hasTaskChange(taskId = pendingExit.taskId)) {
                if (desktopRepository.isTaskInFullImmersiveState(pendingExit.taskId)) {
                    logV("Pending external exit for task ${pendingExit.taskId} verified")
                    desktopRepository.setTaskInFullImmersiveState(
                        displayId = pendingExit.displayId,
                        taskId = pendingExit.taskId,
                        immersive = false
                    )
                }
            }
            return
        }

        // Check if this is a direct immersive enter/exit transition.
        val state = this.state ?: return
        // TODO: b/369443668 - this assumes invoking the exit transition is the only way to exit
        //  immersive, which isn't realistic. The app could crash, the user could dismiss it from
        //  overview, etc. This (or its caller) should search all transitions to look for any
        //  immersive task exiting that state to keep the repository properly updated.
        if (transition == state.transition) {
            logV("Direct move for task ${state.taskId} in ${state.direction} direction verified")
            when (state.direction) {
                Direction.ENTER -> {
                    desktopRepository.setTaskInFullImmersiveState(
@@ -225,6 +337,9 @@ class DesktopFullImmersiveTransitionHandler(
    private fun requireState(): TransitionState =
        state ?: error("Expected non-null transition state")

    private fun TransitionInfo.hasTaskChange(taskId: Int): Boolean =
        changes.any { c -> c.taskInfo?.taskId == taskId }

    /** The state of the currently running transition. */
    private data class TransitionState(
        val transition: IBinder,
@@ -233,12 +348,28 @@ class DesktopFullImmersiveTransitionHandler(
        val direction: Direction
    )

    /**
     * Tracks state of a transition involving an immersive exit that is external to this class' own
     * transitions. This usually means transitions that exit immersive mode as a side-effect and
     * not the primary action (for example, minimizing the immersive task or launching a new task
     * on top of the immersive task).
     */
    data class ExternalPendingExit(
        val taskId: Int,
        val displayId: Int,
        val transition: IBinder,
    )

    private enum class Direction {
        ENTER, EXIT
    }

    private fun logV(msg: String, vararg arguments: Any?) {
        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
    }

    private companion object {
        private const val TAG = "FullImmersiveHandler"
        private const val TAG = "DesktopImmersive"

        private const val FULL_IMMERSIVE_ANIM_DURATION_MS = 336L
    }
+23 −0
Original line number Diff line number Diff line
@@ -122,6 +122,29 @@ fun calculateInitialBounds(
    return positionInScreen(initialSize, stableBounds)
}

/**
 * Calculates the maximized bounds of a task given in the given [DisplayLayout], taking
 * resizability into consideration.
 */
fun calculateMaximizeBounds(
    displayLayout: DisplayLayout,
    taskInfo: RunningTaskInfo,
): Rect {
    val stableBounds = Rect()
    displayLayout.getStableBounds(stableBounds)
    if (taskInfo.isResizeable) {
        // if resizable then expand to entire stable bounds (full display minus insets)
        return Rect(stableBounds)
    } else {
        // if non-resizable then calculate max bounds according to aspect ratio
        val activityAspectRatio = calculateAspectRatio(taskInfo)
        val newSize = maximizeSizeGivenAspectRatio(taskInfo,
            Size(stableBounds.width(), stableBounds.height()), activityAspectRatio)
        return centerInArea(
            newSize, stableBounds, stableBounds.left, stableBounds.top)
    }
}

/**
 * Calculates the largest size that can fit in a given area while maintaining a specific aspect
 * ratio.
+4 −0
Original line number Diff line number Diff line
@@ -328,6 +328,10 @@ class DesktopRepository (
        return desktopTaskDataSequence().any { taskId == it.fullImmersiveTaskId }
    }

    /** Returns the task that is currently in immersive mode in this display, or null. */
    fun getTaskInFullImmersiveState(displayId: Int): Int? =
        desktopTaskDataByDisplayId.getOrCreate(displayId).fullImmersiveTaskId

    private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) {
        visibleTasksListeners.forEach { (listener, executor) ->
            executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) }
+48 −25
Original line number Diff line number Diff line
@@ -91,6 +91,7 @@ import com.android.wm.shell.shared.ShellSharedConstants
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.annotations.ExternalThread
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useDesktopOverrideDensity
@@ -190,6 +191,7 @@ class DesktopTasksController(

    private var recentsAnimationRunning = false
    private lateinit var splitScreenController: SplitScreenController
    lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter
    // Launch cookie used to identify a drag and drop transition to fullscreen after it has begun.
    // Used to prevent handleRequest from moving the new fullscreen task to freeform.
    private var dragAndDropFullscreenCookie: Binder? = null
@@ -354,6 +356,8 @@ class DesktopTasksController(
        // TODO(342378842): Instead of using default display, support multiple displays
        val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(
            DEFAULT_DISPLAY, wct, taskId)
        val runOnTransit = immersiveTransitionHandler
            .exitImmersiveIfApplicable(wct, DEFAULT_DISPLAY)
        wct.startTask(
            taskId,
            ActivityOptions.makeBasic().apply {
@@ -363,6 +367,7 @@ class DesktopTasksController(
        // TODO(343149901): Add DPI changes for task launch
        val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
        addPendingMinimizeTransition(transition, taskToMinimize)
        runOnTransit?.invoke(transition)
        return true
    }

@@ -379,6 +384,7 @@ class DesktopTasksController(
        }
        logV("moveRunningTaskToDesktop taskId=%d", task.taskId)
        exitSplitIfApplicable(wct, task)
        val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(wct, task.displayId)
        // Bring other apps to front first
        val taskToMinimize =
            bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
@@ -386,6 +392,7 @@ class DesktopTasksController(

        val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
        addPendingMinimizeTransition(transition, taskToMinimize)
        runOnTransit?.invoke(transition)
    }

    /**
@@ -422,8 +429,13 @@ class DesktopTasksController(
        val taskToMinimize =
            bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId)
        addMoveToDesktopChanges(wct, taskInfo)
        val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
            wct, taskInfo.displayId)
        val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
        transition?.let { addPendingMinimizeTransition(it, taskToMinimize) }
        transition?.let {
            addPendingMinimizeTransition(it, taskToMinimize)
            runOnTransit?.invoke(transition)
        }
    }

    /**
@@ -455,18 +467,28 @@ class DesktopTasksController(
        taskRepository.addClosingTask(displayId, taskId)
    }

    /**
     * Perform clean up of the desktop wallpaper activity if the minimized window task is the last
     * active task.
     *
     * @param wct transaction to modify if the last active task is minimized
     * @param taskId task id of the window that's being minimized
     */
    fun onDesktopWindowMinimize(wct: WindowContainerTransaction, taskId: Int) {
    fun minimizeTask(taskInfo: RunningTaskInfo) {
        val taskId = taskInfo.taskId
        val displayId = taskInfo.displayId
        val wct = WindowContainerTransaction()
        if (taskRepository.isOnlyVisibleNonClosingTask(taskId)) {
            // Perform clean up of the desktop wallpaper activity if the minimized window task is
            // the last active task.
            removeWallpaperActivity(wct)
        }
        // Do not call taskRepository.minimizeTask because it will be called by DekstopTasksLimiter.
        // Notify immersive handler as it might need to exit immersive state.
        val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(wct, taskInfo)

        wct.reorder(taskInfo.token, false)
        val transition = freeformTaskTransitionStarter.startMinimizedModeTransition(wct)
        desktopTasksLimiter.ifPresent {
            it.addPendingMinimizeChange(
                transition = transition,
                displayId = displayId,
                taskId = taskId
            )
        }
        runOnTransit?.invoke(transition)
    }

    /** Move a task with given `taskId` to fullscreen */
@@ -552,6 +574,8 @@ class DesktopTasksController(
        // TODO: b/342378842 - Instead of using default display, support multiple displays
        val taskToMinimize: RunningTaskInfo? =
            addAndGetMinimizeChangesIfNeeded(DEFAULT_DISPLAY, wct, taskId)
        val runOnTransit = immersiveTransitionHandler
            .exitImmersiveIfApplicable(wct, DEFAULT_DISPLAY)
        wct.startTask(
            taskId,
            ActivityOptions.makeBasic().apply {
@@ -560,6 +584,7 @@ class DesktopTasksController(
        )
        val transition = transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */)
        addPendingMinimizeTransition(transition, taskToMinimize)
        runOnTransit?.invoke(transition)
    }

    /** Move a task to the front */
@@ -567,11 +592,14 @@ class DesktopTasksController(
        logV("moveTaskToFront taskId=%s", taskInfo.taskId)
        val wct = WindowContainerTransaction()
        wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
        val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
            wct, taskInfo.displayId)
        val taskToMinimize =
            addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo.taskId)

        val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */)
        addPendingMinimizeTransition(transition, taskToMinimize)
        runOnTransit?.invoke(transition)
    }

    /**
@@ -643,22 +671,12 @@ class DesktopTasksController(

    private fun moveDesktopTaskToFullImmersive(taskInfo: RunningTaskInfo) {
        check(taskInfo.isFreeform) { "Task must already be in freeform" }
        val wct = WindowContainerTransaction().apply {
            setBounds(taskInfo.token, Rect())
        }
        immersiveTransitionHandler.enterImmersive(taskInfo, wct)
        immersiveTransitionHandler.moveTaskToImmersive(taskInfo)
    }

    private fun exitDesktopTaskFromFullImmersive(taskInfo: RunningTaskInfo) {
        check(taskInfo.isFreeform) { "Task must already be in freeform" }
        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
        val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
        val destinationBounds = getMaximizeBounds(taskInfo, stableBounds)

        val wct = WindowContainerTransaction().apply {
            setBounds(taskInfo.token, destinationBounds)
        }
        immersiveTransitionHandler.exitImmersive(taskInfo, wct)
        immersiveTransitionHandler.moveTaskToNonImmersive(taskInfo)
    }

    /**
@@ -697,7 +715,7 @@ class DesktopTasksController(
            // and toggle to the stable bounds.
            taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)

            destinationBounds.set(getMaximizeBounds(taskInfo, stableBounds))
            destinationBounds.set(calculateMaximizeBounds(displayLayout, taskInfo))
        }


@@ -1285,8 +1303,10 @@ class DesktopTasksController(
        if (useDesktopOverrideDensity()) {
            wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE)
        }
        // Desktop Mode is showing and we're launching a new Task - we might need to minimize
        // a Task.
        // Desktop Mode is showing and we're launching a new Task:
        // 1) Exit immersive if needed.
        immersiveTransitionHandler.exitImmersiveIfApplicable(transition, wct, task.displayId)
        // 2) minimize a Task if needed.
        val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
        if (taskToMinimize != null) {
            addPendingMinimizeTransition(transition, taskToMinimize)
@@ -1316,6 +1336,9 @@ class DesktopTasksController(
                val taskToMinimize =
                    addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
                addPendingMinimizeTransition(transition, taskToMinimize)
                immersiveTransitionHandler.exitImmersiveIfApplicable(
                    transition, wct, task.displayId
                )
            }
        }
        return null
Loading