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.back
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.ValueAnimator
22 import android.content.Context
23 import android.content.res.Configuration
24 import android.graphics.Color
25 import android.graphics.Matrix
26 import android.graphics.PointF
27 import android.graphics.Rect
28 import android.graphics.RectF
29 import android.os.RemoteException
30 import android.util.TimeUtils
31 import android.view.Choreographer
32 import android.view.Display
33 import android.view.IRemoteAnimationFinishedCallback
34 import android.view.IRemoteAnimationRunner
35 import android.view.RemoteAnimationTarget
36 import android.view.SurfaceControl
37 import android.view.animation.DecelerateInterpolator
38 import android.view.animation.Interpolator
39 import android.view.animation.Transformation
40 import android.window.BackEvent
41 import android.window.BackMotionEvent
42 import android.window.BackNavigationInfo
43 import android.window.BackProgressAnimator
44 import android.window.IOnBackInvokedCallback
45 import com.android.internal.dynamicanimation.animation.FloatValueHolder
46 import com.android.internal.dynamicanimation.animation.SpringAnimation
47 import com.android.internal.dynamicanimation.animation.SpringForce
48 import com.android.internal.jank.Cuj
49 import com.android.internal.policy.ScreenDecorationsUtils
50 import com.android.internal.policy.SystemBarUtils
51 import com.android.internal.protolog.common.ProtoLog
52 import com.android.wm.shell.R
53 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
54 import com.android.wm.shell.animation.Interpolators
55 import com.android.wm.shell.protolog.ShellProtoLogGroup
56 import kotlin.math.abs
57 import kotlin.math.max
58 import kotlin.math.min
59 
60 abstract class CrossActivityBackAnimation(
61     private val context: Context,
62     private val background: BackAnimationBackground,
63     private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
64     protected val transaction: SurfaceControl.Transaction
65 ) : ShellBackAnimation() {
66 
67     protected val startClosingRect = RectF()
68     protected val targetClosingRect = RectF()
69     protected val currentClosingRect = RectF()
70 
71     protected val startEnteringRect = RectF()
72     protected val targetEnteringRect = RectF()
73     protected val currentEnteringRect = RectF()
74 
75     protected val backAnimRect = Rect()
76     private val cropRect = Rect()
77     private val tempRectF = RectF()
78 
79     private var cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
80     private var statusbarHeight = SystemBarUtils.getStatusBarHeight(context)
81 
82     private val backAnimationRunner =
83         BackAnimationRunner(Callback(), Runner(), context, Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY)
84     private val initialTouchPos = PointF()
85     private val transformMatrix = Matrix()
86     private val tmpFloat9 = FloatArray(9)
87     protected var enteringTarget: RemoteAnimationTarget? = null
88     protected var closingTarget: RemoteAnimationTarget? = null
89     private var triggerBack = false
90     private var finishCallback: IRemoteAnimationFinishedCallback? = null
91     private val progressAnimator = BackProgressAnimator()
92     protected val displayBoundsMargin =
93         context.resources.getDimension(R.dimen.cross_task_back_vertical_margin)
94 
95     private val gestureInterpolator = Interpolators.BACK_GESTURE
96     private val verticalMoveInterpolator: Interpolator = DecelerateInterpolator()
97 
98     private var scrimLayer: SurfaceControl? = null
99     private var maxScrimAlpha: Float = 0f
100 
101     private var isLetterboxed = false
102     private var enteringHasSameLetterbox = false
103     private var leftLetterboxLayer: SurfaceControl? = null
104     private var rightLetterboxLayer: SurfaceControl? = null
105     private var letterboxColor: Int = 0
106 
107     private val postCommitFlingScale = FloatValueHolder(SPRING_SCALE)
108     private var lastPostCommitFlingScale = SPRING_SCALE
109     private val postCommitFlingSpring = SpringForce(SPRING_SCALE)
110             .setStiffness(SpringForce.STIFFNESS_LOW)
111             .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
112     protected var gestureProgress = 0f
113 
114     /** Background color to be used during the animation, also see [getBackgroundColor] */
115     protected var customizedBackgroundColor = 0
116 
117     /**
118      * Whether the entering target should be shifted vertically with the user gesture in pre-commit
119      */
120     abstract val allowEnteringYShift: Boolean
121 
122     /**
123      * Subclasses must set the [startClosingRect] and [targetClosingRect] to define the movement
124      * of the closingTarget during pre-commit phase.
125      */
126     abstract fun preparePreCommitClosingRectMovement(@BackEvent.SwipeEdge swipeEdge: Int)
127 
128     /**
129      * Subclasses must set the [startEnteringRect] and [targetEnteringRect] to define the movement
130      * of the enteringTarget during pre-commit phase.
131      */
132     abstract fun preparePreCommitEnteringRectMovement()
133 
134     /**
135      * Subclasses must provide a duration (in ms) for the post-commit part of the animation
136      */
137     abstract fun getPostCommitAnimationDuration(): Long
138 
139     /**
140      * Returns a base transformation to apply to the entering target during pre-commit. The system
141      * will apply the default animation on top of it.
142      */
143     protected open fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation? =
144         null
145 
146     override fun onConfigurationChanged(newConfiguration: Configuration) {
147         cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
148         statusbarHeight = SystemBarUtils.getStatusBarHeight(context)
149     }
150 
151     override fun getRunner() = backAnimationRunner
152 
153     private fun getBackgroundColor(): Int =
154         when {
155             customizedBackgroundColor != 0 -> customizedBackgroundColor
156             isLetterboxed -> letterboxColor
157             enteringTarget != null -> enteringTarget!!.taskInfo.taskDescription!!.backgroundColor
158             else -> 0
159         }
160 
161     protected open fun startBackAnimation(backMotionEvent: BackMotionEvent) {
162         if (enteringTarget == null || closingTarget == null) {
163             ProtoLog.d(
164                 ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
165                 "Entering target or closing target is null."
166             )
167             return
168         }
169         triggerBack = backMotionEvent.triggerBack
170         initialTouchPos.set(backMotionEvent.touchX, backMotionEvent.touchY)
171 
172         transaction.setAnimationTransaction()
173         isLetterboxed = closingTarget!!.taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed
174         enteringHasSameLetterbox =
175             isLetterboxed && closingTarget!!.localBounds.equals(enteringTarget!!.localBounds)
176 
177         if (isLetterboxed && !enteringHasSameLetterbox) {
178             // Play animation with letterboxes, if closing and entering target have mismatching
179             // letterboxes
180             backAnimRect.set(closingTarget!!.windowConfiguration.bounds)
181         } else {
182             // otherwise play animation on localBounds only
183             backAnimRect.set(closingTarget!!.localBounds)
184         }
185         // Offset start rectangle to align task bounds.
186         backAnimRect.offsetTo(0, 0)
187 
188         preparePreCommitClosingRectMovement(backMotionEvent.swipeEdge)
189         preparePreCommitEnteringRectMovement()
190 
191         background.ensureBackground(
192             closingTarget!!.windowConfiguration.bounds,
193             getBackgroundColor(),
194             transaction,
195             statusbarHeight
196         )
197         ensureScrimLayer()
198         if (isLetterboxed && enteringHasSameLetterbox) {
199             // crop left and right letterboxes
200             cropRect.set(
201                 closingTarget!!.localBounds.left,
202                 0,
203                 closingTarget!!.localBounds.right,
204                 closingTarget!!.windowConfiguration.bounds.height()
205             )
206             // and add fake letterbox square surfaces instead
207             ensureLetterboxes()
208         } else {
209             cropRect.set(backAnimRect)
210         }
211         applyTransaction()
212     }
213 
214     private fun onGestureProgress(backEvent: BackEvent) {
215         val progress = gestureInterpolator.getInterpolation(backEvent.progress)
216         gestureProgress = progress
217         currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
218         val yOffset = getYOffset(currentClosingRect, backEvent.touchY)
219         currentClosingRect.offset(0f, yOffset)
220         applyTransform(closingTarget?.leash, currentClosingRect, 1f)
221         currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
222         if (allowEnteringYShift) currentEnteringRect.offset(0f, yOffset)
223         val enteringTransformation = getPreCommitEnteringBaseTransformation(progress)
224         applyTransform(
225             enteringTarget?.leash,
226             currentEnteringRect,
227             enteringTransformation?.alpha ?: 1f,
228             enteringTransformation
229         )
230         applyTransaction()
231         background.customizeStatusBarAppearance(currentClosingRect.top.toInt())
232     }
233 
234     private fun getYOffset(centeredRect: RectF, touchY: Float): Float {
235         val screenHeight = backAnimRect.height()
236         // Base the window movement in the Y axis on the touch movement in the Y axis.
237         val rawYDelta = touchY - initialTouchPos.y
238         val yDirection = (if (rawYDelta < 0) -1 else 1)
239         // limit yDelta interpretation to 1/2 of screen height in either direction
240         val deltaYRatio = min(screenHeight / 2f, abs(rawYDelta)) / (screenHeight / 2f)
241         val interpolatedYRatio: Float = verticalMoveInterpolator.getInterpolation(deltaYRatio)
242         // limit y-shift so surface never passes 8dp screen margin
243         val deltaY =
244             max(0f, (screenHeight - centeredRect.height()) / 2f - displayBoundsMargin) *
245                 interpolatedYRatio *
246                 yDirection
247         return deltaY
248     }
249 
250     protected open fun onGestureCommitted(velocity: Float) {
251         if (
252             closingTarget?.leash == null ||
253                 enteringTarget?.leash == null ||
254                 !enteringTarget!!.leash.isValid ||
255                 !closingTarget!!.leash.isValid
256         ) {
257             finishAnimation()
258             return
259         }
260 
261         // kick off spring animation with the current velocity from the pre-commit phase, this
262         // affects the scaling of the closing and/or opening activity during post-commit
263         val startVelocity =
264             if (gestureProgress < 0.1f) -DEFAULT_FLING_VELOCITY else -velocity * SPRING_SCALE
265         val flingAnimation = SpringAnimation(postCommitFlingScale, SPRING_SCALE)
266             .setStartVelocity(startVelocity.coerceIn(-MAX_FLING_VELOCITY, 0f))
267             .setStartValue(SPRING_SCALE)
268             .setSpring(postCommitFlingSpring)
269         flingAnimation.start()
270         // do an animation-frame immediately to prevent idle frame
271         flingAnimation.doAnimationFrame(
272             Choreographer.getInstance().lastFrameTimeNanos / TimeUtils.NANOS_PER_MS
273         )
274 
275         val valueAnimator =
276             ValueAnimator.ofFloat(1f, 0f).setDuration(getPostCommitAnimationDuration())
277         valueAnimator.addUpdateListener { animation: ValueAnimator ->
278             val progress = animation.animatedFraction
279             onPostCommitProgress(progress)
280             if (progress > 1 - BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD) {
281                 background.resetStatusBarCustomization()
282             }
283         }
284         valueAnimator.addListener(
285             object : AnimatorListenerAdapter() {
286                 override fun onAnimationEnd(animation: Animator) {
287                     background.resetStatusBarCustomization()
288                     finishAnimation()
289                 }
290             }
291         )
292         valueAnimator.start()
293     }
294 
295     protected open fun onPostCommitProgress(linearProgress: Float) {
296         scrimLayer?.let { transaction.setAlpha(it, maxScrimAlpha * (1f - linearProgress)) }
297     }
298 
299     protected open fun finishAnimation() {
300         enteringTarget?.let {
301             if (it.leash != null && it.leash.isValid) {
302                 transaction.setCornerRadius(it.leash, 0f)
303                 if (!triggerBack) transaction.setAlpha(it.leash, 0f)
304                 it.leash.release()
305             }
306             enteringTarget = null
307         }
308 
309         closingTarget?.leash?.release()
310         closingTarget = null
311 
312         background.removeBackground(transaction)
313         applyTransaction()
314         transformMatrix.reset()
315         initialTouchPos.set(0f, 0f)
316         try {
317             finishCallback?.onAnimationFinished()
318         } catch (e: RemoteException) {
319             e.printStackTrace()
320         }
321         finishCallback = null
322         removeScrimLayer()
323         removeLetterbox()
324         isLetterboxed = false
325         enteringHasSameLetterbox = false
326         lastPostCommitFlingScale = SPRING_SCALE
327         gestureProgress = 0f
328         triggerBack = false
329     }
330 
331     protected fun applyTransform(
332         leash: SurfaceControl?,
333         rect: RectF,
334         alpha: Float,
335         baseTransformation: Transformation? = null,
336         flingMode: FlingMode = FlingMode.NO_FLING
337     ) {
338         if (leash == null || !leash.isValid) return
339         tempRectF.set(rect)
340         if (flingMode != FlingMode.NO_FLING) {
341             lastPostCommitFlingScale = min(
342                 postCommitFlingScale.value / SPRING_SCALE,
343                 if (flingMode == FlingMode.FLING_BOUNCE) 1f else lastPostCommitFlingScale
344             )
345             // apply an additional scale to the closing target to account for fling velocity
346             tempRectF.scaleCentered(lastPostCommitFlingScale)
347         }
348         val scale = tempRectF.width() / backAnimRect.width()
349         val matrix = baseTransformation?.matrix ?: transformMatrix.apply { reset() }
350         val scalePivotX =
351             if (isLetterboxed && enteringHasSameLetterbox) {
352                 closingTarget!!.localBounds.left.toFloat()
353             } else {
354                 0f
355             }
356         matrix.postScale(scale, scale, scalePivotX, 0f)
357         matrix.postTranslate(tempRectF.left, tempRectF.top)
358         transaction
359             .setAlpha(leash, alpha)
360             .setMatrix(leash, matrix, tmpFloat9)
361             .setCrop(leash, cropRect)
362             .setCornerRadius(leash, cornerRadius)
363     }
364 
365     protected fun applyTransaction() {
366         transaction.setFrameTimelineVsync(Choreographer.getInstance().vsyncId)
367         transaction.apply()
368     }
369 
370     private fun ensureScrimLayer() {
371         if (scrimLayer != null) return
372         val isDarkTheme: Boolean = isDarkMode(context)
373         val scrimBuilder =
374             SurfaceControl.Builder()
375                 .setName("Cross-Activity back animation scrim")
376                 .setCallsite("CrossActivityBackAnimation")
377                 .setColorLayer()
378                 .setOpaque(false)
379                 .setHidden(false)
380 
381         rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, scrimBuilder)
382         scrimLayer = scrimBuilder.build()
383         val colorComponents = floatArrayOf(0f, 0f, 0f)
384         maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT
385         val scrimCrop =
386             if (isLetterboxed) {
387                 closingTarget!!.windowConfiguration.bounds
388             } else {
389                 closingTarget!!.localBounds
390             }
391         transaction
392             .setColor(scrimLayer, colorComponents)
393             .setAlpha(scrimLayer!!, maxScrimAlpha)
394             .setCrop(scrimLayer!!, scrimCrop)
395             .setRelativeLayer(scrimLayer!!, closingTarget!!.leash, -1)
396             .show(scrimLayer)
397     }
398 
399     private fun removeScrimLayer() {
400         if (removeLayer(scrimLayer)) applyTransaction()
401         scrimLayer = null
402     }
403 
404     /**
405      * Adds two "fake" letterbox square surfaces to the left and right of the localBounds of the
406      * closing target
407      */
408     private fun ensureLetterboxes() {
409         closingTarget?.let { t ->
410             if (t.localBounds.left != 0 && leftLetterboxLayer == null) {
411                 val bounds =
412                     Rect(
413                         0,
414                         t.windowConfiguration.bounds.top,
415                         t.localBounds.left,
416                         t.windowConfiguration.bounds.bottom
417                     )
418                 leftLetterboxLayer = ensureLetterbox(bounds)
419             }
420             if (
421                 t.localBounds.right != t.windowConfiguration.bounds.right &&
422                     rightLetterboxLayer == null
423             ) {
424                 val bounds =
425                     Rect(
426                         t.localBounds.right,
427                         t.windowConfiguration.bounds.top,
428                         t.windowConfiguration.bounds.right,
429                         t.windowConfiguration.bounds.bottom
430                     )
431                 rightLetterboxLayer = ensureLetterbox(bounds)
432             }
433         }
434     }
435 
436     private fun ensureLetterbox(bounds: Rect): SurfaceControl {
437         val letterboxBuilder =
438             SurfaceControl.Builder()
439                 .setName("Cross-Activity back animation letterbox")
440                 .setCallsite("CrossActivityBackAnimation")
441                 .setColorLayer()
442                 .setOpaque(true)
443                 .setHidden(false)
444 
445         rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, letterboxBuilder)
446         val layer = letterboxBuilder.build()
447         val colorComponents =
448             floatArrayOf(
449                 Color.red(letterboxColor) / 255f,
450                 Color.green(letterboxColor) / 255f,
451                 Color.blue(letterboxColor) / 255f
452             )
453         transaction
454             .setColor(layer, colorComponents)
455             .setCrop(layer, bounds)
456             .setRelativeLayer(layer, closingTarget!!.leash, 1)
457             .show(layer)
458         return layer
459     }
460 
461     private fun removeLetterbox() {
462         if (removeLayer(leftLetterboxLayer) || removeLayer(rightLetterboxLayer)) applyTransaction()
463         leftLetterboxLayer = null
464         rightLetterboxLayer = null
465     }
466 
467     private fun removeLayer(layer: SurfaceControl?): Boolean {
468         layer?.let {
469             if (it.isValid) {
470                 transaction.remove(it)
471                 return true
472             }
473         }
474         return false
475     }
476 
477     override fun prepareNextAnimation(
478         animationInfo: BackNavigationInfo.CustomAnimationInfo?,
479         letterboxColor: Int
480     ): Boolean {
481         this.letterboxColor = letterboxColor
482         return false
483     }
484 
485     private inner class Callback : IOnBackInvokedCallback.Default() {
486         override fun onBackStarted(backMotionEvent: BackMotionEvent) {
487             // in case we're still animating an onBackCancelled event, let's remove the finish-
488             // callback from the progress animator to prevent calling finishAnimation() before
489             // restarting a new animation
490             progressAnimator.removeOnBackCancelledFinishCallback()
491 
492             startBackAnimation(backMotionEvent)
493             progressAnimator.onBackStarted(backMotionEvent) { backEvent: BackEvent ->
494                 onGestureProgress(backEvent)
495             }
496         }
497 
498         override fun onBackProgressed(backEvent: BackMotionEvent) {
499             triggerBack = backEvent.triggerBack
500             progressAnimator.onBackProgressed(backEvent)
501         }
502 
503         override fun onBackCancelled() {
504             triggerBack = false
505             progressAnimator.onBackCancelled { finishAnimation() }
506         }
507 
508         override fun onBackInvoked() {
509             triggerBack = true
510             progressAnimator.reset()
511             onGestureCommitted(progressAnimator.velocity)
512         }
513     }
514 
515     private inner class Runner : IRemoteAnimationRunner.Default() {
516         override fun onAnimationStart(
517             transit: Int,
518             apps: Array<RemoteAnimationTarget>,
519             wallpapers: Array<RemoteAnimationTarget>?,
520             nonApps: Array<RemoteAnimationTarget>?,
521             finishedCallback: IRemoteAnimationFinishedCallback
522         ) {
523             ProtoLog.d(
524                 ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
525                 "Start back to activity animation."
526             )
527             for (a in apps) {
528                 when (a.mode) {
529                     RemoteAnimationTarget.MODE_CLOSING -> closingTarget = a
530                     RemoteAnimationTarget.MODE_OPENING -> enteringTarget = a
531                 }
532             }
533             finishCallback = finishedCallback
534         }
535 
536         override fun onAnimationCancelled() {
537             finishAnimation()
538         }
539     }
540 
541     companion object {
542         /** Max scale of the closing window. */
543         internal const val MAX_SCALE = 0.9f
544         private const val MAX_SCRIM_ALPHA_DARK = 0.8f
545         private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f
546         private const val SPRING_SCALE = 100f
547         private const val MAX_FLING_VELOCITY = 1000f
548         private const val DEFAULT_FLING_VELOCITY = 120f
549     }
550 
551     enum class FlingMode {
552         NO_FLING,
553 
554         /**
555          * This is used for the closing target in custom cross-activity back animations. When the
556          * back gesture is flung, the closing target shrinks a bit further with a spring motion.
557          */
558         FLING_SHRINK,
559 
560         /**
561          * This is used for the closing and opening target in the default cross-activity back
562          * animation. When the back gesture is flung, the closing and opening targets shrink a
563          * bit further and then bounce back with a spring motion.
564          */
565         FLING_BOUNCE
566     }
567 }
568 
isDarkModenull569 private fun isDarkMode(context: Context): Boolean {
570     return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
571         Configuration.UI_MODE_NIGHT_YES
572 }
573 
setInterpolatedRectFnull574 internal fun RectF.setInterpolatedRectF(start: RectF, target: RectF, progress: Float) {
575     require(!(progress < 0 || progress > 1)) { "Progress value must be between 0 and 1" }
576     left = start.left + (target.left - start.left) * progress
577     top = start.top + (target.top - start.top) * progress
578     right = start.right + (target.right - start.right) * progress
579     bottom = start.bottom + (target.bottom - start.bottom) * progress
580 }
581 
scaleCenterednull582 internal fun RectF.scaleCentered(
583     scale: Float,
584     pivotX: Float = left + width() / 2,
585     pivotY: Float = top + height() / 2
586 ) {
587     offset(-pivotX, -pivotY) // move pivot to origin
588     scale(scale)
589     offset(pivotX, pivotY) // Move back to the original position
590 }
591