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