• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 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.animation.Animator
20 import android.animation.RectEvaluator
21 import android.animation.ValueAnimator
22 import android.graphics.Rect
23 import android.os.IBinder
24 import android.view.Choreographer
25 import android.view.SurfaceControl
26 import android.view.WindowManager.TRANSIT_CHANGE
27 import android.window.TransitionInfo
28 import android.window.TransitionRequestInfo
29 import android.window.WindowContainerTransaction
30 import androidx.core.animation.addListener
31 import com.android.internal.jank.Cuj
32 import com.android.internal.jank.InteractionJankMonitor
33 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE
34 import com.android.wm.shell.transition.Transitions
35 import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
36 import java.util.function.Supplier
37 
38 /** Handles the animation of quick resizing of desktop tasks. */
39 class ToggleResizeDesktopTaskTransitionHandler(
40     private val transitions: Transitions,
41     private val transactionSupplier: Supplier<SurfaceControl.Transaction>,
42     private val interactionJankMonitor: InteractionJankMonitor,
43 ) : Transitions.TransitionHandler {
44 
45     private val rectEvaluator = RectEvaluator(Rect())
46     private lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener
47 
48     private var boundsAnimator: Animator? = null
49     private var initialBounds: Rect? = null
50     private var callback: (() -> Unit)? = null
51 
52     constructor(
53         transitions: Transitions,
54         interactionJankMonitor: InteractionJankMonitor,
55     ) : this(transitions, Supplier { SurfaceControl.Transaction() }, interactionJankMonitor)
56 
57     /**
58      * Starts a quick resize transition.
59      *
60      * @param wct WindowContainerTransaction that will update core about the task changes applied
61      * @param taskLeashBounds current bounds of the task leash (Note: not guaranteed to be the
62      *   bounds of the actual task). This is provided so that the animation resizing can begin where
63      *   the task leash currently is for smoother UX.
64      */
65     fun startTransition(
66         wct: WindowContainerTransaction,
67         taskLeashBounds: Rect? = null,
68         callback: (() -> Unit)? = null,
69     ) {
70         transitions.startTransition(TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE, wct, this)
71         initialBounds = taskLeashBounds
72         this.callback = callback
73     }
74 
75     fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) {
76         onTaskResizeAnimationListener = listener
77     }
78 
79     override fun startAnimation(
80         transition: IBinder,
81         info: TransitionInfo,
82         startTransaction: SurfaceControl.Transaction,
83         finishTransaction: SurfaceControl.Transaction,
84         finishCallback: Transitions.TransitionFinishCallback,
85     ): Boolean {
86         val change = findRelevantChange(info)
87         val leash = change.leash
88         val taskId = checkNotNull(change.taskInfo).taskId
89         val startBounds = initialBounds ?: change.startAbsBounds
90         val endBounds = change.endAbsBounds
91 
92         val tx = transactionSupplier.get()
93         boundsAnimator?.cancel()
94         boundsAnimator =
95             ValueAnimator.ofObject(rectEvaluator, startBounds, endBounds)
96                 .setDuration(RESIZE_DURATION_MS)
97                 .apply {
98                     addListener(
99                         onStart = {
100                             startTransaction
101                                 .setPosition(
102                                     leash,
103                                     startBounds.left.toFloat(),
104                                     startBounds.top.toFloat(),
105                                 )
106                                 .setWindowCrop(leash, startBounds.width(), startBounds.height())
107                                 .show(leash)
108                             onTaskResizeAnimationListener.onAnimationStart(
109                                 taskId,
110                                 startTransaction,
111                                 startBounds,
112                             )
113                         },
114                         onEnd = {
115                             finishTransaction
116                                 .setPosition(
117                                     leash,
118                                     endBounds.left.toFloat(),
119                                     endBounds.top.toFloat(),
120                                 )
121                                 .setWindowCrop(leash, endBounds.width(), endBounds.height())
122                                 .show(leash)
123                             onTaskResizeAnimationListener.onAnimationEnd(taskId)
124                             finishCallback.onTransitionFinished(null)
125                             initialBounds = null
126                             boundsAnimator = null
127                             interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW)
128                             interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW)
129                             interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE)
130                             callback?.invoke()
131                             callback = null
132                         },
133                     )
134                     addUpdateListener { anim ->
135                         val rect = anim.animatedValue as Rect
136                         tx.setPosition(leash, rect.left.toFloat(), rect.top.toFloat())
137                             .setWindowCrop(leash, rect.width(), rect.height())
138                             .show(leash)
139                             .setFrameTimeline(Choreographer.getInstance().getVsyncId())
140                         onTaskResizeAnimationListener.onBoundsChange(taskId, tx, rect)
141                     }
142                     start()
143                 }
144         return true
145     }
146 
147     override fun handleRequest(
148         transition: IBinder,
149         request: TransitionRequestInfo,
150     ): WindowContainerTransaction? {
151         return null
152     }
153 
154     private fun findRelevantChange(info: TransitionInfo): TransitionInfo.Change {
155         val matchingChanges =
156             info.changes.filter { c ->
157                 !isWallpaper(c) && isValidTaskChange(c) && c.mode == TRANSIT_CHANGE
158             }
159         if (matchingChanges.size != 1) {
160             throw IllegalStateException(
161                 "Expected 1 relevant change but found: ${matchingChanges.size}"
162             )
163         }
164         return matchingChanges.first()
165     }
166 
167     private fun isWallpaper(change: TransitionInfo.Change): Boolean =
168         (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0
169 
170     private fun isValidTaskChange(change: TransitionInfo.Change): Boolean =
171         change.taskInfo != null && change.taskInfo?.taskId != -1
172 
173     companion object {
174         private const val RESIZE_DURATION_MS = 300L
175     }
176 }
177