Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt +103 −131 Original line number Diff line number Diff line Loading @@ -74,13 +74,11 @@ class DesktopImmersiveController( { SurfaceControl.Transaction() }, ) @VisibleForTesting var state: TransitionState? = null @VisibleForTesting val pendingExternalExitTransitions = mutableListOf<ExternalPendingExit>() @VisibleForTesting val pendingImmersiveTransitions = mutableListOf<PendingTransition>() /** Whether there is an immersive transition that hasn't completed yet. */ private val inProgress: Boolean get() = state != null || pendingExternalExitTransitions.isNotEmpty() get() = pendingImmersiveTransitions.isNotEmpty() private val rectEvaluator = RectEvaluator() Loading @@ -101,19 +99,18 @@ class DesktopImmersiveController( if (inProgress) { logV( "Cannot start entry because transition(s) already in progress: %s", getRunningTransitions(), pendingImmersiveTransitions, ) 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, displayId = taskInfo.displayId, addPendingImmersiveTransition( taskId = taskInfo.taskId, displayId = taskInfo.displayId, direction = Direction.ENTER, transition = transition, ) } Loading @@ -123,7 +120,7 @@ class DesktopImmersiveController( if (inProgress) { logV( "Cannot start exit because transition(s) already in progress: %s", getRunningTransitions(), pendingImmersiveTransitions, ) return } Loading @@ -134,12 +131,11 @@ class DesktopImmersiveController( } logV("Moving task %d out of immersive mode, reason: %s", taskInfo.taskId, reason) val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this) state = TransitionState( transition = transition, displayId = taskInfo.displayId, addPendingImmersiveTransition( taskId = taskInfo.taskId, displayId = taskInfo.displayId, direction = Direction.EXIT, transition = transition, ) } Loading Loading @@ -194,7 +190,13 @@ class DesktopImmersiveController( return ExitResult.Exit( exitingTask = immersiveTask, runOnTransitionStart = { transition -> addPendingImmersiveExit(immersiveTask, displayId, transition) addPendingImmersiveTransition( taskId = immersiveTask, displayId = displayId, direction = Direction.EXIT, transition = transition, animate = false, ) }, ) } Loading @@ -220,10 +222,12 @@ class DesktopImmersiveController( return ExitResult.Exit( exitingTask = taskInfo.taskId, runOnTransitionStart = { transition -> addPendingImmersiveExit( addPendingImmersiveTransition( taskId = taskInfo.taskId, displayId = taskInfo.displayId, direction = Direction.EXIT, transition = transition, animate = false, ) }, ) Loading @@ -233,14 +237,26 @@ class DesktopImmersiveController( /** Whether the [change] in the [transition] is a known immersive change. */ fun isImmersiveChange(transition: IBinder, change: TransitionInfo.Change): Boolean { return pendingExternalExitTransitions.any { return pendingImmersiveTransitions.any { it.transition == transition && it.taskId == change.taskInfo?.taskId } } private fun addPendingImmersiveExit(taskId: Int, displayId: Int, transition: IBinder) { pendingExternalExitTransitions.add( ExternalPendingExit(taskId = taskId, displayId = displayId, transition = transition) private fun addPendingImmersiveTransition( taskId: Int, displayId: Int, direction: Direction, transition: IBinder, animate: Boolean = true, ) { pendingImmersiveTransitions.add( PendingTransition( taskId = taskId, displayId = displayId, direction = direction, transition = transition, animate = animate, ) ) } Loading @@ -251,19 +267,17 @@ class DesktopImmersiveController( finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback, ): Boolean { val state = requireState() check(state.transition == transition) { "Transition $transition did not match expected state=$state" } val immersiveTransition = getImmersiveTransition(transition) ?: return false if (!immersiveTransition.animate) return false logD("startAnimation transition=%s", transition) animateResize( targetTaskId = state.taskId, targetTaskId = immersiveTransition.taskId, info = info, startTransaction = startTransaction, finishTransaction = finishTransaction, finishCallback = { finishCallback.onTransitionFinished(/* wct= */ null) clearState() pendingImmersiveTransitions.remove(immersiveTransition) }, ) return true Loading Loading @@ -346,18 +360,6 @@ class DesktopImmersiveController( request: TransitionRequestInfo, ): WindowContainerTransaction? = null override fun onTransitionConsumed( transition: IBinder, aborted: Boolean, finishTransaction: SurfaceControl.Transaction?, ) { val state = this.state ?: return if (transition == state.transition && aborted) { clearState() } super.onTransitionConsumed(transition, aborted, finishTransaction) } /** * 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 Loading @@ -371,67 +373,42 @@ class DesktopImmersiveController( finishTransaction: SurfaceControl.Transaction, ) { val desktopRepository: DesktopRepository = desktopUserRepositories.current // Check if this is a pending external exit transition. val pendingExit = pendingExternalExitTransitions.firstOrNull { pendingExit -> pendingExit.transition == transition } if (pendingExit != null) { if (info.hasTaskChange(taskId = pendingExit.taskId)) { if (desktopRepository.isTaskInFullImmersiveState(pendingExit.taskId)) { logV("Pending external exit for task#%d verified", pendingExit.taskId) desktopRepository.setTaskInFullImmersiveState( displayId = pendingExit.displayId, taskId = pendingExit.taskId, immersive = false, ) if (Flags.enableRestoreToPreviousSizeFromDesktopImmersive()) { desktopRepository.removeBoundsBeforeFullImmersive(pendingExit.taskId) } } } return } val pendingTransition = getImmersiveTransition(transition) // Check if this is a direct immersive enter/exit transition. if (transition == state?.transition) { val state = requireState() val immersiveChange = info.changes.firstOrNull { c -> c.taskInfo?.taskId == state.taskId } if (pendingTransition != null) { val taskId = pendingTransition.taskId val immersiveChange = info.getTaskChange(taskId = taskId) if (immersiveChange == null) { logV( "Direct move for task#%d in %s direction missing immersive change.", state.taskId, state.direction, "Transition for task#%d in %s direction missing immersive change.", taskId, pendingTransition.direction, ) return } val startBounds = immersiveChange.startAbsBounds logV("Direct move for task#%d in %s direction verified", state.taskId, state.direction) when (state.direction) { Direction.ENTER -> { logV( "Immersive transition for task#%d in %s direction verified", taskId, pendingTransition.direction, ) desktopRepository.setTaskInFullImmersiveState( displayId = state.displayId, taskId = state.taskId, immersive = true, displayId = pendingTransition.displayId, taskId = taskId, immersive = pendingTransition.direction == Direction.ENTER, ) if (Flags.enableRestoreToPreviousSizeFromDesktopImmersive()) { desktopRepository.saveBoundsBeforeFullImmersive(state.taskId, startBounds) } } when (pendingTransition.direction) { Direction.EXIT -> { desktopRepository.setTaskInFullImmersiveState( displayId = state.displayId, taskId = state.taskId, immersive = false, desktopRepository.removeBoundsBeforeFullImmersive(taskId) } Direction.ENTER -> { desktopRepository.saveBoundsBeforeFullImmersive( taskId, immersiveChange.startAbsBounds, ) if (Flags.enableRestoreToPreviousSizeFromDesktopImmersive()) { desktopRepository.removeBoundsBeforeFullImmersive(state.taskId) } } } return } // Check if this is an untracked exit transition, like display rotation. Loading @@ -450,35 +427,31 @@ class DesktopImmersiveController( } override fun onTransitionMerged(merged: IBinder, playing: IBinder) { val pendingExit = pendingExternalExitTransitions.firstOrNull { pendingExit -> pendingExit.transition == merged val pendingTransition = pendingImmersiveTransitions.firstOrNull { pendingTransition -> pendingTransition.transition == merged } if (pendingExit != null) { if (pendingTransition != null) { logV( "Pending exit transition %s for task#%s merged into %s", "Pending transition %s for task#%s merged into %s", merged, pendingExit.taskId, pendingTransition.taskId, playing, ) pendingExit.transition = playing pendingTransition.transition = playing } } override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { val pendingExit = pendingExternalExitTransitions.firstOrNull { pendingExit -> pendingExit.transition == transition } if (pendingExit != null) { logV("Pending exit transition %s for task#%s finished", transition, pendingExit) pendingExternalExitTransitions.remove(pendingExit) val pendingTransition = getImmersiveTransition(transition) if (pendingTransition != null) { logV("Pending exit transition %s for task#%s finished", transition, pendingTransition) pendingImmersiveTransitions.remove(pendingTransition) } } private fun clearState() { state = null } private fun getImmersiveTransition(transition: IBinder) = pendingImmersiveTransitions.firstOrNull { it.transition == transition } private fun getExitDestinationBounds(taskInfo: RunningTaskInfo): Rect { val displayLayout = Loading @@ -496,24 +469,13 @@ class DesktopImmersiveController( } } private fun requireState(): TransitionState = state ?: error("Expected non-null transition state") private fun getRunningTransitions(): List<IBinder> { val running = mutableListOf<IBinder>() state?.let { running.add(it.transition) } pendingExternalExitTransitions.forEach { running.add(it.transition) } return running } private fun TransitionInfo.hasTaskChange(taskId: Int): Boolean = changes.any { c -> c.taskInfo?.taskId == taskId } private fun TransitionInfo.getTaskChange(taskId: Int): TransitionInfo.Change? = changes.firstOrNull { c -> c.taskInfo?.taskId == taskId } private fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopImmersiveController") pw.println(innerPrefix + "state=" + state) pw.println(innerPrefix + "pendingExternalExitTransitions=" + pendingExternalExitTransitions) pw.println(innerPrefix + "pendingImmersiveTransitions=" + pendingImmersiveTransitions) } /** The state of the currently running transition. */ Loading @@ -526,12 +488,22 @@ class DesktopImmersiveController( ) /** * 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). * Tracks state of a transition involving an immersive enter or exit. This includes both * transitions that should and should not be animated by this handler. * * @param taskId of the task that should enter/exit immersive mode * @param displayId of the display that should enter/exit immersive mode * @param direction of the immersive transition * @param transition that will apply this transaction * @param animate whether transition should be animated by this handler */ data class ExternalPendingExit(val taskId: Int, val displayId: Int, var transition: IBinder) data class PendingTransition( val taskId: Int, val displayId: Int, val direction: Direction, var transition: IBinder, val animate: Boolean, ) /** The result of an external exit request. */ sealed class ExitResult { Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt +89 −32 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.desktopmode.DesktopImmersiveController.Direction import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitReason.USER_INTERACTION import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.sysui.ShellInit Loading Loading @@ -95,6 +96,8 @@ class DesktopImmersiveControllerTest : ShellTestCase() { whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { invocation -> (invocation.getArgument(0) as Rect).set(STABLE_BOUNDS) } whenever(mockDisplayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) whenever(mockDisplayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) controller = DesktopImmersiveController( shellInit = mock(), transitions = mockTransitions, Loading Loading @@ -277,10 +280,12 @@ class DesktopImmersiveControllerTest : ShellTestCase() { controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION) assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isTrue() assertTransitionPending( transition = transition, taskId = task.taskId, direction = Direction.EXIT, animate = false ) } @Test Loading @@ -298,10 +303,12 @@ class DesktopImmersiveControllerTest : ShellTestCase() { controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION) assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isFalse() assertTransitionNotPending( transition = transition, taskId = task.taskId, direction = Direction.EXIT, animate = false ) } @Test Loading Loading @@ -360,10 +367,12 @@ class DesktopImmersiveControllerTest : ShellTestCase() { reason = USER_INTERACTION, ).asExit()?.runOnTransitionStart?.invoke(transition) assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isFalse() assertTransitionNotPending( transition = transition, taskId = task.taskId, animate = false, direction = Direction.EXIT ) } @Test Loading Loading @@ -416,10 +425,12 @@ class DesktopImmersiveControllerTest : ShellTestCase() { controller.exitImmersiveIfApplicable(wct, task, USER_INTERACTION) .asExit()?.runOnTransitionStart?.invoke(transition) assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isTrue() assertTransitionPending( transition = transition, taskId = task.taskId, direction = Direction.EXIT, animate = false ) } @Test Loading Loading @@ -481,10 +492,12 @@ class DesktopImmersiveControllerTest : ShellTestCase() { ) controller.onTransitionFinished(transition, aborted = false) assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isFalse() assertTransitionNotPending( transition = transition, taskId = task.taskId, direction = Direction.EXIT, animate = false ) } @Test Loading Loading @@ -513,14 +526,18 @@ class DesktopImmersiveControllerTest : ShellTestCase() { controller.onTransitionMerged(transition, mergedToTransition) controller.onTransitionFinished(mergedToTransition, aborted = false) assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isFalse() assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == mergedToTransition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isFalse() assertTransitionNotPending( transition = transition, taskId = task.taskId, animate = false, direction = Direction.EXIT ) assertTransitionNotPending( transition = mergedToTransition, taskId = task.taskId, animate = false, direction = Direction.EXIT ) } @Test Loading Loading @@ -686,7 +703,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun externalAnimateResizeChange_doesNotCleanUpPendingTransitionState() { fun externalAnimateResizeChange_doesNotRemovePendingTransition() { val task = createFreeformTask() val mockBinder = mock(IBinder::class.java) whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))) Loading @@ -709,12 +726,16 @@ class DesktopImmersiveControllerTest : ShellTestCase() { ) animatorTestRule.advanceTimeBy(DesktopImmersiveController.FULL_IMMERSIVE_ANIM_DURATION_MS) assertThat(controller.state).isNotNull() assertTransitionPending( transition = mockBinder, taskId = task.taskId, direction = Direction.EXIT ) } @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun startAnimation_missingChange_clearsState() { fun startAnimation_missingChange_removesPendingTransition() { val task = createFreeformTask() val mockBinder = mock(IBinder::class.java) whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))) Loading @@ -735,7 +756,42 @@ class DesktopImmersiveControllerTest : ShellTestCase() { finishCallback = {} ) assertThat(controller.state).isNull() assertTransitionNotPending( transition = mockBinder, taskId = task.taskId, direction = Direction.ENTER ) } private fun assertTransitionPending( transition: IBinder, taskId: Int, direction: Direction, animate: Boolean = true, displayId: Int = DEFAULT_DISPLAY ) { assertThat(controller.pendingImmersiveTransitions.any { pendingTransition -> pendingTransition.transition == transition && pendingTransition.displayId == displayId && pendingTransition.taskId == taskId && pendingTransition.animate == animate && pendingTransition.direction == direction }).isTrue() } private fun assertTransitionNotPending( transition: IBinder, taskId: Int, direction: Direction, animate: Boolean = true, displayId: Int = DEFAULT_DISPLAY ) { assertThat(controller.pendingImmersiveTransitions.any { pendingTransition -> pendingTransition.transition == transition && pendingTransition.displayId == displayId && pendingTransition.taskId == taskId && pendingTransition.direction == direction }).isFalse() } private fun createTransitionInfo( Loading Loading @@ -768,5 +824,6 @@ class DesktopImmersiveControllerTest : ShellTestCase() { companion object { private val STABLE_BOUNDS = Rect(0, 100, 2000, 1900) private val DISPLAY_BOUNDS = Rect(0, 0, 2000, 2000) } } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt +103 −131 Original line number Diff line number Diff line Loading @@ -74,13 +74,11 @@ class DesktopImmersiveController( { SurfaceControl.Transaction() }, ) @VisibleForTesting var state: TransitionState? = null @VisibleForTesting val pendingExternalExitTransitions = mutableListOf<ExternalPendingExit>() @VisibleForTesting val pendingImmersiveTransitions = mutableListOf<PendingTransition>() /** Whether there is an immersive transition that hasn't completed yet. */ private val inProgress: Boolean get() = state != null || pendingExternalExitTransitions.isNotEmpty() get() = pendingImmersiveTransitions.isNotEmpty() private val rectEvaluator = RectEvaluator() Loading @@ -101,19 +99,18 @@ class DesktopImmersiveController( if (inProgress) { logV( "Cannot start entry because transition(s) already in progress: %s", getRunningTransitions(), pendingImmersiveTransitions, ) 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, displayId = taskInfo.displayId, addPendingImmersiveTransition( taskId = taskInfo.taskId, displayId = taskInfo.displayId, direction = Direction.ENTER, transition = transition, ) } Loading @@ -123,7 +120,7 @@ class DesktopImmersiveController( if (inProgress) { logV( "Cannot start exit because transition(s) already in progress: %s", getRunningTransitions(), pendingImmersiveTransitions, ) return } Loading @@ -134,12 +131,11 @@ class DesktopImmersiveController( } logV("Moving task %d out of immersive mode, reason: %s", taskInfo.taskId, reason) val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this) state = TransitionState( transition = transition, displayId = taskInfo.displayId, addPendingImmersiveTransition( taskId = taskInfo.taskId, displayId = taskInfo.displayId, direction = Direction.EXIT, transition = transition, ) } Loading Loading @@ -194,7 +190,13 @@ class DesktopImmersiveController( return ExitResult.Exit( exitingTask = immersiveTask, runOnTransitionStart = { transition -> addPendingImmersiveExit(immersiveTask, displayId, transition) addPendingImmersiveTransition( taskId = immersiveTask, displayId = displayId, direction = Direction.EXIT, transition = transition, animate = false, ) }, ) } Loading @@ -220,10 +222,12 @@ class DesktopImmersiveController( return ExitResult.Exit( exitingTask = taskInfo.taskId, runOnTransitionStart = { transition -> addPendingImmersiveExit( addPendingImmersiveTransition( taskId = taskInfo.taskId, displayId = taskInfo.displayId, direction = Direction.EXIT, transition = transition, animate = false, ) }, ) Loading @@ -233,14 +237,26 @@ class DesktopImmersiveController( /** Whether the [change] in the [transition] is a known immersive change. */ fun isImmersiveChange(transition: IBinder, change: TransitionInfo.Change): Boolean { return pendingExternalExitTransitions.any { return pendingImmersiveTransitions.any { it.transition == transition && it.taskId == change.taskInfo?.taskId } } private fun addPendingImmersiveExit(taskId: Int, displayId: Int, transition: IBinder) { pendingExternalExitTransitions.add( ExternalPendingExit(taskId = taskId, displayId = displayId, transition = transition) private fun addPendingImmersiveTransition( taskId: Int, displayId: Int, direction: Direction, transition: IBinder, animate: Boolean = true, ) { pendingImmersiveTransitions.add( PendingTransition( taskId = taskId, displayId = displayId, direction = direction, transition = transition, animate = animate, ) ) } Loading @@ -251,19 +267,17 @@ class DesktopImmersiveController( finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback, ): Boolean { val state = requireState() check(state.transition == transition) { "Transition $transition did not match expected state=$state" } val immersiveTransition = getImmersiveTransition(transition) ?: return false if (!immersiveTransition.animate) return false logD("startAnimation transition=%s", transition) animateResize( targetTaskId = state.taskId, targetTaskId = immersiveTransition.taskId, info = info, startTransaction = startTransaction, finishTransaction = finishTransaction, finishCallback = { finishCallback.onTransitionFinished(/* wct= */ null) clearState() pendingImmersiveTransitions.remove(immersiveTransition) }, ) return true Loading Loading @@ -346,18 +360,6 @@ class DesktopImmersiveController( request: TransitionRequestInfo, ): WindowContainerTransaction? = null override fun onTransitionConsumed( transition: IBinder, aborted: Boolean, finishTransaction: SurfaceControl.Transaction?, ) { val state = this.state ?: return if (transition == state.transition && aborted) { clearState() } super.onTransitionConsumed(transition, aborted, finishTransaction) } /** * 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 Loading @@ -371,67 +373,42 @@ class DesktopImmersiveController( finishTransaction: SurfaceControl.Transaction, ) { val desktopRepository: DesktopRepository = desktopUserRepositories.current // Check if this is a pending external exit transition. val pendingExit = pendingExternalExitTransitions.firstOrNull { pendingExit -> pendingExit.transition == transition } if (pendingExit != null) { if (info.hasTaskChange(taskId = pendingExit.taskId)) { if (desktopRepository.isTaskInFullImmersiveState(pendingExit.taskId)) { logV("Pending external exit for task#%d verified", pendingExit.taskId) desktopRepository.setTaskInFullImmersiveState( displayId = pendingExit.displayId, taskId = pendingExit.taskId, immersive = false, ) if (Flags.enableRestoreToPreviousSizeFromDesktopImmersive()) { desktopRepository.removeBoundsBeforeFullImmersive(pendingExit.taskId) } } } return } val pendingTransition = getImmersiveTransition(transition) // Check if this is a direct immersive enter/exit transition. if (transition == state?.transition) { val state = requireState() val immersiveChange = info.changes.firstOrNull { c -> c.taskInfo?.taskId == state.taskId } if (pendingTransition != null) { val taskId = pendingTransition.taskId val immersiveChange = info.getTaskChange(taskId = taskId) if (immersiveChange == null) { logV( "Direct move for task#%d in %s direction missing immersive change.", state.taskId, state.direction, "Transition for task#%d in %s direction missing immersive change.", taskId, pendingTransition.direction, ) return } val startBounds = immersiveChange.startAbsBounds logV("Direct move for task#%d in %s direction verified", state.taskId, state.direction) when (state.direction) { Direction.ENTER -> { logV( "Immersive transition for task#%d in %s direction verified", taskId, pendingTransition.direction, ) desktopRepository.setTaskInFullImmersiveState( displayId = state.displayId, taskId = state.taskId, immersive = true, displayId = pendingTransition.displayId, taskId = taskId, immersive = pendingTransition.direction == Direction.ENTER, ) if (Flags.enableRestoreToPreviousSizeFromDesktopImmersive()) { desktopRepository.saveBoundsBeforeFullImmersive(state.taskId, startBounds) } } when (pendingTransition.direction) { Direction.EXIT -> { desktopRepository.setTaskInFullImmersiveState( displayId = state.displayId, taskId = state.taskId, immersive = false, desktopRepository.removeBoundsBeforeFullImmersive(taskId) } Direction.ENTER -> { desktopRepository.saveBoundsBeforeFullImmersive( taskId, immersiveChange.startAbsBounds, ) if (Flags.enableRestoreToPreviousSizeFromDesktopImmersive()) { desktopRepository.removeBoundsBeforeFullImmersive(state.taskId) } } } return } // Check if this is an untracked exit transition, like display rotation. Loading @@ -450,35 +427,31 @@ class DesktopImmersiveController( } override fun onTransitionMerged(merged: IBinder, playing: IBinder) { val pendingExit = pendingExternalExitTransitions.firstOrNull { pendingExit -> pendingExit.transition == merged val pendingTransition = pendingImmersiveTransitions.firstOrNull { pendingTransition -> pendingTransition.transition == merged } if (pendingExit != null) { if (pendingTransition != null) { logV( "Pending exit transition %s for task#%s merged into %s", "Pending transition %s for task#%s merged into %s", merged, pendingExit.taskId, pendingTransition.taskId, playing, ) pendingExit.transition = playing pendingTransition.transition = playing } } override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { val pendingExit = pendingExternalExitTransitions.firstOrNull { pendingExit -> pendingExit.transition == transition } if (pendingExit != null) { logV("Pending exit transition %s for task#%s finished", transition, pendingExit) pendingExternalExitTransitions.remove(pendingExit) val pendingTransition = getImmersiveTransition(transition) if (pendingTransition != null) { logV("Pending exit transition %s for task#%s finished", transition, pendingTransition) pendingImmersiveTransitions.remove(pendingTransition) } } private fun clearState() { state = null } private fun getImmersiveTransition(transition: IBinder) = pendingImmersiveTransitions.firstOrNull { it.transition == transition } private fun getExitDestinationBounds(taskInfo: RunningTaskInfo): Rect { val displayLayout = Loading @@ -496,24 +469,13 @@ class DesktopImmersiveController( } } private fun requireState(): TransitionState = state ?: error("Expected non-null transition state") private fun getRunningTransitions(): List<IBinder> { val running = mutableListOf<IBinder>() state?.let { running.add(it.transition) } pendingExternalExitTransitions.forEach { running.add(it.transition) } return running } private fun TransitionInfo.hasTaskChange(taskId: Int): Boolean = changes.any { c -> c.taskInfo?.taskId == taskId } private fun TransitionInfo.getTaskChange(taskId: Int): TransitionInfo.Change? = changes.firstOrNull { c -> c.taskInfo?.taskId == taskId } private fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopImmersiveController") pw.println(innerPrefix + "state=" + state) pw.println(innerPrefix + "pendingExternalExitTransitions=" + pendingExternalExitTransitions) pw.println(innerPrefix + "pendingImmersiveTransitions=" + pendingImmersiveTransitions) } /** The state of the currently running transition. */ Loading @@ -526,12 +488,22 @@ class DesktopImmersiveController( ) /** * 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). * Tracks state of a transition involving an immersive enter or exit. This includes both * transitions that should and should not be animated by this handler. * * @param taskId of the task that should enter/exit immersive mode * @param displayId of the display that should enter/exit immersive mode * @param direction of the immersive transition * @param transition that will apply this transaction * @param animate whether transition should be animated by this handler */ data class ExternalPendingExit(val taskId: Int, val displayId: Int, var transition: IBinder) data class PendingTransition( val taskId: Int, val displayId: Int, val direction: Direction, var transition: IBinder, val animate: Boolean, ) /** The result of an external exit request. */ sealed class ExitResult { Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt +89 −32 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.desktopmode.DesktopImmersiveController.Direction import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitReason.USER_INTERACTION import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.sysui.ShellInit Loading Loading @@ -95,6 +96,8 @@ class DesktopImmersiveControllerTest : ShellTestCase() { whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { invocation -> (invocation.getArgument(0) as Rect).set(STABLE_BOUNDS) } whenever(mockDisplayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) whenever(mockDisplayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) controller = DesktopImmersiveController( shellInit = mock(), transitions = mockTransitions, Loading Loading @@ -277,10 +280,12 @@ class DesktopImmersiveControllerTest : ShellTestCase() { controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION) assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isTrue() assertTransitionPending( transition = transition, taskId = task.taskId, direction = Direction.EXIT, animate = false ) } @Test Loading @@ -298,10 +303,12 @@ class DesktopImmersiveControllerTest : ShellTestCase() { controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION) assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isFalse() assertTransitionNotPending( transition = transition, taskId = task.taskId, direction = Direction.EXIT, animate = false ) } @Test Loading Loading @@ -360,10 +367,12 @@ class DesktopImmersiveControllerTest : ShellTestCase() { reason = USER_INTERACTION, ).asExit()?.runOnTransitionStart?.invoke(transition) assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isFalse() assertTransitionNotPending( transition = transition, taskId = task.taskId, animate = false, direction = Direction.EXIT ) } @Test Loading Loading @@ -416,10 +425,12 @@ class DesktopImmersiveControllerTest : ShellTestCase() { controller.exitImmersiveIfApplicable(wct, task, USER_INTERACTION) .asExit()?.runOnTransitionStart?.invoke(transition) assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isTrue() assertTransitionPending( transition = transition, taskId = task.taskId, direction = Direction.EXIT, animate = false ) } @Test Loading Loading @@ -481,10 +492,12 @@ class DesktopImmersiveControllerTest : ShellTestCase() { ) controller.onTransitionFinished(transition, aborted = false) assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isFalse() assertTransitionNotPending( transition = transition, taskId = task.taskId, direction = Direction.EXIT, animate = false ) } @Test Loading Loading @@ -513,14 +526,18 @@ class DesktopImmersiveControllerTest : ShellTestCase() { controller.onTransitionMerged(transition, mergedToTransition) controller.onTransitionFinished(mergedToTransition, aborted = false) assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isFalse() assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == mergedToTransition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isFalse() assertTransitionNotPending( transition = transition, taskId = task.taskId, animate = false, direction = Direction.EXIT ) assertTransitionNotPending( transition = mergedToTransition, taskId = task.taskId, animate = false, direction = Direction.EXIT ) } @Test Loading Loading @@ -686,7 +703,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun externalAnimateResizeChange_doesNotCleanUpPendingTransitionState() { fun externalAnimateResizeChange_doesNotRemovePendingTransition() { val task = createFreeformTask() val mockBinder = mock(IBinder::class.java) whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))) Loading @@ -709,12 +726,16 @@ class DesktopImmersiveControllerTest : ShellTestCase() { ) animatorTestRule.advanceTimeBy(DesktopImmersiveController.FULL_IMMERSIVE_ANIM_DURATION_MS) assertThat(controller.state).isNotNull() assertTransitionPending( transition = mockBinder, taskId = task.taskId, direction = Direction.EXIT ) } @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun startAnimation_missingChange_clearsState() { fun startAnimation_missingChange_removesPendingTransition() { val task = createFreeformTask() val mockBinder = mock(IBinder::class.java) whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))) Loading @@ -735,7 +756,42 @@ class DesktopImmersiveControllerTest : ShellTestCase() { finishCallback = {} ) assertThat(controller.state).isNull() assertTransitionNotPending( transition = mockBinder, taskId = task.taskId, direction = Direction.ENTER ) } private fun assertTransitionPending( transition: IBinder, taskId: Int, direction: Direction, animate: Boolean = true, displayId: Int = DEFAULT_DISPLAY ) { assertThat(controller.pendingImmersiveTransitions.any { pendingTransition -> pendingTransition.transition == transition && pendingTransition.displayId == displayId && pendingTransition.taskId == taskId && pendingTransition.animate == animate && pendingTransition.direction == direction }).isTrue() } private fun assertTransitionNotPending( transition: IBinder, taskId: Int, direction: Direction, animate: Boolean = true, displayId: Int = DEFAULT_DISPLAY ) { assertThat(controller.pendingImmersiveTransitions.any { pendingTransition -> pendingTransition.transition == transition && pendingTransition.displayId == displayId && pendingTransition.taskId == taskId && pendingTransition.direction == direction }).isFalse() } private fun createTransitionInfo( Loading Loading @@ -768,5 +824,6 @@ class DesktopImmersiveControllerTest : ShellTestCase() { companion object { private val STABLE_BOUNDS = Rect(0, 100, 2000, 1900) private val DISPLAY_BOUNDS = Rect(0, 0, 2000, 2000) } }