1 /*
<lambda>null2  * 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.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.ValueAnimator
22 import android.app.Dialog
23 import android.graphics.Color
24 import android.graphics.Rect
25 import android.os.Looper
26 import android.util.Log
27 import android.util.MathUtils
28 import android.view.View
29 import android.view.ViewGroup
30 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
31 import android.view.ViewRootImpl
32 import android.view.WindowInsets
33 import android.view.WindowManager
34 import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
35 import com.android.app.animation.Interpolators
36 import com.android.internal.jank.Cuj.CujType
37 import com.android.internal.jank.InteractionJankMonitor
38 import com.android.systemui.util.maybeForceFullscreen
39 import com.android.systemui.util.registerAnimationOnBackInvoked
40 import java.util.concurrent.Executor
41 import kotlin.math.roundToInt
42 
43 private const val TAG = "DialogTransitionAnimator"
44 
45 /**
46  * A class that allows dialogs to be started in a seamless way from a view that is transforming
47  * nicely into the starting dialog.
48  *
49  * This animator also allows to easily animate a dialog into an activity.
50  *
51  * @see show
52  * @see showFromView
53  * @see showFromDialog
54  * @see createActivityTransitionController
55  */
56 class DialogTransitionAnimator
57 @JvmOverloads
58 constructor(
59     private val mainExecutor: Executor,
60     private val callback: Callback,
61     private val interactionJankMonitor: InteractionJankMonitor,
62     private val featureFlags: AnimationFeatureFlags,
63     private val transitionAnimator: TransitionAnimator =
64         TransitionAnimator(
65             mainExecutor,
66             TIMINGS,
67             INTERPOLATORS,
68         ),
69     private val isForTesting: Boolean = false,
70 ) {
71     private companion object {
72         private val TIMINGS = ActivityTransitionAnimator.TIMINGS
73 
74         // We use the same interpolator for X and Y axis to make sure the dialog does not move out
75         // of the screen bounds during the animation.
76         private val INTERPOLATORS =
77             ActivityTransitionAnimator.INTERPOLATORS.copy(
78                 positionXInterpolator =
79                     ActivityTransitionAnimator.INTERPOLATORS.positionInterpolator
80             )
81     }
82 
83     /**
84      * A controller that takes care of applying the dialog launch and exit animations to the source
85      * that triggered the animation.
86      */
87     interface Controller {
88         /** The [ViewRootImpl] of this controller. */
89         val viewRoot: ViewRootImpl?
90 
91         /**
92          * The identity object of the source animated by this controller. This animator will ensure
93          * that 2 animations with the same source identity are not going to run at the same time, to
94          * avoid flickers when a dialog is shown from the same source more or less at the same time
95          * (for instance if the user clicks an expandable button twice).
96          */
97         val sourceIdentity: Any
98 
99         /** The CUJ associated to this controller. */
100         val cuj: DialogCuj?
101 
102         /**
103          * Move the drawing of the source in the overlay of [viewGroup].
104          *
105          * Once this method is called, and until [stopDrawingInOverlay] is called, the source
106          * controlled by this Controller should be drawn in the overlay of [viewGroup] so that it is
107          * drawn above all other elements in the same [viewRoot].
108          */
109         fun startDrawingInOverlayOf(viewGroup: ViewGroup)
110 
111         /**
112          * Move the drawing of the source back in its original location.
113          *
114          * @see startDrawingInOverlayOf
115          */
116         fun stopDrawingInOverlay()
117 
118         /**
119          * Create the [TransitionAnimator.Controller] that will be called to animate the source
120          * controlled by this [Controller] during the dialog launch animation.
121          *
122          * At the end of this animation, the source should *not* be visible anymore (until the
123          * dialog is closed and is animated back into the source).
124          */
125         fun createTransitionController(): TransitionAnimator.Controller
126 
127         /**
128          * Create the [TransitionAnimator.Controller] that will be called to animate the source
129          * controlled by this [Controller] during the dialog exit animation.
130          *
131          * At the end of this animation, the source should be visible again.
132          */
133         fun createExitController(): TransitionAnimator.Controller
134 
135         /**
136          * Whether we should animate the dialog back into the source when it is dismissed. If this
137          * methods returns `false`, then the dialog will simply fade out and
138          * [onExitAnimationCancelled] will be called.
139          *
140          * Note that even when this returns `true`, the exit animation might still be cancelled (in
141          * which case [onExitAnimationCancelled] will also be called).
142          */
143         fun shouldAnimateExit(): Boolean
144 
145         /**
146          * Called if we decided to *not* animate the dialog into the source for some reason. This
147          * means that [createExitController] will *not* be called and this implementation should
148          * make sure that the source is back in its original state, before it was animated into the
149          * dialog. In particular, the source should be visible again.
150          */
151         fun onExitAnimationCancelled()
152 
153         /**
154          * Return the [InteractionJankMonitor.Configuration.Builder] to be used for animations
155          * controlled by this controller.
156          */
157         // TODO(b/252723237): Make this non-nullable
158         fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder?
159 
160         companion object {
161             /**
162              * Create a [Controller] that can animate [source] to and from a dialog.
163              *
164              * Important: The view must be attached to a [ViewGroup] when calling this function and
165              * during the animation. For safety, this method will return null when it is not. The
166              * view must also implement [LaunchableView], otherwise this method will throw.
167              *
168              * Note: The background of [view] should be a (rounded) rectangle so that it can be
169              * properly animated.
170              */
171             fun fromView(source: View, cuj: DialogCuj? = null): Controller? {
172                 // Make sure the View we launch from implements LaunchableView to avoid visibility
173                 // issues.
174                 if (source !is LaunchableView) {
175                     throw IllegalArgumentException(
176                         "A DialogTransitionAnimator.Controller was created from a View that does " +
177                             "not implement LaunchableView. This can lead to subtle bugs where " +
178                             "the visibility of the View we are launching from is not what we " +
179                             "expected."
180                     )
181                 }
182 
183                 if (source.parent !is ViewGroup) {
184                     Log.e(
185                         TAG,
186                         "Skipping animation as view $source is not attached to a ViewGroup",
187                         Exception(),
188                     )
189                     return null
190                 }
191 
192                 return ViewDialogTransitionAnimatorController(source, cuj)
193             }
194         }
195     }
196 
197     /**
198      * The set of dialogs that were animated using this animator and that are still opened (not
199      * dismissed, but can be hidden).
200      */
201     // TODO(b/201264644): Remove this set.
202     private val openedDialogs = hashSetOf<AnimatedDialog>()
203 
204     /**
205      * Show [dialog] by expanding it from [view]. If [view] is a view inside another dialog that was
206      * shown using this method, then we will animate from that dialog instead.
207      *
208      * If [animateBackgroundBoundsChange] is true, then the background of the dialog will be
209      * animated when the dialog bounds change.
210      *
211      * Note: The background of [view] should be a (rounded) rectangle so that it can be properly
212      * animated.
213      *
214      * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be
215      * made fullscreen and 2 views will be inserted between the dialog DecorView and its children.
216      */
217     @JvmOverloads
218     fun showFromView(
219         dialog: Dialog,
220         view: View,
221         cuj: DialogCuj? = null,
222         animateBackgroundBoundsChange: Boolean = false
223     ) {
224         val controller = Controller.fromView(view, cuj)
225         if (controller == null) {
226             dialog.show()
227         } else {
228             show(dialog, controller, animateBackgroundBoundsChange)
229         }
230     }
231 
232     /**
233      * Show [dialog] by expanding it from a source controlled by [controller].
234      *
235      * If [animateBackgroundBoundsChange] is true, then the background of the dialog will be
236      * animated when the dialog bounds change.
237      *
238      * Note: The background of [view] should be a (rounded) rectangle so that it can be properly
239      * animated.
240      *
241      * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be
242      * made fullscreen and 2 views will be inserted between the dialog DecorView and its children.
243      */
244     @JvmOverloads
245     fun show(
246         dialog: Dialog,
247         controller: Controller,
248         animateBackgroundBoundsChange: Boolean = false
249     ) {
250         if (Looper.myLooper() != Looper.getMainLooper()) {
251             throw IllegalStateException(
252                 "showFromView must be called from the main thread and dialog must be created in " +
253                     "the main thread"
254             )
255         }
256 
257         // If the view we are launching from belongs to another dialog, then this means the caller
258         // intent is to launch a dialog from another dialog.
259         val animatedParent =
260             openedDialogs.firstOrNull {
261                 it.dialog.window?.decorView?.viewRootImpl == controller.viewRoot
262             }
263         val controller =
264             animatedParent?.dialogContentWithBackground?.let {
265                 Controller.fromView(it, controller.cuj)
266             }
267                 ?: controller
268 
269         // Make sure we don't run the launch animation from the same source twice at the same time.
270         if (openedDialogs.any { it.controller.sourceIdentity == controller.sourceIdentity }) {
271             Log.e(
272                 TAG,
273                 "Not running dialog launch animation from source as it is already expanded into a" +
274                     " dialog"
275             )
276             dialog.show()
277             return
278         }
279 
280         val animatedDialog =
281             AnimatedDialog(
282                 transitionAnimator = transitionAnimator,
283                 callback = callback,
284                 interactionJankMonitor = interactionJankMonitor,
285                 controller = controller,
286                 onDialogDismissed = { openedDialogs.remove(it) },
287                 dialog = dialog,
288                 animateBackgroundBoundsChange = animateBackgroundBoundsChange,
289                 parentAnimatedDialog = animatedParent,
290                 forceDisableSynchronization = isForTesting,
291                 featureFlags = featureFlags,
292             )
293 
294         openedDialogs.add(animatedDialog)
295         animatedDialog.start()
296     }
297 
298     /**
299      * Launch [dialog] from [another dialog][animateFrom] that was shown using [show]. This will
300      * allow for dismissing the whole stack.
301      *
302      * @see dismissStack
303      */
304     fun showFromDialog(
305         dialog: Dialog,
306         animateFrom: Dialog,
307         cuj: DialogCuj? = null,
308         animateBackgroundBoundsChange: Boolean = false
309     ) {
310         val view =
311             openedDialogs.firstOrNull { it.dialog == animateFrom }?.dialogContentWithBackground
312         if (view == null) {
313             Log.w(
314                 TAG,
315                 "Showing dialog $dialog normally as the dialog it is shown from was not shown " +
316                     "using DialogTransitionAnimator"
317             )
318             dialog.show()
319             return
320         }
321 
322         showFromView(
323             dialog,
324             view,
325             animateBackgroundBoundsChange = animateBackgroundBoundsChange,
326             cuj = cuj
327         )
328     }
329 
330     /**
331      * Create an [ActivityTransitionAnimator.Controller] that can be used to launch an activity from
332      * the dialog that contains [View]. Note that the dialog must have been shown using this
333      * animator, otherwise this method will return null.
334      *
335      * The returned controller will take care of dismissing the dialog at the right time after the
336      * activity started, when the dialog to app animation is done (or when it is cancelled). If this
337      * method returns null, then the dialog won't be dismissed.
338      *
339      * @param view any view inside the dialog to animate.
340      */
341     @JvmOverloads
342     fun createActivityTransitionController(
343         view: View,
344         cujType: Int? = null,
345     ): ActivityTransitionAnimator.Controller? {
346         val animatedDialog =
347             openedDialogs.firstOrNull {
348                 it.dialog.window?.decorView?.viewRootImpl == view.viewRootImpl
349             }
350                 ?: return null
351         return createActivityTransitionController(animatedDialog, cujType)
352     }
353 
354     /**
355      * Create an [ActivityTransitionAnimator.Controller] that can be used to launch an activity from
356      * [dialog]. Note that the dialog must have been shown using this animator, otherwise this
357      * method will return null.
358      *
359      * The returned controller will take care of dismissing the dialog at the right time after the
360      * activity started, when the dialog to app animation is done (or when it is cancelled). If this
361      * method returns null, then the dialog won't be dismissed.
362      *
363      * @param dialog the dialog to animate.
364      */
365     @JvmOverloads
366     fun createActivityTransitionController(
367         dialog: Dialog,
368         cujType: Int? = null,
369     ): ActivityTransitionAnimator.Controller? {
370         val animatedDialog = openedDialogs.firstOrNull { it.dialog == dialog } ?: return null
371         return createActivityTransitionController(animatedDialog, cujType)
372     }
373 
374     private fun createActivityTransitionController(
375         animatedDialog: AnimatedDialog,
376         cujType: Int? = null
377     ): ActivityTransitionAnimator.Controller? {
378         // At this point, we know that the intent of the caller is to dismiss the dialog to show
379         // an app, so we disable the exit animation into the source because we will never want to
380         // run it anyways.
381         animatedDialog.exitAnimationDisabled = true
382 
383         val dialog = animatedDialog.dialog
384 
385         // Don't animate if the dialog is not showing or if we are locked and going to show the
386         // primary bouncer.
387         if (
388             !dialog.isShowing ||
389                 (!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock())
390         ) {
391             return null
392         }
393 
394         val dialogContentWithBackground = animatedDialog.dialogContentWithBackground ?: return null
395         val controller =
396             ActivityTransitionAnimator.Controller.fromView(dialogContentWithBackground, cujType)
397                 ?: return null
398 
399         // Wrap the controller into one that will instantly dismiss the dialog when the animation is
400         // done or dismiss it normally (fading it out) if the animation is cancelled.
401         return object : ActivityTransitionAnimator.Controller by controller {
402             override val isDialogLaunch = true
403 
404             override fun onIntentStarted(willAnimate: Boolean) {
405                 controller.onIntentStarted(willAnimate)
406 
407                 if (!willAnimate) {
408                     dialog.dismiss()
409                 }
410             }
411 
412             override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
413                 controller.onTransitionAnimationCancelled()
414                 enableDialogDismiss()
415                 dialog.dismiss()
416             }
417 
418             override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
419                 controller.onTransitionAnimationStart(isExpandingFullyAbove)
420 
421                 // Make sure the dialog is not dismissed during the animation.
422                 disableDialogDismiss()
423 
424                 // If this dialog was shown from a cascade of other dialogs, make sure those ones
425                 // are dismissed too.
426                 animatedDialog.prepareForStackDismiss()
427 
428                 // Remove the dim.
429                 dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
430             }
431 
432             override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
433                 controller.onTransitionAnimationEnd(isExpandingFullyAbove)
434 
435                 // Hide the dialog then dismiss it to instantly dismiss it without playing the
436                 // animation.
437                 dialog.hide()
438                 enableDialogDismiss()
439                 dialog.dismiss()
440             }
441 
442             private fun disableDialogDismiss() {
443                 dialog.setDismissOverride { /* Do nothing */}
444             }
445 
446             private fun enableDialogDismiss() {
447                 // We don't set the override to null given that [AnimatedDialog.OnDialogDismissed]
448                 // will still properly dismiss the dialog but will also make sure to clean up
449                 // everything (like making sure that the touched view that triggered the dialog is
450                 // made VISIBLE again).
451                 dialog.setDismissOverride(animatedDialog::onDialogDismissed)
452             }
453         }
454     }
455 
456     /**
457      * Ensure that all dialogs currently shown won't animate into their source when dismissed.
458      *
459      * This is a temporary API meant to be called right before we both dismiss a dialog and start an
460      * activity, which currently does not look good if we animate the dialog into their source at
461      * the same time as the activity starts.
462      *
463      * TODO(b/193634619): Remove this function and animate dialog into opening activity instead.
464      */
465     fun disableAllCurrentDialogsExitAnimations() {
466         openedDialogs.forEach { it.exitAnimationDisabled = true }
467     }
468 
469     /**
470      * Dismiss [dialog]. If it was launched from another dialog using this animator, also dismiss
471      * the stack of dialogs and simply fade out [dialog].
472      */
473     fun dismissStack(dialog: Dialog) {
474         openedDialogs.firstOrNull { it.dialog == dialog }?.prepareForStackDismiss()
475         dialog.dismiss()
476     }
477 
478     interface Callback {
479         /** Whether the device is currently in dreaming (screensaver) mode. */
480         fun isDreaming(): Boolean
481 
482         /**
483          * Whether the device is currently unlocked, i.e. if it is *not* on the keyguard or if the
484          * keyguard can be dismissed.
485          */
486         fun isUnlocked(): Boolean
487 
488         /**
489          * Whether we are going to show alternate authentication (like UDFPS) instead of the
490          * traditional bouncer when unlocking the device.
491          */
492         fun isShowingAlternateAuthOnUnlock(): Boolean
493     }
494 }
495 
496 /**
497  * The CUJ interaction associated with opening the dialog.
498  *
499  * The optional tag indicates the specific dialog being opened.
500  */
501 data class DialogCuj(@CujType val cujType: Int, val tag: String? = null)
502 
503 private class AnimatedDialog(
504     private val transitionAnimator: TransitionAnimator,
505     private val callback: DialogTransitionAnimator.Callback,
506     private val interactionJankMonitor: InteractionJankMonitor,
507 
508     /**
509      * The controller of the source that triggered the dialog and that will animate into/from the
510      * dialog.
511      */
512     val controller: DialogTransitionAnimator.Controller,
513 
514     /**
515      * A callback that will be called with this [AnimatedDialog] after the dialog was dismissed and
516      * the exit animation is done.
517      */
518     private val onDialogDismissed: (AnimatedDialog) -> Unit,
519 
520     /** The dialog to show and animate. */
521     val dialog: Dialog,
522 
523     /** Whether we should animate the dialog background when its bounds change. */
524     animateBackgroundBoundsChange: Boolean,
525 
526     /** Launch animation corresponding to the parent [AnimatedDialog]. */
527     private val parentAnimatedDialog: AnimatedDialog? = null,
528 
529     /**
530      * Whether synchronization should be disabled, which can be useful if we are running in a test.
531      */
532     private val forceDisableSynchronization: Boolean,
533     private val featureFlags: AnimationFeatureFlags,
534 ) {
535     /**
536      * The DecorView of this dialog window.
537      *
538      * Note that we access this DecorView lazily to avoid accessing it before the dialog is created,
539      * which can sometimes cause crashes (e.g. with the Cast dialog).
540      */
<lambda>null541     private val decorView by lazy { dialog.window!!.decorView as ViewGroup }
542 
543     /**
544      * The dialog content with its background. When animating a fullscreen dialog, this is just the
545      * first ViewGroup of the dialog that has a background. When animating a normal (not fullscreen)
546      * dialog, this is an additional view that serves as a fake window that will have the same size
547      * as the dialog window initially had and to which we will set the dialog window background.
548      */
549     var dialogContentWithBackground: ViewGroup? = null
550 
551     /** The background color of [dialog], taking into consideration its window background color. */
552     private var originalDialogBackgroundColor = Color.BLACK
553 
554     /**
555      * Whether we are currently launching/showing the dialog by animating it from its source
556      * controlled by [controller].
557      */
558     private var isLaunching = true
559 
560     /** Whether we are currently dismissing/hiding the dialog by animating into its source. */
561     private var isDismissing = false
562 
563     private var dismissRequested = false
564     var exitAnimationDisabled = false
565 
566     private var isSourceDrawnInDialog = false
567     private var isOriginalDialogViewLaidOut = false
568 
569     /** A layout listener to animate the dialog height change. */
570     private val backgroundLayoutListener =
571         if (animateBackgroundBoundsChange) {
572             AnimatedBoundsLayoutListener()
573         } else {
574             null
575         }
576 
577     /*
578      * A layout listener in case the dialog (window) size changes (for instance because of a
579      * configuration change) to ensure that the dialog stays full width.
580      */
581     private var decorViewLayoutListener: View.OnLayoutChangeListener? = null
582 
583     private var hasInstrumentedJank = false
584 
startnull585     fun start() {
586         val cuj = controller.cuj
587         if (cuj != null) {
588             val config = controller.jankConfigurationBuilder()
589             if (config != null) {
590                 if (cuj.tag != null) {
591                     config.setTag(cuj.tag)
592                 }
593 
594                 interactionJankMonitor.begin(config)
595                 hasInstrumentedJank = true
596             }
597         }
598 
599         // Create the dialog so that its onCreate() method is called, which usually sets the dialog
600         // content.
601         dialog.create()
602 
603         val window = dialog.window!!
604         val isWindowFullScreen =
605             window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT
606         val dialogContentWithBackground =
607             if (isWindowFullScreen) {
608                 // If the dialog window is already fullscreen, then we look for the first ViewGroup
609                 // that has a background (and is not the DecorView, which always has a background)
610                 // and animate towards that ViewGroup given that this is probably what represents
611                 // the actual dialog view.
612                 var viewGroupWithBackground: ViewGroup? = null
613                 for (i in 0 until decorView.childCount) {
614                     viewGroupWithBackground =
615                         findFirstViewGroupWithBackground(decorView.getChildAt(i))
616                     if (viewGroupWithBackground != null) {
617                         break
618                     }
619                 }
620 
621                 // Animate that view with the background. Throw if we didn't find one, because
622                 // otherwise it's not clear what we should animate.
623                 if (viewGroupWithBackground == null) {
624                     error("Unable to find ViewGroup with background")
625                 }
626 
627                 if (viewGroupWithBackground !is LaunchableView) {
628                     error("The animated ViewGroup with background must implement LaunchableView")
629                 }
630 
631                 viewGroupWithBackground
632             } else {
633                 val (dialogContentWithBackground, decorViewLayoutListener) =
634                     dialog.maybeForceFullscreen()!!
635                 this.decorViewLayoutListener = decorViewLayoutListener
636                 dialogContentWithBackground
637             }
638 
639         this.dialogContentWithBackground = dialogContentWithBackground
640         dialogContentWithBackground.setTag(R.id.tag_dialog_background, true)
641 
642         val background = dialogContentWithBackground.background
643         originalDialogBackgroundColor =
644             GhostedViewTransitionAnimatorController.findGradientDrawable(background)
645                 ?.color
646                 ?.defaultColor
647                 ?: Color.BLACK
648 
649         // Make the background view invisible until we start the animation. We use the transition
650         // visibility like GhostView does so that we don't mess up with the accessibility tree (see
651         // b/204944038#comment17). Given that this background implements LaunchableView, we call
652         // setShouldBlockVisibilityChanges() early so that the current visibility (VISIBLE) is
653         // restored at the end of the animation.
654         dialogContentWithBackground.setShouldBlockVisibilityChanges(true)
655         dialogContentWithBackground.setTransitionVisibility(View.INVISIBLE)
656 
657         // Make sure the dialog is visible instantly and does not do any window animation.
658         val attributes = window.attributes
659         attributes.windowAnimations = R.style.Animation_LaunchAnimation
660 
661         // Ensure that the animation is not clipped by the display cut-out when animating this
662         // dialog into an app.
663         attributes.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
664 
665         // Ensure that the animation is not clipped by the navigation/task bars when animating this
666         // dialog into an app.
667         val wasFittingNavigationBars =
668             attributes.fitInsetsTypes and WindowInsets.Type.navigationBars() != 0
669         attributes.fitInsetsTypes =
670             attributes.fitInsetsTypes and WindowInsets.Type.navigationBars().inv()
671 
672         window.attributes = window.attributes
673 
674         // We apply the insets ourselves to make sure that the paddings are set on the correct
675         // View.
676         window.setDecorFitsSystemWindows(false)
677         val viewWithInsets = (dialogContentWithBackground.parent as ViewGroup)
678         viewWithInsets.setOnApplyWindowInsetsListener { view, windowInsets ->
679             val type =
680                 if (wasFittingNavigationBars) {
681                     WindowInsets.Type.displayCutout() or WindowInsets.Type.navigationBars()
682                 } else {
683                     WindowInsets.Type.displayCutout()
684                 }
685 
686             val insets = windowInsets.getInsets(type)
687             view.setPadding(insets.left, insets.top, insets.right, insets.bottom)
688             WindowInsets.CONSUMED
689         }
690 
691         // Start the animation once the background view is properly laid out.
692         dialogContentWithBackground.addOnLayoutChangeListener(
693             object : View.OnLayoutChangeListener {
694                 override fun onLayoutChange(
695                     v: View,
696                     left: Int,
697                     top: Int,
698                     right: Int,
699                     bottom: Int,
700                     oldLeft: Int,
701                     oldTop: Int,
702                     oldRight: Int,
703                     oldBottom: Int
704                 ) {
705                     dialogContentWithBackground.removeOnLayoutChangeListener(this)
706 
707                     isOriginalDialogViewLaidOut = true
708                     maybeStartLaunchAnimation()
709                 }
710             }
711         )
712 
713         // Disable the dim. We will enable it once we start the animation.
714         window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
715 
716         // Override the dialog dismiss() so that we can animate the exit before actually dismissing
717         // the dialog.
718         dialog.setDismissOverride(this::onDialogDismissed)
719 
720         if (featureFlags.isPredictiveBackQsDialogAnim) {
721             dialog.registerAnimationOnBackInvoked(targetView = dialogContentWithBackground)
722         }
723 
724         // Show the dialog.
725         dialog.show()
726         moveSourceDrawingToDialog()
727     }
728 
moveSourceDrawingToDialognull729     private fun moveSourceDrawingToDialog() {
730         if (decorView.viewRootImpl == null) {
731             // Make sure that we have access to the dialog view root to move the drawing to the
732             // dialog overlay.
733             decorView.post(::moveSourceDrawingToDialog)
734             return
735         }
736 
737         // Move the drawing of the source in the overlay of this dialog, then animate. We trigger a
738         // one-off synchronization to make sure that this is done in sync between the two different
739         // windows.
740         controller.startDrawingInOverlayOf(decorView)
741         synchronizeNextDraw(
742             then = {
743                 isSourceDrawnInDialog = true
744                 maybeStartLaunchAnimation()
745             }
746         )
747     }
748 
749     /**
750      * Synchronize the next draw of the source and dialog view roots so that they are performed at
751      * the same time, in the same transaction. This is necessary to make sure that the source is
752      * drawn in the overlay at the same time as it is removed from its original position (or
753      * inversely, removed from the overlay when the source is moved back to its original position).
754      */
synchronizeNextDrawnull755     private fun synchronizeNextDraw(then: () -> Unit) {
756         val controllerRootView = controller.viewRoot?.view
757         if (forceDisableSynchronization || controllerRootView == null) {
758             // Don't synchronize when inside an automated test or if the controller root view is
759             // detached.
760             then()
761             return
762         }
763 
764         ViewRootSync.synchronizeNextDraw(controllerRootView, decorView, then)
765         decorView.invalidate()
766         controllerRootView.invalidate()
767     }
768 
findFirstViewGroupWithBackgroundnull769     private fun findFirstViewGroupWithBackground(view: View): ViewGroup? {
770         if (view !is ViewGroup) {
771             return null
772         }
773 
774         if (view.background != null) {
775             return view
776         }
777 
778         for (i in 0 until view.childCount) {
779             val match = findFirstViewGroupWithBackground(view.getChildAt(i))
780             if (match != null) {
781                 return match
782             }
783         }
784 
785         return null
786     }
787 
maybeStartLaunchAnimationnull788     private fun maybeStartLaunchAnimation() {
789         if (!isSourceDrawnInDialog || !isOriginalDialogViewLaidOut) {
790             return
791         }
792 
793         // Show the background dim.
794         dialog.window?.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
795 
796         startAnimation(
797             isLaunching = true,
798             onLaunchAnimationEnd = {
799                 isLaunching = false
800 
801                 // dismiss was called during the animation, dismiss again now to actually dismiss.
802                 if (dismissRequested) {
803                     dialog.dismiss()
804                 }
805 
806                 // If necessary, we animate the dialog background when its bounds change. We do it
807                 // at the end of the launch animation, because the lauch animation already correctly
808                 // handles bounds changes.
809                 if (backgroundLayoutListener != null) {
810                     dialogContentWithBackground!!.addOnLayoutChangeListener(
811                         backgroundLayoutListener
812                     )
813                 }
814 
815                 if (hasInstrumentedJank) {
816                     interactionJankMonitor.end(controller.cuj!!.cujType)
817                 }
818             }
819         )
820     }
821 
onDialogDismissednull822     fun onDialogDismissed() {
823         if (Looper.myLooper() != Looper.getMainLooper()) {
824             dialog.context.mainExecutor.execute { onDialogDismissed() }
825             return
826         }
827 
828         // TODO(b/193634619): Support interrupting the launch animation in the middle.
829         if (isLaunching) {
830             dismissRequested = true
831             return
832         }
833 
834         if (isDismissing) {
835             return
836         }
837 
838         isDismissing = true
839         hideDialogIntoView { animationRan: Boolean ->
840             if (animationRan) {
841                 // Instantly dismiss the dialog if we ran the animation into view. If it was
842                 // skipped, dismiss() will run the window animation (which fades out the dialog).
843                 dialog.hide()
844             }
845 
846             dialog.setDismissOverride(null)
847             dialog.dismiss()
848         }
849     }
850 
851     /**
852      * Hide the dialog into the source and call [onAnimationFinished] when the animation is done
853      * (passing animationRan=true) or if it's skipped (passing animationRan=false) to actually
854      * dismiss the dialog.
855      */
hideDialogIntoViewnull856     private fun hideDialogIntoView(onAnimationFinished: (Boolean) -> Unit) {
857         // Remove the layout change listener we have added to the DecorView earlier.
858         if (decorViewLayoutListener != null) {
859             decorView.removeOnLayoutChangeListener(decorViewLayoutListener)
860         }
861 
862         if (!shouldAnimateDialogIntoSource()) {
863             Log.i(TAG, "Skipping animation of dialog into the source")
864             controller.onExitAnimationCancelled()
865             onAnimationFinished(false /* instantDismiss */)
866             onDialogDismissed(this@AnimatedDialog)
867             return
868         }
869 
870         startAnimation(
871             isLaunching = false,
872             onLaunchAnimationStart = {
873                 // Remove the dim background as soon as we start the animation.
874                 dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
875             },
876             onLaunchAnimationEnd = {
877                 val dialogContentWithBackground = this.dialogContentWithBackground!!
878                 dialogContentWithBackground.visibility = View.INVISIBLE
879 
880                 if (backgroundLayoutListener != null) {
881                     dialogContentWithBackground.removeOnLayoutChangeListener(
882                         backgroundLayoutListener
883                     )
884                 }
885 
886                 controller.stopDrawingInOverlay()
887                 synchronizeNextDraw {
888                     onAnimationFinished(true /* instantDismiss */)
889                     onDialogDismissed(this@AnimatedDialog)
890                 }
891             }
892         )
893     }
894 
startAnimationnull895     private fun startAnimation(
896         isLaunching: Boolean,
897         onLaunchAnimationStart: () -> Unit = {},
<lambda>null898         onLaunchAnimationEnd: () -> Unit = {}
899     ) {
900         // Create 2 controllers to animate both the dialog and the source.
901         val startController =
902             if (isLaunching) {
903                 controller.createTransitionController()
904             } else {
905                 GhostedViewTransitionAnimatorController(dialogContentWithBackground!!)
906             }
907         val endController =
908             if (isLaunching) {
909                 GhostedViewTransitionAnimatorController(dialogContentWithBackground!!)
910             } else {
911                 controller.createExitController()
912             }
913         startController.transitionContainer = decorView
914         endController.transitionContainer = decorView
915 
916         val endState = endController.createAnimatorState()
917         val controller =
918             object : TransitionAnimator.Controller {
919                 override var transitionContainer: ViewGroup
920                     get() = startController.transitionContainer
921                     set(value) {
922                         startController.transitionContainer = value
923                         endController.transitionContainer = value
924                     }
925 
926                 // We tell TransitionController that this is always a launch, and handle the launch
927                 // vs return logic internally.
928                 // TODO(b/323863002): maybe move the launch vs return logic out of this class and
929                 //     delegate it to TransitionController?
930                 override val isLaunching: Boolean = true
931 
createAnimatorStatenull932                 override fun createAnimatorState(): TransitionAnimator.State {
933                     return startController.createAnimatorState()
934                 }
935 
onTransitionAnimationStartnull936                 override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
937                     // During launch, onLaunchAnimationStart will be used to remove the temporary
938                     // touch surface ghost so it is important to call this before calling
939                     // onLaunchAnimationStart on the controller (which will create its own ghost).
940                     onLaunchAnimationStart()
941 
942                     startController.onTransitionAnimationStart(isExpandingFullyAbove)
943                     endController.onTransitionAnimationStart(isExpandingFullyAbove)
944                 }
945 
onTransitionAnimationEndnull946                 override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
947                     // onLaunchAnimationEnd is called by an Animator at the end of the animation,
948                     // on a Choreographer animation tick. The following calls will move the animated
949                     // content from the dialog overlay back to its original position, and this
950                     // change must be reflected in the next frame given that we then sync the next
951                     // frame of both the content and dialog ViewRoots. However, in case that content
952                     // is rendered by Compose, whose compositions are also scheduled on a
953                     // Choreographer frame, any state change made *right now* won't be reflected in
954                     // the next frame given that a Choreographer frame can't schedule another and
955                     // have it happen in the same frame. So we post the forwarded calls to
956                     // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring
957                     // that the move of the content back to its original window will be reflected in
958                     // the next frame right after [onLaunchAnimationEnd] is called.
959                     //
960                     // TODO(b/330672236): Move this to TransitionAnimator.
961                     dialog.context.mainExecutor.execute {
962                         startController.onTransitionAnimationEnd(isExpandingFullyAbove)
963                         endController.onTransitionAnimationEnd(isExpandingFullyAbove)
964 
965                         onLaunchAnimationEnd()
966                     }
967                 }
968 
onTransitionAnimationProgressnull969                 override fun onTransitionAnimationProgress(
970                     state: TransitionAnimator.State,
971                     progress: Float,
972                     linearProgress: Float
973                 ) {
974                     startController.onTransitionAnimationProgress(state, progress, linearProgress)
975 
976                     // The end view is visible only iff the starting view is not visible.
977                     state.visible = !state.visible
978                     endController.onTransitionAnimationProgress(state, progress, linearProgress)
979 
980                     // If the dialog content is complex, its dimension might change during the
981                     // launch animation. The animation end position might also change during the
982                     // exit animation, for instance when locking the phone when the dialog is open.
983                     // Therefore we update the end state to the new position/size. Usually the
984                     // dialog dimension or position will change in the early frames, so changing the
985                     // end state shouldn't really be noticeable.
986                     if (endController is GhostedViewTransitionAnimatorController) {
987                         endController.fillGhostedViewState(endState)
988                     }
989                 }
990             }
991 
992         transitionAnimator.startAnimation(controller, endState, originalDialogBackgroundColor)
993     }
994 
shouldAnimateDialogIntoSourcenull995     private fun shouldAnimateDialogIntoSource(): Boolean {
996         // Don't animate if the dialog was previously hidden using hide() or if we disabled the exit
997         // animation.
998         if (exitAnimationDisabled || !dialog.isShowing) {
999             return false
1000         }
1001 
1002         // If we are dreaming, the dialog was probably closed because of that so we don't animate
1003         // into the source.
1004         if (callback.isDreaming()) {
1005             return false
1006         }
1007 
1008         return controller.shouldAnimateExit()
1009     }
1010 
1011     /** A layout listener to animate the change of bounds of the dialog background. */
1012     class AnimatedBoundsLayoutListener : View.OnLayoutChangeListener {
1013         companion object {
1014             private const val ANIMATION_DURATION = 500L
1015         }
1016 
1017         private var lastBounds: Rect? = null
1018         private var currentAnimator: ValueAnimator? = null
1019 
onLayoutChangenull1020         override fun onLayoutChange(
1021             view: View,
1022             left: Int,
1023             top: Int,
1024             right: Int,
1025             bottom: Int,
1026             oldLeft: Int,
1027             oldTop: Int,
1028             oldRight: Int,
1029             oldBottom: Int
1030         ) {
1031             // Don't animate if bounds didn't actually change.
1032             if (left == oldLeft && top == oldTop && right == oldRight && bottom == oldBottom) {
1033                 // Make sure that we that the last bounds set by the animator were not overridden.
1034                 lastBounds?.let { bounds ->
1035                     view.left = bounds.left
1036                     view.top = bounds.top
1037                     view.right = bounds.right
1038                     view.bottom = bounds.bottom
1039                 }
1040                 return
1041             }
1042 
1043             if (lastBounds == null) {
1044                 lastBounds = Rect(oldLeft, oldTop, oldRight, oldBottom)
1045             }
1046 
1047             val bounds = lastBounds!!
1048             val startLeft = bounds.left
1049             val startTop = bounds.top
1050             val startRight = bounds.right
1051             val startBottom = bounds.bottom
1052 
1053             currentAnimator?.cancel()
1054             currentAnimator = null
1055 
1056             val animator =
1057                 ValueAnimator.ofFloat(0f, 1f).apply {
1058                     duration = ANIMATION_DURATION
1059                     interpolator = Interpolators.STANDARD
1060 
1061                     addListener(
1062                         object : AnimatorListenerAdapter() {
1063                             override fun onAnimationEnd(animation: Animator) {
1064                                 currentAnimator = null
1065                             }
1066                         }
1067                     )
1068 
1069                     addUpdateListener { animatedValue ->
1070                         val progress = animatedValue.animatedFraction
1071 
1072                         // Compute new bounds.
1073                         bounds.left = MathUtils.lerp(startLeft, left, progress).roundToInt()
1074                         bounds.top = MathUtils.lerp(startTop, top, progress).roundToInt()
1075                         bounds.right = MathUtils.lerp(startRight, right, progress).roundToInt()
1076                         bounds.bottom = MathUtils.lerp(startBottom, bottom, progress).roundToInt()
1077 
1078                         // Set the new bounds.
1079                         view.left = bounds.left
1080                         view.top = bounds.top
1081                         view.right = bounds.right
1082                         view.bottom = bounds.bottom
1083                     }
1084                 }
1085 
1086             currentAnimator = animator
1087             animator.start()
1088         }
1089     }
1090 
prepareForStackDismissnull1091     fun prepareForStackDismiss() {
1092         if (parentAnimatedDialog == null) {
1093             return
1094         }
1095         parentAnimatedDialog.exitAnimationDisabled = true
1096         parentAnimatedDialog.dialog.hide()
1097         parentAnimatedDialog.prepareForStackDismiss()
1098         parentAnimatedDialog.dialog.dismiss()
1099     }
1100 }
1101