• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.desktopmode
18 
19 import android.app.ActivityTaskManager.INVALID_TASK_ID
20 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
21 import android.content.Context
22 import android.os.Handler
23 import android.os.IBinder
24 import android.view.SurfaceControl
25 import android.view.WindowManager
26 import android.view.WindowManager.TRANSIT_CLOSE
27 import android.view.WindowManager.TRANSIT_OPEN
28 import android.window.DesktopModeFlags
29 import android.window.TransitionInfo
30 import android.window.TransitionInfo.Change
31 import android.window.TransitionRequestInfo
32 import android.window.WindowContainerTransaction
33 import androidx.annotation.VisibleForTesting
34 import com.android.internal.jank.InteractionJankMonitor
35 import com.android.internal.protolog.ProtoLog
36 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
37 import com.android.wm.shell.freeform.FreeformTaskTransitionHandler
38 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
39 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
40 import com.android.wm.shell.shared.TransitionUtil
41 import com.android.wm.shell.shared.annotations.ShellMainThread
42 import com.android.wm.shell.sysui.ShellInit
43 import com.android.wm.shell.transition.MixedTransitionHandler
44 import com.android.wm.shell.transition.Transitions
45 import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
46 
47 /** The [Transitions.TransitionHandler] coordinates transition handlers in desktop windowing. */
48 class DesktopMixedTransitionHandler(
49     private val context: Context,
50     private val transitions: Transitions,
51     private val desktopUserRepositories: DesktopUserRepositories,
52     private val freeformTaskTransitionHandler: FreeformTaskTransitionHandler,
53     private val closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler,
54     private val desktopImmersiveController: DesktopImmersiveController,
55     private val desktopMinimizationTransitionHandler: DesktopMinimizationTransitionHandler,
56     private val interactionJankMonitor: InteractionJankMonitor,
57     @ShellMainThread private val handler: Handler,
58     shellInit: ShellInit,
59     private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
60 ) : MixedTransitionHandler, FreeformTaskTransitionStarter {
61 
62     init {
63         shellInit.addInitCallback({ transitions.addHandler(this) }, this)
64     }
65 
66     @VisibleForTesting val pendingMixedTransitions = mutableListOf<PendingMixedTransition>()
67 
68     /** Delegates starting transition to [FreeformTaskTransitionHandler]. */
69     override fun startWindowingModeTransition(
70         targetWindowingMode: Int,
71         wct: WindowContainerTransaction?,
72     ) = freeformTaskTransitionHandler.startWindowingModeTransition(targetWindowingMode, wct)
73 
74     /**
75      * Starts a minimize transition for [taskId], with [isLastTask] which is true if the task going
76      * to be minimized is the last visible task.
77      */
78     override fun startMinimizedModeTransition(
79         wct: WindowContainerTransaction?,
80         taskId: Int,
81         isLastTask: Boolean,
82     ): IBinder {
83         if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX.isTrue) {
84             return freeformTaskTransitionHandler.startMinimizedModeTransition(
85                 wct,
86                 taskId,
87                 isLastTask,
88             )
89         }
90         requireNotNull(wct)
91         return transitions
92             .startTransition(Transitions.TRANSIT_MINIMIZE, wct, /* handler= */ this)
93             .also { transition ->
94                 pendingMixedTransitions.add(
95                     PendingMixedTransition.Minimize(transition, taskId, isLastTask)
96                 )
97             }
98     }
99 
100     /** Delegates starting PiP transition to [FreeformTaskTransitionHandler]. */
101     override fun startPipTransition(wct: WindowContainerTransaction?): IBinder =
102         freeformTaskTransitionHandler.startPipTransition(wct)
103 
104     /** Starts close transition and handles or delegates desktop task close animation. */
105     override fun startRemoveTransition(wct: WindowContainerTransaction?): IBinder {
106         if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX.isTrue) {
107             return freeformTaskTransitionHandler.startRemoveTransition(wct)
108         }
109         requireNotNull(wct)
110         return transitions
111             .startTransition(WindowManager.TRANSIT_CLOSE, wct, /* handler= */ this)
112             .also { transition ->
113                 pendingMixedTransitions.add(PendingMixedTransition.Close(transition))
114             }
115     }
116 
117     /**
118      * Starts a launch transition for [taskId], with an optional [exitingImmersiveTask] if it was
119      * included in the [wct] and is expected to be animated by this handler.
120      */
121     fun startLaunchTransition(
122         @WindowManager.TransitionType transitionType: Int,
123         wct: WindowContainerTransaction,
124         taskId: Int?,
125         minimizingTaskId: Int? = null,
126         exitingImmersiveTask: Int? = null,
127     ): IBinder {
128         if (
129             !DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue &&
130                 !DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX.isTrue
131         ) {
132             return transitions.startTransition(transitionType, wct, /* handler= */ null)
133         }
134         if (exitingImmersiveTask == null) {
135             logV("Starting mixed launch transition for task#%d", taskId)
136         } else {
137             logV(
138                 "Starting mixed launch transition for task#%d with immersive exit of task#%d",
139                 taskId,
140                 exitingImmersiveTask,
141             )
142         }
143         return transitions.startTransition(transitionType, wct, /* handler= */ this).also {
144             transition ->
145             pendingMixedTransitions.add(
146                 PendingMixedTransition.Launch(
147                     transition = transition,
148                     launchingTask = taskId,
149                     minimizingTask = minimizingTaskId,
150                     exitingImmersiveTask = exitingImmersiveTask,
151                 )
152             )
153         }
154     }
155 
156     /** Notifies this handler that there is a pending transition for it to handle. */
157     fun addPendingMixedTransition(pendingMixedTransition: PendingMixedTransition) {
158         pendingMixedTransitions.add(pendingMixedTransition)
159     }
160 
161     /** Returns null, as it only handles transitions started from Shell. */
162     override fun handleRequest(
163         transition: IBinder,
164         request: TransitionRequestInfo,
165     ): WindowContainerTransaction? = null
166 
167     override fun startAnimation(
168         transition: IBinder,
169         info: TransitionInfo,
170         startTransaction: SurfaceControl.Transaction,
171         finishTransaction: SurfaceControl.Transaction,
172         finishCallback: TransitionFinishCallback,
173     ): Boolean {
174         val pending =
175             pendingMixedTransitions.find { pending -> pending.transition == transition }
176                 ?: return false.also { logV("No pending desktop transition") }
177         pendingMixedTransitions.remove(pending)
178         logV("Animating pending mixed transition: %s", pending)
179         return when (pending) {
180             is PendingMixedTransition.Close ->
181                 animateCloseTransition(
182                     transition,
183                     info,
184                     startTransaction,
185                     finishTransaction,
186                     finishCallback,
187                 )
188             is PendingMixedTransition.Launch ->
189                 animateLaunchTransition(
190                     pending,
191                     transition,
192                     info,
193                     startTransaction,
194                     finishTransaction,
195                     finishCallback,
196                 )
197             is PendingMixedTransition.Minimize ->
198                 animateMinimizeTransition(
199                     pending,
200                     transition,
201                     info,
202                     startTransaction,
203                     finishTransaction,
204                     finishCallback,
205                 )
206         }
207     }
208 
209     private fun animateCloseTransition(
210         transition: IBinder,
211         info: TransitionInfo,
212         startTransaction: SurfaceControl.Transaction,
213         finishTransaction: SurfaceControl.Transaction,
214         finishCallback: TransitionFinishCallback,
215     ): Boolean {
216         val closeChange = findCloseDesktopTaskChange(info)
217         if (closeChange == null) {
218             logW("Should have closing desktop task")
219             return false
220         }
221         if (isWallpaperActivityClosing(info)) {
222             // If the wallpaper activity is closing then the desktop is closing, animate the closing
223             // desktop by dispatching to other transition handlers.
224             return dispatchCloseLastDesktopTaskAnimation(
225                 transition,
226                 info,
227                 startTransaction,
228                 finishTransaction,
229                 finishCallback,
230             )
231         }
232         // Animate close desktop task transition with [CloseDesktopTaskTransitionHandler].
233         return closeDesktopTaskTransitionHandler.startAnimation(
234             transition,
235             info,
236             startTransaction,
237             finishTransaction,
238             finishCallback,
239         )
240     }
241 
242     private fun animateLaunchTransition(
243         pending: PendingMixedTransition.Launch,
244         transition: IBinder,
245         info: TransitionInfo,
246         startTransaction: SurfaceControl.Transaction,
247         finishTransaction: SurfaceControl.Transaction,
248         finishCallback: TransitionFinishCallback,
249     ): Boolean {
250         // Check if there's also an immersive change during this launch.
251         val immersiveExitChange =
252             pending.exitingImmersiveTask?.let { exitingTask -> findTaskChange(info, exitingTask) }
253         val minimizeChange =
254             pending.minimizingTask?.let { minimizingTask -> findTaskChange(info, minimizingTask) }
255         val launchChange = findDesktopTaskLaunchChange(info, pending.launchingTask)
256         if (launchChange == null) {
257             check(immersiveExitChange == null)
258             logV("No launch Change, returning")
259             return false
260         }
261 
262         var subAnimationCount = -1
263         var combinedWct: WindowContainerTransaction? = null
264         val finishCb = TransitionFinishCallback { wct ->
265             --subAnimationCount
266             combinedWct = combinedWct.merge(wct)
267             if (subAnimationCount > 0) return@TransitionFinishCallback
268             finishCallback.onTransitionFinished(combinedWct)
269         }
270 
271         logV(
272             "Animating mixed launch transition task#%d, minimizingTask#%s immersiveExitTask#%s",
273             launchChange.taskInfo!!.taskId,
274             minimizeChange?.taskInfo?.taskId,
275             immersiveExitChange?.taskInfo?.taskId,
276         )
277         if (DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX.isTrue) {
278             // Only apply minimize change reparenting here if we implement the new app launch
279             // transitions, otherwise this reparenting is handled in the default handler.
280             minimizeChange?.let {
281                 applyMinimizeChangeReparenting(info, minimizeChange, startTransaction)
282             }
283         }
284         if (immersiveExitChange != null) {
285             subAnimationCount = 2
286             // Animate the immersive exit change separately.
287             info.changes.remove(immersiveExitChange)
288             desktopImmersiveController.animateResizeChange(
289                 immersiveExitChange,
290                 startTransaction,
291                 finishTransaction,
292                 finishCb,
293             )
294             // Let the leftover/default handler animate the remaining changes.
295             return dispatchToLeftoverHandler(
296                 transition,
297                 info,
298                 startTransaction,
299                 finishTransaction,
300                 finishCb,
301             )
302         }
303         // There's nothing to animate separately, so let the left over handler animate
304         // the entire transition.
305         subAnimationCount = 1
306         return dispatchToLeftoverHandler(
307             transition,
308             info,
309             startTransaction,
310             finishTransaction,
311             finishCb,
312         )
313     }
314 
315     private fun animateMinimizeTransition(
316         pending: PendingMixedTransition.Minimize,
317         transition: IBinder,
318         info: TransitionInfo,
319         startTransaction: SurfaceControl.Transaction,
320         finishTransaction: SurfaceControl.Transaction,
321         finishCallback: TransitionFinishCallback,
322     ): Boolean {
323         val shouldAnimate =
324             if (info.type == Transitions.TRANSIT_MINIMIZE) {
325                 DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX.isTrue
326             } else {
327                 DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue
328             }
329         if (!shouldAnimate) {
330             return false
331         }
332 
333         val minimizeChange = findTaskChange(info, pending.minimizingTask)
334         if (minimizeChange == null) {
335             logW("Should have minimizing desktop task")
336             return false
337         }
338         if (pending.isLastTask) {
339             // Dispatch close desktop task animation to the default transition handlers.
340             return dispatchToLeftoverHandler(
341                 transition,
342                 info,
343                 startTransaction,
344                 finishTransaction,
345                 finishCallback,
346             )
347         }
348 
349         // Animate minimizing desktop task transition with [DesktopMinimizationTransitionHandler].
350         return desktopMinimizationTransitionHandler.startAnimation(
351             transition,
352             info,
353             startTransaction,
354             finishTransaction,
355             finishCallback,
356         )
357     }
358 
359     override fun onTransitionConsumed(
360         transition: IBinder,
361         aborted: Boolean,
362         finishTransaction: SurfaceControl.Transaction?,
363     ) {
364         pendingMixedTransitions.removeAll { pending -> pending.transition == transition }
365         super.onTransitionConsumed(transition, aborted, finishTransaction)
366     }
367 
368     /**
369      * Dispatch close desktop task animation to the default transition handlers. Allows delegating
370      * it to Launcher to animate in sync with show Home transition.
371      */
372     private fun dispatchCloseLastDesktopTaskAnimation(
373         transition: IBinder,
374         info: TransitionInfo,
375         startTransaction: SurfaceControl.Transaction,
376         finishTransaction: SurfaceControl.Transaction,
377         finishCallback: TransitionFinishCallback,
378     ): Boolean {
379         // Dispatch the last desktop task closing animation.
380         return dispatchToLeftoverHandler(
381             transition = transition,
382             info = info,
383             startTransaction = startTransaction,
384             finishTransaction = finishTransaction,
385             finishCallback = finishCallback,
386         )
387     }
388 
389     /**
390      * Reparent the minimizing task back to its root display area.
391      *
392      * During the launch/minimize animation the all animated tasks will be reparented to a
393      * transition leash shown in front of other desktop tasks. Reparenting the minimizing task back
394      * to its root display area ensures that task stays behind other desktop tasks during the
395      * animation.
396      */
397     private fun applyMinimizeChangeReparenting(
398         info: TransitionInfo,
399         minimizeChange: Change,
400         startTransaction: SurfaceControl.Transaction,
401     ) {
402         require(TransitionUtil.isOpeningMode(info.type))
403         require(minimizeChange.taskInfo != null)
404         val taskInfo = minimizeChange.taskInfo!!
405         require(taskInfo.isFreeform)
406         logV("Reparenting minimizing task#%d", taskInfo.taskId)
407         rootTaskDisplayAreaOrganizer.reparentToDisplayArea(
408             taskInfo.displayId,
409             minimizeChange.leash,
410             startTransaction,
411         )
412     }
413 
414     private fun dispatchToLeftoverHandler(
415         transition: IBinder,
416         info: TransitionInfo,
417         startTransaction: SurfaceControl.Transaction,
418         finishTransaction: SurfaceControl.Transaction,
419         finishCallback: TransitionFinishCallback,
420         doOnFinishCallback: (() -> Unit)? = null,
421     ): Boolean {
422         return transitions.dispatchTransition(
423             transition,
424             info,
425             startTransaction,
426             finishTransaction,
427             { wct ->
428                 doOnFinishCallback?.invoke()
429                 finishCallback.onTransitionFinished(wct)
430             },
431             /* skip= */ this,
432         ) != null
433     }
434 
435     private fun isWallpaperActivityClosing(info: TransitionInfo) =
436         info.changes.any { change ->
437             TransitionUtil.isClosingMode(change.mode) &&
438                 change.taskInfo != null &&
439                 DesktopWallpaperActivity.isWallpaperTask(change.taskInfo!!)
440         }
441 
442     private fun findCloseDesktopTaskChange(info: TransitionInfo): TransitionInfo.Change? {
443         if (info.type != WindowManager.TRANSIT_CLOSE) return null
444         return info.changes.firstOrNull { change ->
445             change.mode == WindowManager.TRANSIT_CLOSE &&
446                 !change.hasFlags(TransitionInfo.FLAG_IS_WALLPAPER) &&
447                 change.taskInfo?.taskId != INVALID_TASK_ID &&
448                 change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM
449         }
450     }
451 
452     private fun findTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? =
453         info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId }
454 
455     private fun findLaunchChange(info: TransitionInfo): TransitionInfo.Change? =
456         info.changes.firstOrNull { change ->
457             change.mode == TRANSIT_OPEN && change.taskInfo != null && change.taskInfo!!.isFreeform
458         }
459 
460     private fun findDesktopTaskLaunchChange(
461         info: TransitionInfo,
462         launchTaskId: Int?,
463     ): TransitionInfo.Change? {
464         return if (launchTaskId != null) {
465             // Launching a known task (probably from background or moving to front), so
466             // specifically look for it.
467             val launchChange = findTaskChange(info, launchTaskId)
468             if (
469                 DesktopModeFlags.ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX.isTrue &&
470                     launchChange == null
471             ) {
472                 findLaunchChange(info)
473             } else {
474                 launchChange
475             }
476         } else {
477             // Launching a new task, so the first opening freeform task.
478             findLaunchChange(info)
479         }
480     }
481 
482     private fun WindowContainerTransaction?.merge(
483         wct: WindowContainerTransaction?
484     ): WindowContainerTransaction? {
485         if (wct == null) return this
486         if (this == null) return wct
487         return this.merge(wct)
488     }
489 
490     /** A scheduled transition that will potentially be animated by more than one handler */
491     sealed class PendingMixedTransition {
492         abstract val transition: IBinder
493 
494         /** A task is closing. */
495         data class Close(override val transition: IBinder) : PendingMixedTransition()
496 
497         /** A task is opening or moving to front. */
498         data class Launch(
499             override val transition: IBinder,
500             val launchingTask: Int?,
501             val minimizingTask: Int?,
502             val exitingImmersiveTask: Int?,
503         ) : PendingMixedTransition()
504 
505         /**
506          * A task is minimizing. This should be used for task going to back and some closing cases
507          * with back navigation.
508          */
509         data class Minimize(
510             override val transition: IBinder,
511             val minimizingTask: Int,
512             val isLastTask: Boolean,
513         ) : PendingMixedTransition()
514     }
515 
516     private fun logV(msg: String, vararg arguments: Any?) {
517         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
518     }
519 
520     private fun logW(msg: String, vararg arguments: Any?) {
521         ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
522     }
523 
524     companion object {
525         private const val TAG = "DesktopMixedTransitionHandler"
526     }
527 }
528