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 package com.android.wm.shell.back
17 
18 import android.content.Context
19 import android.graphics.Rect
20 import android.graphics.RectF
21 import android.util.MathUtils
22 import android.view.SurfaceControl
23 import android.view.animation.Animation
24 import android.view.animation.Transformation
25 import android.window.BackEvent
26 import android.window.BackMotionEvent
27 import android.window.BackNavigationInfo
28 import com.android.internal.R
29 import com.android.internal.policy.TransitionAnimation
30 import com.android.internal.protolog.common.ProtoLog
31 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
32 import com.android.wm.shell.protolog.ShellProtoLogGroup
33 import javax.inject.Inject
34 import kotlin.math.max
35 import kotlin.math.min
36 
37 /** Class that handles customized predictive cross activity back animations. */
38 class CustomCrossActivityBackAnimation(
39     context: Context,
40     background: BackAnimationBackground,
41     rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
42     transaction: SurfaceControl.Transaction,
43     private val customAnimationLoader: CustomAnimationLoader
44 ) :
45     CrossActivityBackAnimation(
46         context,
47         background,
48         rootTaskDisplayAreaOrganizer,
49         transaction
50     ) {
51 
52     private var enterAnimation: Animation? = null
53     private var closeAnimation: Animation? = null
54     private val transformation = Transformation()
55 
56     override val allowEnteringYShift = false
57 
58     @Inject
59     constructor(
60         context: Context,
61         background: BackAnimationBackground,
62         rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
63     ) : this(
64         context,
65         background,
66         rootTaskDisplayAreaOrganizer,
67         SurfaceControl.Transaction(),
68         CustomAnimationLoader(
69             TransitionAnimation(context, false /* debug */, "CustomCrossActivityBackAnimation")
70         )
71     )
72 
73     override fun preparePreCommitClosingRectMovement(swipeEdge: Int) {
74         startClosingRect.set(backAnimRect)
75 
76         // scale closing target to the left for right-hand-swipe and to the right for
77         // left-hand-swipe
78         targetClosingRect.set(startClosingRect)
79         targetClosingRect.scaleCentered(MAX_SCALE)
80         val offset = if (swipeEdge != BackEvent.EDGE_RIGHT) {
81             startClosingRect.right - targetClosingRect.right - displayBoundsMargin
82         } else {
83             -targetClosingRect.left + displayBoundsMargin
84         }
85         targetClosingRect.offset(offset, 0f)
86     }
87 
88     override fun preparePreCommitEnteringRectMovement() {
89         // No movement for the entering rect
90         startEnteringRect.set(startClosingRect)
91         targetEnteringRect.set(startClosingRect)
92     }
93 
94     override fun getPostCommitAnimationDuration(): Long {
95         return min(
96             MAX_POST_COMMIT_ANIM_DURATION, max(closeAnimation!!.duration, enterAnimation!!.duration)
97         )
98     }
99 
100     override fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation {
101         transformation.clear()
102         enterAnimation!!.getTransformationAt(progress * PRE_COMMIT_MAX_PROGRESS, transformation)
103         return transformation
104     }
105 
106     override fun startBackAnimation(backMotionEvent: BackMotionEvent) {
107         super.startBackAnimation(backMotionEvent)
108         if (
109             closeAnimation == null ||
110             enterAnimation == null ||
111             closingTarget == null ||
112             enteringTarget == null
113         ) {
114             ProtoLog.d(
115                 ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
116                 "Enter animation or close animation is null."
117             )
118             return
119         }
120         initializeAnimation(closeAnimation!!, closingTarget!!.localBounds)
121         initializeAnimation(enterAnimation!!, enteringTarget!!.localBounds)
122     }
123 
124     override fun onPostCommitProgress(linearProgress: Float) {
125         super.onPostCommitProgress(linearProgress)
126         if (closingTarget == null || enteringTarget == null) return
127 
128         val closingProgress = closeAnimation!!.getPostCommitProgress(linearProgress)
129         applyTransform(
130             closingTarget!!.leash,
131             currentClosingRect,
132             closingProgress,
133             closeAnimation!!,
134             FlingMode.FLING_SHRINK
135         )
136         val enteringProgress = MathUtils.lerp(
137             gestureProgress * PRE_COMMIT_MAX_PROGRESS,
138             1f,
139             enterAnimation!!.getPostCommitProgress(linearProgress)
140         )
141         applyTransform(
142             enteringTarget!!.leash,
143             currentEnteringRect,
144             enteringProgress,
145             enterAnimation!!,
146             FlingMode.NO_FLING
147         )
148         applyTransaction()
149     }
150 
151     private fun applyTransform(
152         leash: SurfaceControl,
153         rect: RectF,
154         progress: Float,
155         animation: Animation,
156         flingMode: FlingMode
157     ) {
158         transformation.clear()
159         animation.getTransformationAt(progress, transformation)
160         applyTransform(leash, rect, transformation.alpha, transformation, flingMode)
161     }
162 
163     override fun finishAnimation() {
164         closeAnimation?.reset()
165         closeAnimation = null
166         enterAnimation?.reset()
167         enterAnimation = null
168         transformation.clear()
169         super.finishAnimation()
170     }
171 
172     /** Load customize animation before animation start. */
173     override fun prepareNextAnimation(
174         animationInfo: BackNavigationInfo.CustomAnimationInfo?,
175         letterboxColor: Int
176     ): Boolean {
177         super.prepareNextAnimation(animationInfo, letterboxColor)
178         if (animationInfo == null) return false
179         customAnimationLoader.loadAll(animationInfo)?.let { result ->
180             closeAnimation = result.closeAnimation
181             enterAnimation = result.enterAnimation
182             customizedBackgroundColor = result.backgroundColor
183             return true
184         }
185         return false
186     }
187 
188     private fun Animation.getPostCommitProgress(linearProgress: Float): Float {
189         return when (duration) {
190             0L -> 1f
191             else -> min(
192                 1f,
193                 getPostCommitAnimationDuration() / min(
194                     MAX_POST_COMMIT_ANIM_DURATION,
195                     duration
196                 ).toFloat() * linearProgress
197             )
198         }
199     }
200 
201     class AnimationLoadResult {
202         var closeAnimation: Animation? = null
203         var enterAnimation: Animation? = null
204         var backgroundColor = 0
205     }
206 
207     companion object {
208         private const val PRE_COMMIT_MAX_PROGRESS = 0.2f
209         private const val MAX_POST_COMMIT_ANIM_DURATION = 2000L
210     }
211 }
212 
213 /** Helper class to load custom animation. */
214 class CustomAnimationLoader(private val transitionAnimation: TransitionAnimation) {
215 
216     /**
217      * Load both enter and exit animation for the close activity transition. Note that the result is
218      * only valid if the exit animation has set and loaded success. If the entering animation has
219      * not set(i.e. 0), here will load the default entering animation for it.
220      *
221      * @param animationInfo The information of customize animation, which can be set from
222      *   [Activity.overrideActivityTransition] and/or [LayoutParams.windowAnimations]
223      */
loadAllnull224     fun loadAll(
225         animationInfo: BackNavigationInfo.CustomAnimationInfo
226     ): CustomCrossActivityBackAnimation.AnimationLoadResult? {
227         if (animationInfo.packageName.isEmpty()) return null
228         val close = loadAnimation(animationInfo, false) ?: return null
229         val open = loadAnimation(animationInfo, true)
230         val result = CustomCrossActivityBackAnimation.AnimationLoadResult()
231         result.closeAnimation = close
232         result.enterAnimation = open
233         result.backgroundColor = animationInfo.customBackground
234         return result
235     }
236 
237     /**
238      * Load enter or exit animation from CustomAnimationInfo
239      *
240      * @param animationInfo The information for customize animation.
241      * @param enterAnimation true when load for enter animation, false for exit animation.
242      * @return Loaded animation.
243      */
loadAnimationnull244     fun loadAnimation(
245         animationInfo: BackNavigationInfo.CustomAnimationInfo,
246         enterAnimation: Boolean
247     ): Animation? {
248         var a: Animation? = null
249         // Activity#overrideActivityTransition has higher priority than windowAnimations
250         // Try to get animation from Activity#overrideActivityTransition
251         if (
252             enterAnimation && animationInfo.customEnterAnim != 0 ||
253             !enterAnimation && animationInfo.customExitAnim != 0
254         ) {
255             a =
256                 transitionAnimation.loadAppTransitionAnimation(
257                     animationInfo.packageName,
258                     if (enterAnimation) animationInfo.customEnterAnim
259                     else animationInfo.customExitAnim
260                 )
261         } else if (animationInfo.windowAnimations != 0) {
262             // try to get animation from LayoutParams#windowAnimations
263             a =
264                 transitionAnimation.loadAnimationAttr(
265                     animationInfo.packageName,
266                     animationInfo.windowAnimations,
267                     if (enterAnimation) R.styleable.WindowAnimation_activityCloseEnterAnimation
268                     else R.styleable.WindowAnimation_activityCloseExitAnimation,
269                     false /* translucent */
270                 )
271         }
272         // Only allow to load default animation for opening target.
273         if (a == null && enterAnimation) {
274             a = loadDefaultOpenAnimation()
275         }
276         if (a != null) {
277             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "custom animation loaded %s", a)
278         } else {
279             ProtoLog.e(ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "No custom animation loaded")
280         }
281         return a
282     }
283 
loadDefaultOpenAnimationnull284     private fun loadDefaultOpenAnimation(): Animation? {
285         return transitionAnimation.loadDefaultAnimationAttr(
286             R.styleable.WindowAnimation_activityCloseEnterAnimation,
287             false /* translucent */
288         )
289     }
290 }
291 
initializeAnimationnull292 private fun initializeAnimation(animation: Animation, bounds: Rect) {
293     val width = bounds.width()
294     val height = bounds.height()
295     animation.initialize(width, height, width, height)
296 }
297