1 /*
2  * 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.util.SparseArray
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.wm.shell.transition.Transitions
32 import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE
33 import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
34 import java.util.function.Supplier
35 
36 /** Handles the animation of quick resizing of desktop tasks. */
37 class ToggleResizeDesktopTaskTransitionHandler(
38     private val transitions: Transitions,
39     private val transactionSupplier: Supplier<SurfaceControl.Transaction>
40 ) : Transitions.TransitionHandler {
41 
42     private val rectEvaluator = RectEvaluator(Rect())
43     private val taskToDecorationMap = SparseArray<DesktopModeWindowDecoration>()
44 
45     private var boundsAnimator: Animator? = null
46 
47     constructor(
48         transitions: Transitions
49     ) : this(transitions, Supplier { SurfaceControl.Transaction() })
50 
51     /** Starts a quick resize transition. */
52     fun startTransition(
53         wct: WindowContainerTransaction,
54         taskId: Int,
55         windowDecoration: DesktopModeWindowDecoration
56     ) {
57         // Pause relayout until the transition animation finishes.
58         windowDecoration.incrementRelayoutBlock()
59         transitions.startTransition(TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE, wct, this)
60         taskToDecorationMap.put(taskId, windowDecoration)
61     }
62 
63     override fun startAnimation(
64         transition: IBinder,
65         info: TransitionInfo,
66         startTransaction: SurfaceControl.Transaction,
67         finishTransaction: SurfaceControl.Transaction,
68         finishCallback: Transitions.TransitionFinishCallback
69     ): Boolean {
70         val change = findRelevantChange(info)
71         val leash = change.leash
72         val taskId = checkNotNull(change.taskInfo).taskId
73         val startBounds = change.startAbsBounds
74         val endBounds = change.endAbsBounds
75         val windowDecor =
76             taskToDecorationMap.removeReturnOld(taskId)
77                 ?: throw IllegalStateException("Window decoration not found for task $taskId")
78 
79         val tx = transactionSupplier.get()
80         boundsAnimator?.cancel()
81         boundsAnimator =
82             ValueAnimator.ofObject(rectEvaluator, startBounds, endBounds)
83                 .setDuration(RESIZE_DURATION_MS)
84                 .apply {
85                     addListener(
86                         onStart = {
87                             startTransaction
88                                 .setPosition(
89                                     leash,
90                                     startBounds.left.toFloat(),
91                                     startBounds.top.toFloat()
92                                 )
93                                 .setWindowCrop(leash, startBounds.width(), startBounds.height())
94                                 .show(leash)
95                             windowDecor.showResizeVeil(startTransaction, startBounds)
96                         },
97                         onEnd = {
98                             finishTransaction
99                                 .setPosition(
100                                     leash,
101                                     endBounds.left.toFloat(),
102                                     endBounds.top.toFloat()
103                                 )
104                                 .setWindowCrop(leash, endBounds.width(), endBounds.height())
105                                 .show(leash)
106                             windowDecor.hideResizeVeil()
107                             finishCallback.onTransitionFinished(null)
108                             boundsAnimator = null
109                         }
110                     )
111                     addUpdateListener { anim ->
112                         val rect = anim.animatedValue as Rect
113                         tx.setPosition(leash, rect.left.toFloat(), rect.top.toFloat())
114                             .setWindowCrop(leash, rect.width(), rect.height())
115                             .show(leash)
116                         windowDecor.updateResizeVeil(tx, rect)
117                     }
118                     start()
119                 }
120         return true
121     }
122 
123     override fun handleRequest(
124         transition: IBinder,
125         request: TransitionRequestInfo
126     ): WindowContainerTransaction? {
127         return null
128     }
129 
130     private fun findRelevantChange(info: TransitionInfo): TransitionInfo.Change {
131         val matchingChanges =
132             info.changes.filter { c ->
133                 !isWallpaper(c) && isValidTaskChange(c) && c.mode == TRANSIT_CHANGE
134             }
135         if (matchingChanges.size != 1) {
136             throw IllegalStateException(
137                 "Expected 1 relevant change but found: ${matchingChanges.size}"
138             )
139         }
140         return matchingChanges.first()
141     }
142 
143     private fun isWallpaper(change: TransitionInfo.Change): Boolean {
144         return (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0
145     }
146 
147     private fun isValidTaskChange(change: TransitionInfo.Change): Boolean {
148         return change.taskInfo != null && change.taskInfo?.taskId != -1
149     }
150 
151     companion object {
152         private const val RESIZE_DURATION_MS = 300L
153     }
154 }
155