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