1 /*
2  * Copyright (C) 2021 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.systemui.animation
18 
19 import android.app.ActivityManager
20 import android.app.ActivityTaskManager
21 import android.app.PendingIntent
22 import android.app.TaskInfo
23 import android.graphics.Matrix
24 import android.graphics.Path
25 import android.graphics.Rect
26 import android.graphics.RectF
27 import android.os.Looper
28 import android.os.RemoteException
29 import android.util.Log
30 import android.view.IRemoteAnimationFinishedCallback
31 import android.view.IRemoteAnimationRunner
32 import android.view.RemoteAnimationAdapter
33 import android.view.RemoteAnimationTarget
34 import android.view.SyncRtSurfaceTransactionApplier
35 import android.view.View
36 import android.view.ViewGroup
37 import android.view.WindowManager
38 import android.view.animation.Interpolator
39 import android.view.animation.PathInterpolator
40 import com.android.internal.annotations.VisibleForTesting
41 import com.android.internal.policy.ScreenDecorationsUtils
42 import kotlin.math.roundToInt
43 
44 private const val TAG = "ActivityLaunchAnimator"
45 
46 /**
47  * A class that allows activities to be started in a seamless way from a view that is transforming
48  * nicely into the starting window.
49  */
50 class ActivityLaunchAnimator(
51     private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS)
52 ) {
53     companion object {
54         @JvmField
55         val TIMINGS = LaunchAnimator.Timings(
56             totalDuration = 500L,
57             contentBeforeFadeOutDelay = 0L,
58             contentBeforeFadeOutDuration = 150L,
59             contentAfterFadeInDelay = 150L,
60             contentAfterFadeInDuration = 183L
61         )
62 
63         val INTERPOLATORS = LaunchAnimator.Interpolators(
64             positionInterpolator = Interpolators.EMPHASIZED,
65             positionXInterpolator = createPositionXInterpolator(),
66             contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN,
67             contentAfterFadeInInterpolator = PathInterpolator(0f, 0f, 0.6f, 1f)
68         )
69 
70         /** Durations & interpolators for the navigation bar fading in & out. */
71         private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
72         private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
73         private val ANIMATION_DELAY_NAV_FADE_IN =
74             TIMINGS.totalDuration - ANIMATION_DURATION_NAV_FADE_IN
75 
76         private val NAV_FADE_IN_INTERPOLATOR = Interpolators.STANDARD_DECELERATE
77         private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
78 
79         /** The time we wait before timing out the remote animation after starting the intent. */
80         private const val LAUNCH_TIMEOUT = 1000L
81 
82         private fun createPositionXInterpolator(): Interpolator {
83             val path = Path().apply {
84                 moveTo(0f, 0f)
85                 cubicTo(0.1217f, 0.0462f, 0.15f, 0.4686f, 0.1667f, 0.66f)
86                 cubicTo(0.1834f, 0.8878f, 0.1667f, 1f, 1f, 1f)
87             }
88             return PathInterpolator(path)
89         }
90     }
91 
92     /**
93      * The callback of this animator. This should be set before any call to
94      * [start(Pending)IntentWithAnimation].
95      */
96     var callback: Callback? = null
97 
98     /**
99      * Start an intent and animate the opening window. The intent will be started by running
100      * [intentStarter], which should use the provided [RemoteAnimationAdapter] and return the launch
101      * result. [controller] is responsible from animating the view from which the intent was started
102      * in [Controller.onLaunchAnimationProgress]. No animation will start if there is no window
103      * opening.
104      *
105      * If [controller] is null or [animate] is false, then the intent will be started and no
106      * animation will run.
107      *
108      * If possible, you should pass the [packageName] of the intent that will be started so that
109      * trampoline activity launches will also be animated.
110      *
111      * If the device is currently locked, the user will have to unlock it before the intent is
112      * started unless [showOverLockscreen] is true. In that case, the activity will be started
113      * directly over the lockscreen.
114      *
115      * This method will throw any exception thrown by [intentStarter].
116      */
117     @JvmOverloads
118     fun startIntentWithAnimation(
119         controller: Controller?,
120         animate: Boolean = true,
121         packageName: String? = null,
122         showOverLockscreen: Boolean = false,
123         intentStarter: (RemoteAnimationAdapter?) -> Int
124     ) {
125         if (controller == null || !animate) {
126             Log.i(TAG, "Starting intent with no animation")
127             intentStarter(null)
128             controller?.callOnIntentStartedOnMainThread(willAnimate = false)
129             return
130         }
131 
132         val callback = this.callback ?: throw IllegalStateException(
133             "ActivityLaunchAnimator.callback must be set before using this animator")
134         val runner = Runner(controller)
135         val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen
136 
137         // Pass the RemoteAnimationAdapter to the intent starter only if we are not hiding the
138         // keyguard with the animation
139         val animationAdapter = if (!hideKeyguardWithAnimation) {
140             RemoteAnimationAdapter(
141                 runner,
142                 TIMINGS.totalDuration,
143                 TIMINGS.totalDuration - 150 /* statusBarTransitionDelay */
144             )
145         } else {
146             null
147         }
148 
149         // Register the remote animation for the given package to also animate trampoline
150         // activity launches.
151         if (packageName != null && animationAdapter != null) {
152             try {
153                 ActivityTaskManager.getService().registerRemoteAnimationForNextActivityStart(
154                     packageName, animationAdapter)
155             } catch (e: RemoteException) {
156                 Log.w(TAG, "Unable to register the remote animation", e)
157             }
158         }
159 
160         val launchResult = intentStarter(animationAdapter)
161 
162         // Only animate if the app is not already on top and will be opened, unless we are on the
163         // keyguard.
164         val willAnimate =
165             launchResult == ActivityManager.START_TASK_TO_FRONT ||
166                     launchResult == ActivityManager.START_SUCCESS ||
167                     (launchResult == ActivityManager.START_DELIVERED_TO_TOP &&
168                             hideKeyguardWithAnimation)
169 
170         Log.i(TAG, "launchResult=$launchResult willAnimate=$willAnimate " +
171                 "hideKeyguardWithAnimation=$hideKeyguardWithAnimation")
172         controller.callOnIntentStartedOnMainThread(willAnimate)
173 
174         // If we expect an animation, post a timeout to cancel it in case the remote animation is
175         // never started.
176         if (willAnimate) {
177             runner.postTimeout()
178 
179             // Hide the keyguard using the launch animation instead of the default unlock animation.
180             if (hideKeyguardWithAnimation) {
181                 callback.hideKeyguardWithAnimation(runner)
182             }
183         }
184     }
185 
186     private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) {
187         if (Looper.myLooper() != Looper.getMainLooper()) {
188             this.launchContainer.context.mainExecutor.execute {
189                 this.onIntentStarted(willAnimate)
190             }
191         } else {
192             this.onIntentStarted(willAnimate)
193         }
194     }
195 
196     /**
197      * Same as [startIntentWithAnimation] but allows [intentStarter] to throw a
198      * [PendingIntent.CanceledException] which must then be handled by the caller. This is useful
199      * for Java caller starting a [PendingIntent].
200      *
201      * If possible, you should pass the [packageName] of the intent that will be started so that
202      * trampoline activity launches will also be animated.
203      */
204     @Throws(PendingIntent.CanceledException::class)
205     @JvmOverloads
206     fun startPendingIntentWithAnimation(
207         controller: Controller?,
208         animate: Boolean = true,
209         packageName: String? = null,
210         intentStarter: PendingIntentStarter
211     ) {
212         startIntentWithAnimation(controller, animate, packageName) {
213             intentStarter.startPendingIntent(it)
214         }
215     }
216 
217     /** Create a new animation [Runner] controlled by [controller]. */
218     @VisibleForTesting
219     fun createRunner(controller: Controller): Runner = Runner(controller)
220 
221     interface PendingIntentStarter {
222         /**
223          * Start a pending intent using the provided [animationAdapter] and return the launch
224          * result.
225          */
226         @Throws(PendingIntent.CanceledException::class)
227         fun startPendingIntent(animationAdapter: RemoteAnimationAdapter?): Int
228     }
229 
230     interface Callback {
231         /** Whether we are currently on the keyguard or not. */
232         fun isOnKeyguard(): Boolean
233 
234         /** Hide the keyguard and animate using [runner]. */
235         fun hideKeyguardWithAnimation(runner: IRemoteAnimationRunner)
236 
237         /** Enable/disable window blur so they don't overlap with the window launch animation **/
238         fun setBlursDisabledForAppLaunch(disabled: Boolean)
239 
240         /* Get the background color of [task]. */
241         fun getBackgroundColor(task: TaskInfo): Int
242     }
243 
244     /**
245      * A controller that takes care of applying the animation to an expanding view.
246      *
247      * Note that all callbacks (onXXX methods) are all called on the main thread.
248      */
249     interface Controller : LaunchAnimator.Controller {
250         companion object {
251             /**
252              * Return a [Controller] that will animate and expand [view] into the opening window.
253              *
254              * Important: The view must be attached to a [ViewGroup] when calling this function and
255              * during the animation. For safety, this method will return null when it is not.
256              */
257             @JvmStatic
258             fun fromView(view: View, cujType: Int? = null): Controller? {
259                 if (view.parent !is ViewGroup) {
260                     // TODO(b/192194319): Throw instead of just logging.
261                     Log.wtf(
262                         TAG,
263                         "Skipping animation as view $view is not attached to a ViewGroup",
264                         Exception()
265                     )
266                     return null
267                 }
268 
269                 return GhostedViewLaunchAnimatorController(view, cujType)
270             }
271         }
272 
273         /**
274          * The intent was started. If [willAnimate] is false, nothing else will happen and the
275          * animation will not be started.
276          */
277         fun onIntentStarted(willAnimate: Boolean) {}
278 
279         /**
280          * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after
281          * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called
282          * before the cancellation.
283          */
284         fun onLaunchAnimationCancelled() {}
285     }
286 
287     @VisibleForTesting
288     inner class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() {
289         private val launchContainer = controller.launchContainer
290         private val context = launchContainer.context
291         private val transactionApplier = SyncRtSurfaceTransactionApplier(launchContainer)
292 
293         private val matrix = Matrix()
294         private val invertMatrix = Matrix()
295         private var windowCrop = Rect()
296         private var windowCropF = RectF()
297         private var timedOut = false
298         private var cancelled = false
299         private var animation: LaunchAnimator.Animation? = null
300 
301         // A timeout to cancel the remote animation if it is not started within X milliseconds after
302         // the intent was started.
303         //
304         // Note that this is important to keep this a Runnable (and not a Kotlin lambda), otherwise
305         // it will be automatically converted when posted and we wouldn't be able to remove it after
306         // posting it.
307         private var onTimeout = Runnable { onAnimationTimedOut() }
308 
309         internal fun postTimeout() {
310             launchContainer.postDelayed(onTimeout, LAUNCH_TIMEOUT)
311         }
312 
313         private fun removeTimeout() {
314             launchContainer.removeCallbacks(onTimeout)
315         }
316 
317         override fun onAnimationStart(
318             @WindowManager.TransitionOldType transit: Int,
319             apps: Array<out RemoteAnimationTarget>?,
320             wallpapers: Array<out RemoteAnimationTarget>?,
321             nonApps: Array<out RemoteAnimationTarget>?,
322             iCallback: IRemoteAnimationFinishedCallback?
323         ) {
324             removeTimeout()
325 
326             // The animation was started too late and we already notified the controller that it
327             // timed out.
328             if (timedOut) {
329                 iCallback?.invoke()
330                 return
331             }
332 
333             // This should not happen, but let's make sure we don't start the animation if it was
334             // cancelled before and we already notified the controller.
335             if (cancelled) {
336                 return
337             }
338 
339             context.mainExecutor.execute {
340                 startAnimation(apps, nonApps, iCallback)
341             }
342         }
343 
344         private fun startAnimation(
345             apps: Array<out RemoteAnimationTarget>?,
346             nonApps: Array<out RemoteAnimationTarget>?,
347             iCallback: IRemoteAnimationFinishedCallback?
348         ) {
349             if (LaunchAnimator.DEBUG) {
350                 Log.d(TAG, "Remote animation started")
351             }
352 
353             val window = apps?.firstOrNull {
354                 it.mode == RemoteAnimationTarget.MODE_OPENING
355             }
356 
357             if (window == null) {
358                 Log.i(TAG, "Aborting the animation as no window is opening")
359                 removeTimeout()
360                 iCallback?.invoke()
361                 controller.onLaunchAnimationCancelled()
362                 return
363             }
364 
365             val navigationBar = nonApps?.firstOrNull {
366                 it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
367             }
368 
369             val windowBounds = window.screenSpaceBounds
370             val endState = LaunchAnimator.State(
371                 top = windowBounds.top,
372                 bottom = windowBounds.bottom,
373                 left = windowBounds.left,
374                 right = windowBounds.right
375             )
376             val callback = this@ActivityLaunchAnimator.callback!!
377             val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo)
378 
379             // TODO(b/184121838): We should somehow get the top and bottom radius of the window
380             // instead of recomputing isExpandingFullyAbove here.
381             val isExpandingFullyAbove =
382                 launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState)
383             val endRadius = if (isExpandingFullyAbove) {
384                 // Most of the time, expanding fully above the root view means expanding in full
385                 // screen.
386                 ScreenDecorationsUtils.getWindowCornerRadius(context)
387             } else {
388                 // This usually means we are in split screen mode, so 2 out of 4 corners will have
389                 // a radius of 0.
390                 0f
391             }
392             endState.topCornerRadius = endRadius
393             endState.bottomCornerRadius = endRadius
394 
395             // We animate the opening window and delegate the view expansion to [this.controller].
396             val delegate = this.controller
397             val controller = object : LaunchAnimator.Controller by delegate {
398                 override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
399                     callback.setBlursDisabledForAppLaunch(true)
400                     delegate.onLaunchAnimationStart(isExpandingFullyAbove)
401                 }
402 
403                 override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
404                     callback.setBlursDisabledForAppLaunch(false)
405                     iCallback?.invoke()
406                     delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
407                 }
408 
409                 override fun onLaunchAnimationProgress(
410                     state: LaunchAnimator.State,
411                     progress: Float,
412                     linearProgress: Float
413                 ) {
414                     applyStateToWindow(window, state)
415                     navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
416                     delegate.onLaunchAnimationProgress(state, progress, linearProgress)
417                 }
418             }
419 
420             // We draw a hole when the additional layer is fading out to reveal the opening window.
421             animation = launchAnimator.startAnimation(
422                 controller, endState, windowBackgroundColor, drawHole = true)
423         }
424 
425         private fun applyStateToWindow(window: RemoteAnimationTarget, state: LaunchAnimator.State) {
426             val screenBounds = window.screenSpaceBounds
427             val centerX = (screenBounds.left + screenBounds.right) / 2f
428             val centerY = (screenBounds.top + screenBounds.bottom) / 2f
429             val width = screenBounds.right - screenBounds.left
430             val height = screenBounds.bottom - screenBounds.top
431 
432             // Scale the window. We use the max of (widthRatio, heightRatio) so that there is no
433             // blank space on any side.
434             val widthRatio = state.width.toFloat() / width
435             val heightRatio = state.height.toFloat() / height
436             val scale = maxOf(widthRatio, heightRatio)
437             matrix.reset()
438             matrix.setScale(scale, scale, centerX, centerY)
439 
440             // Align it to the top and center it in the x-axis.
441             val heightChange = height * scale - height
442             val translationX = state.centerX - centerX
443             val translationY = state.top - screenBounds.top + heightChange / 2f
444             matrix.postTranslate(translationX, translationY)
445 
446             // Crop it. The matrix will also be applied to the crop, so we apply the inverse
447             // operation. Given that we only scale (by factor > 0) then translate, we can assume
448             // that the matrix is invertible.
449             val cropX = state.left.toFloat() - screenBounds.left
450             val cropY = state.top.toFloat() - screenBounds.top
451             windowCropF.set(cropX, cropY, cropX + state.width, cropY + state.height)
452             matrix.invert(invertMatrix)
453             invertMatrix.mapRect(windowCropF)
454             windowCrop.set(
455                 windowCropF.left.roundToInt(),
456                 windowCropF.top.roundToInt(),
457                 windowCropF.right.roundToInt(),
458                 windowCropF.bottom.roundToInt()
459             )
460 
461             // The scale will also be applied to the corner radius, so we divide by the scale to
462             // keep the original radius. We use the max of (topCornerRadius, bottomCornerRadius) to
463             // make sure that the window does not draw itself behind the expanding view. This is
464             // especially important for lock screen animations, where the window is not clipped by
465             // the shade.
466             val cornerRadius = maxOf(state.topCornerRadius, state.bottomCornerRadius) / scale
467             val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(window.leash)
468                 .withAlpha(1f)
469                 .withMatrix(matrix)
470                 .withWindowCrop(windowCrop)
471                 .withCornerRadius(cornerRadius)
472                 .withVisibility(true)
473                 .build()
474 
475             transactionApplier.scheduleApply(params)
476         }
477 
478         private fun applyStateToNavigationBar(
479             navigationBar: RemoteAnimationTarget,
480             state: LaunchAnimator.State,
481             linearProgress: Float
482         ) {
483             val fadeInProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress,
484                 ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT)
485 
486             val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash)
487             if (fadeInProgress > 0) {
488                 matrix.reset()
489                 matrix.setTranslate(
490                     0f, (state.top - navigationBar.sourceContainerBounds.top).toFloat())
491                 windowCrop.set(state.left, 0, state.right, state.height)
492                 params
493                     .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress))
494                     .withMatrix(matrix)
495                     .withWindowCrop(windowCrop)
496                     .withVisibility(true)
497             } else {
498                 val fadeOutProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress, 0,
499                     ANIMATION_DURATION_NAV_FADE_OUT)
500                 params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
501             }
502 
503             transactionApplier.scheduleApply(params.build())
504         }
505 
506         private fun onAnimationTimedOut() {
507             if (cancelled) {
508                 return
509             }
510 
511             Log.i(TAG, "Remote animation timed out")
512             timedOut = true
513             controller.onLaunchAnimationCancelled()
514         }
515 
516         override fun onAnimationCancelled() {
517             if (timedOut) {
518                 return
519             }
520 
521             Log.i(TAG, "Remote animation was cancelled")
522             cancelled = true
523             removeTimeout()
524             context.mainExecutor.execute {
525                 animation?.cancel()
526                 controller.onLaunchAnimationCancelled()
527             }
528         }
529 
530         private fun IRemoteAnimationFinishedCallback.invoke() {
531             try {
532                 onAnimationFinished()
533             } catch (e: RemoteException) {
534                 e.printStackTrace()
535             }
536         }
537     }
538 }
539