Loading libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +8 −2 Original line number Diff line number Diff line Loading @@ -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(); } Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt +151 −20 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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, Loading @@ -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, Loading @@ -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, Loading Loading @@ -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( Loading @@ -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, Loading @@ -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 } Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt +23 −0 Original line number Diff line number Diff line Loading @@ -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. Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +4 −0 Original line number Diff line number Diff line Loading @@ -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) } Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +48 −25 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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 { Loading @@ -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 } Loading @@ -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) Loading @@ -386,6 +392,7 @@ class DesktopTasksController( val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource) addPendingMinimizeTransition(transition, taskToMinimize) runOnTransit?.invoke(transition) } /** Loading Loading @@ -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) } } /** Loading Loading @@ -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 */ Loading Loading @@ -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 { Loading @@ -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 */ Loading @@ -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) } /** Loading Loading @@ -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) } /** Loading Loading @@ -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)) } Loading Loading @@ -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) Loading Loading @@ -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 Loading
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +8 −2 Original line number Diff line number Diff line Loading @@ -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(); } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt +151 −20 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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, Loading @@ -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, Loading @@ -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, Loading Loading @@ -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( Loading @@ -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, Loading @@ -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 } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt +23 −0 Original line number Diff line number Diff line Loading @@ -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. Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +4 −0 Original line number Diff line number Diff line Loading @@ -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) } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +48 −25 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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 { Loading @@ -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 } Loading @@ -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) Loading @@ -386,6 +392,7 @@ class DesktopTasksController( val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource) addPendingMinimizeTransition(transition, taskToMinimize) runOnTransit?.invoke(transition) } /** Loading Loading @@ -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) } } /** Loading Loading @@ -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 */ Loading Loading @@ -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 { Loading @@ -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 */ Loading @@ -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) } /** Loading Loading @@ -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) } /** Loading Loading @@ -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)) } Loading Loading @@ -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) Loading Loading @@ -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