• 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 android.widget.FrameLayout
36 import com.android.internal.jank.InteractionJankMonitor
37 import com.android.internal.jank.InteractionJankMonitor.CujType
38 import com.android.systemui.util.registerAnimationOnBackInvoked
39 import java.lang.IllegalArgumentException
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      */
<lambda>null532     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 
startnull576     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                 // We will make the dialog window (and therefore its DecorView) fullscreen to make
625                 // it possible to animate outside its bounds.
626                 //
627                 // Before that, we add a new View as a child of the DecorView with the same size and
628                 // gravity as that DecorView, then we add all original children of the DecorView to
629                 // that new View. Finally we remove the background of the DecorView and add it to
630                 // the new View, then we make the DecorView fullscreen. This new View now acts as a
631                 // fake (non fullscreen) window.
632                 //
633                 // On top of that, we also add a fullscreen transparent background between the
634                 // DecorView and the view that we added so that we can dismiss the dialog when this
635                 // view is clicked. This is necessary because DecorView overrides onTouchEvent and
636                 // therefore we can't set the click listener directly on the (now fullscreen)
637                 // DecorView.
638                 val fullscreenTransparentBackground = FrameLayout(dialog.context)
639                 decorView.addView(
640                     fullscreenTransparentBackground,
641                     0 /* index */,
642                     FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
643                 )
644 
645                 val dialogContentWithBackground = LaunchableFrameLayout(dialog.context)
646                 dialogContentWithBackground.background = decorView.background
647 
648                 // Make the window background transparent. Note that setting the window (or
649                 // DecorView) background drawable to null leads to issues with background color (not
650                 // being transparent) or with insets that are not refreshed. Therefore we need to
651                 // set it to something not null, hence we are using android.R.color.transparent
652                 // here.
653                 window.setBackgroundDrawableResource(android.R.color.transparent)
654 
655                 // Close the dialog when clicking outside of it.
656                 fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() }
657                 dialogContentWithBackground.isClickable = true
658 
659                 // Make sure the transparent and dialog backgrounds are not focusable by
660                 // accessibility
661                 // features.
662                 fullscreenTransparentBackground.importantForAccessibility =
663                     View.IMPORTANT_FOR_ACCESSIBILITY_NO
664                 dialogContentWithBackground.importantForAccessibility =
665                     View.IMPORTANT_FOR_ACCESSIBILITY_NO
666 
667                 fullscreenTransparentBackground.addView(
668                     dialogContentWithBackground,
669                     FrameLayout.LayoutParams(
670                         window.attributes.width,
671                         window.attributes.height,
672                         window.attributes.gravity
673                     )
674                 )
675 
676                 // Move all original children of the DecorView to the new View we just added.
677                 for (i in 1 until decorView.childCount) {
678                     val view = decorView.getChildAt(1)
679                     decorView.removeViewAt(1)
680                     dialogContentWithBackground.addView(view)
681                 }
682 
683                 // Make the window fullscreen and add a layout listener to ensure it stays
684                 // fullscreen.
685                 window.setLayout(MATCH_PARENT, MATCH_PARENT)
686                 decorViewLayoutListener =
687                     View.OnLayoutChangeListener {
688                         v,
689                         left,
690                         top,
691                         right,
692                         bottom,
693                         oldLeft,
694                         oldTop,
695                         oldRight,
696                         oldBottom ->
697                         if (
698                             window.attributes.width != MATCH_PARENT ||
699                                 window.attributes.height != MATCH_PARENT
700                         ) {
701                             // The dialog size changed, copy its size to dialogContentWithBackground
702                             // and make the dialog window full screen again.
703                             val layoutParams = dialogContentWithBackground.layoutParams
704                             layoutParams.width = window.attributes.width
705                             layoutParams.height = window.attributes.height
706                             dialogContentWithBackground.layoutParams = layoutParams
707                             window.setLayout(MATCH_PARENT, MATCH_PARENT)
708                         }
709                     }
710                 decorView.addOnLayoutChangeListener(decorViewLayoutListener)
711 
712                 dialogContentWithBackground
713             }
714         this.dialogContentWithBackground = dialogContentWithBackground
715         dialogContentWithBackground.setTag(R.id.tag_dialog_background, true)
716 
717         val background = dialogContentWithBackground.background
718         originalDialogBackgroundColor =
719             GhostedViewLaunchAnimatorController.findGradientDrawable(background)
720                 ?.color
721                 ?.defaultColor
722                 ?: Color.BLACK
723 
724         // Make the background view invisible until we start the animation. We use the transition
725         // visibility like GhostView does so that we don't mess up with the accessibility tree (see
726         // b/204944038#comment17). Given that this background implements LaunchableView, we call
727         // setShouldBlockVisibilityChanges() early so that the current visibility (VISIBLE) is
728         // restored at the end of the animation.
729         dialogContentWithBackground.setShouldBlockVisibilityChanges(true)
730         dialogContentWithBackground.setTransitionVisibility(View.INVISIBLE)
731 
732         // Make sure the dialog is visible instantly and does not do any window animation.
733         val attributes = window.attributes
734         attributes.windowAnimations = R.style.Animation_LaunchAnimation
735 
736         // Ensure that the animation is not clipped by the display cut-out when animating this
737         // dialog into an app.
738         attributes.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
739 
740         // Ensure that the animation is not clipped by the navigation/task bars when animating this
741         // dialog into an app.
742         val wasFittingNavigationBars =
743             attributes.fitInsetsTypes and WindowInsets.Type.navigationBars() != 0
744         attributes.fitInsetsTypes =
745             attributes.fitInsetsTypes and WindowInsets.Type.navigationBars().inv()
746 
747         window.attributes = window.attributes
748 
749         // We apply the insets ourselves to make sure that the paddings are set on the correct
750         // View.
751         window.setDecorFitsSystemWindows(false)
752         val viewWithInsets = (dialogContentWithBackground.parent as ViewGroup)
753         viewWithInsets.setOnApplyWindowInsetsListener { view, windowInsets ->
754             val type =
755                 if (wasFittingNavigationBars) {
756                     WindowInsets.Type.displayCutout() or WindowInsets.Type.navigationBars()
757                 } else {
758                     WindowInsets.Type.displayCutout()
759                 }
760 
761             val insets = windowInsets.getInsets(type)
762             view.setPadding(insets.left, insets.top, insets.right, insets.bottom)
763             WindowInsets.CONSUMED
764         }
765 
766         // Start the animation once the background view is properly laid out.
767         dialogContentWithBackground.addOnLayoutChangeListener(
768             object : View.OnLayoutChangeListener {
769                 override fun onLayoutChange(
770                     v: View,
771                     left: Int,
772                     top: Int,
773                     right: Int,
774                     bottom: Int,
775                     oldLeft: Int,
776                     oldTop: Int,
777                     oldRight: Int,
778                     oldBottom: Int
779                 ) {
780                     dialogContentWithBackground.removeOnLayoutChangeListener(this)
781 
782                     isOriginalDialogViewLaidOut = true
783                     maybeStartLaunchAnimation()
784                 }
785             }
786         )
787 
788         // Disable the dim. We will enable it once we start the animation.
789         window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
790 
791         // Override the dialog dismiss() so that we can animate the exit before actually dismissing
792         // the dialog.
793         dialog.setDismissOverride(this::onDialogDismissed)
794 
795         if (featureFlags.isPredictiveBackQsDialogAnim) {
796             // TODO(b/265923095) Improve animations for QS dialogs on configuration change
797             dialog.registerAnimationOnBackInvoked(targetView = dialogContentWithBackground)
798         }
799 
800         // Show the dialog.
801         dialog.show()
802         moveSourceDrawingToDialog()
803     }
804 
moveSourceDrawingToDialognull805     private fun moveSourceDrawingToDialog() {
806         if (decorView.viewRootImpl == null) {
807             // Make sure that we have access to the dialog view root to move the drawing to the
808             // dialog overlay.
809             decorView.post(::moveSourceDrawingToDialog)
810             return
811         }
812 
813         // Move the drawing of the source in the overlay of this dialog, then animate. We trigger a
814         // one-off synchronization to make sure that this is done in sync between the two different
815         // windows.
816         controller.startDrawingInOverlayOf(decorView)
817         synchronizeNextDraw(
818             then = {
819                 isSourceDrawnInDialog = true
820                 maybeStartLaunchAnimation()
821             }
822         )
823     }
824 
825     /**
826      * Synchronize the next draw of the source and dialog view roots so that they are performed at
827      * the same time, in the same transaction. This is necessary to make sure that the source is
828      * drawn in the overlay at the same time as it is removed from its original position (or
829      * inversely, removed from the overlay when the source is moved back to its original position).
830      */
synchronizeNextDrawnull831     private fun synchronizeNextDraw(then: () -> Unit) {
832         val controllerRootView = controller.viewRoot?.view
833         if (forceDisableSynchronization || controllerRootView == null) {
834             // Don't synchronize when inside an automated test or if the controller root view is
835             // detached.
836             then()
837             return
838         }
839 
840         ViewRootSync.synchronizeNextDraw(controllerRootView, decorView, then)
841         decorView.invalidate()
842         controllerRootView.invalidate()
843     }
844 
findFirstViewGroupWithBackgroundnull845     private fun findFirstViewGroupWithBackground(view: View): ViewGroup? {
846         if (view !is ViewGroup) {
847             return null
848         }
849 
850         if (view.background != null) {
851             return view
852         }
853 
854         for (i in 0 until view.childCount) {
855             val match = findFirstViewGroupWithBackground(view.getChildAt(i))
856             if (match != null) {
857                 return match
858             }
859         }
860 
861         return null
862     }
863 
maybeStartLaunchAnimationnull864     private fun maybeStartLaunchAnimation() {
865         if (!isSourceDrawnInDialog || !isOriginalDialogViewLaidOut) {
866             return
867         }
868 
869         // Show the background dim.
870         dialog.window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
871 
872         startAnimation(
873             isLaunching = true,
874             onLaunchAnimationEnd = {
875                 isLaunching = false
876 
877                 // dismiss was called during the animation, dismiss again now to actually dismiss.
878                 if (dismissRequested) {
879                     dialog.dismiss()
880                 }
881 
882                 // If necessary, we animate the dialog background when its bounds change. We do it
883                 // at the end of the launch animation, because the lauch animation already correctly
884                 // handles bounds changes.
885                 if (backgroundLayoutListener != null) {
886                     dialogContentWithBackground!!.addOnLayoutChangeListener(
887                         backgroundLayoutListener
888                     )
889                 }
890 
891                 if (hasInstrumentedJank) {
892                     interactionJankMonitor.end(controller.cuj!!.cujType)
893                 }
894             }
895         )
896     }
897 
onDialogDismissednull898     fun onDialogDismissed() {
899         if (Looper.myLooper() != Looper.getMainLooper()) {
900             dialog.context.mainExecutor.execute { onDialogDismissed() }
901             return
902         }
903 
904         // TODO(b/193634619): Support interrupting the launch animation in the middle.
905         if (isLaunching) {
906             dismissRequested = true
907             return
908         }
909 
910         if (isDismissing) {
911             return
912         }
913 
914         isDismissing = true
915         hideDialogIntoView { animationRan: Boolean ->
916             if (animationRan) {
917                 // Instantly dismiss the dialog if we ran the animation into view. If it was
918                 // skipped, dismiss() will run the window animation (which fades out the dialog).
919                 dialog.hide()
920             }
921 
922             dialog.setDismissOverride(null)
923             dialog.dismiss()
924         }
925     }
926 
927     /**
928      * Hide the dialog into the source and call [onAnimationFinished] when the animation is done
929      * (passing animationRan=true) or if it's skipped (passing animationRan=false) to actually
930      * dismiss the dialog.
931      */
hideDialogIntoViewnull932     private fun hideDialogIntoView(onAnimationFinished: (Boolean) -> Unit) {
933         // Remove the layout change listener we have added to the DecorView earlier.
934         if (decorViewLayoutListener != null) {
935             decorView.removeOnLayoutChangeListener(decorViewLayoutListener)
936         }
937 
938         if (!shouldAnimateDialogIntoSource()) {
939             Log.i(TAG, "Skipping animation of dialog into the source")
940             controller.onExitAnimationCancelled()
941             onAnimationFinished(false /* instantDismiss */)
942             onDialogDismissed(this@AnimatedDialog)
943             return
944         }
945 
946         startAnimation(
947             isLaunching = false,
948             onLaunchAnimationStart = {
949                 // Remove the dim background as soon as we start the animation.
950                 dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
951             },
952             onLaunchAnimationEnd = {
953                 val dialogContentWithBackground = this.dialogContentWithBackground!!
954                 dialogContentWithBackground.visibility = View.INVISIBLE
955 
956                 if (backgroundLayoutListener != null) {
957                     dialogContentWithBackground.removeOnLayoutChangeListener(
958                         backgroundLayoutListener
959                     )
960                 }
961 
962                 controller.stopDrawingInOverlay()
963                 synchronizeNextDraw {
964                     onAnimationFinished(true /* instantDismiss */)
965                     onDialogDismissed(this@AnimatedDialog)
966                 }
967             }
968         )
969     }
970 
startAnimationnull971     private fun startAnimation(
972         isLaunching: Boolean,
973         onLaunchAnimationStart: () -> Unit = {},
<lambda>null974         onLaunchAnimationEnd: () -> Unit = {}
975     ) {
976         // Create 2 controllers to animate both the dialog and the source.
977         val startController =
978             if (isLaunching) {
979                 controller.createLaunchController()
980             } else {
981                 GhostedViewLaunchAnimatorController(dialogContentWithBackground!!)
982             }
983         val endController =
984             if (isLaunching) {
985                 GhostedViewLaunchAnimatorController(dialogContentWithBackground!!)
986             } else {
987                 controller.createExitController()
988             }
989         startController.launchContainer = decorView
990         endController.launchContainer = decorView
991 
992         val endState = endController.createAnimatorState()
993         val controller =
994             object : LaunchAnimator.Controller {
995                 override var launchContainer: ViewGroup
996                     get() = startController.launchContainer
997                     set(value) {
998                         startController.launchContainer = value
999                         endController.launchContainer = value
1000                     }
1001 
createAnimatorStatenull1002                 override fun createAnimatorState(): LaunchAnimator.State {
1003                     return startController.createAnimatorState()
1004                 }
1005 
onLaunchAnimationStartnull1006                 override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
1007                     // During launch, onLaunchAnimationStart will be used to remove the temporary
1008                     // touch surface ghost so it is important to call this before calling
1009                     // onLaunchAnimationStart on the controller (which will create its own ghost).
1010                     onLaunchAnimationStart()
1011 
1012                     startController.onLaunchAnimationStart(isExpandingFullyAbove)
1013                     endController.onLaunchAnimationStart(isExpandingFullyAbove)
1014                 }
1015 
onLaunchAnimationEndnull1016                 override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
1017                     // onLaunchAnimationEnd is called by an Animator at the end of the animation,
1018                     // on a Choreographer animation tick. The following calls will move the animated
1019                     // content from the dialog overlay back to its original position, and this
1020                     // change must be reflected in the next frame given that we then sync the next
1021                     // frame of both the content and dialog ViewRoots. However, in case that content
1022                     // is rendered by Compose, whose compositions are also scheduled on a
1023                     // Choreographer frame, any state change made *right now* won't be reflected in
1024                     // the next frame given that a Choreographer frame can't schedule another and
1025                     // have it happen in the same frame. So we post the forwarded calls to
1026                     // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring
1027                     // that the move of the content back to its original window will be reflected in
1028                     // the next frame right after [onLaunchAnimationEnd] is called.
1029                     dialog.context.mainExecutor.execute {
1030                         startController.onLaunchAnimationEnd(isExpandingFullyAbove)
1031                         endController.onLaunchAnimationEnd(isExpandingFullyAbove)
1032 
1033                         onLaunchAnimationEnd()
1034                     }
1035                 }
1036 
onLaunchAnimationProgressnull1037                 override fun onLaunchAnimationProgress(
1038                     state: LaunchAnimator.State,
1039                     progress: Float,
1040                     linearProgress: Float
1041                 ) {
1042                     startController.onLaunchAnimationProgress(state, progress, linearProgress)
1043 
1044                     // The end view is visible only iff the starting view is not visible.
1045                     state.visible = !state.visible
1046                     endController.onLaunchAnimationProgress(state, progress, linearProgress)
1047 
1048                     // If the dialog content is complex, its dimension might change during the
1049                     // launch animation. The animation end position might also change during the
1050                     // exit animation, for instance when locking the phone when the dialog is open.
1051                     // Therefore we update the end state to the new position/size. Usually the
1052                     // dialog dimension or position will change in the early frames, so changing the
1053                     // end state shouldn't really be noticeable.
1054                     if (endController is GhostedViewLaunchAnimatorController) {
1055                         endController.fillGhostedViewState(endState)
1056                     }
1057                 }
1058             }
1059 
1060         launchAnimator.startAnimation(controller, endState, originalDialogBackgroundColor)
1061     }
1062 
shouldAnimateDialogIntoSourcenull1063     private fun shouldAnimateDialogIntoSource(): Boolean {
1064         // Don't animate if the dialog was previously hidden using hide() or if we disabled the exit
1065         // animation.
1066         if (exitAnimationDisabled || !dialog.isShowing) {
1067             return false
1068         }
1069 
1070         // If we are dreaming, the dialog was probably closed because of that so we don't animate
1071         // into the source.
1072         if (callback.isDreaming()) {
1073             return false
1074         }
1075 
1076         return controller.shouldAnimateExit()
1077     }
1078 
1079     /** A layout listener to animate the change of bounds of the dialog background. */
1080     class AnimatedBoundsLayoutListener : View.OnLayoutChangeListener {
1081         companion object {
1082             private const val ANIMATION_DURATION = 500L
1083         }
1084 
1085         private var lastBounds: Rect? = null
1086         private var currentAnimator: ValueAnimator? = null
1087 
onLayoutChangenull1088         override fun onLayoutChange(
1089             view: View,
1090             left: Int,
1091             top: Int,
1092             right: Int,
1093             bottom: Int,
1094             oldLeft: Int,
1095             oldTop: Int,
1096             oldRight: Int,
1097             oldBottom: Int
1098         ) {
1099             // Don't animate if bounds didn't actually change.
1100             if (left == oldLeft && top == oldTop && right == oldRight && bottom == oldBottom) {
1101                 // Make sure that we that the last bounds set by the animator were not overridden.
1102                 lastBounds?.let { bounds ->
1103                     view.left = bounds.left
1104                     view.top = bounds.top
1105                     view.right = bounds.right
1106                     view.bottom = bounds.bottom
1107                 }
1108                 return
1109             }
1110 
1111             if (lastBounds == null) {
1112                 lastBounds = Rect(oldLeft, oldTop, oldRight, oldBottom)
1113             }
1114 
1115             val bounds = lastBounds!!
1116             val startLeft = bounds.left
1117             val startTop = bounds.top
1118             val startRight = bounds.right
1119             val startBottom = bounds.bottom
1120 
1121             currentAnimator?.cancel()
1122             currentAnimator = null
1123 
1124             val animator =
1125                 ValueAnimator.ofFloat(0f, 1f).apply {
1126                     duration = ANIMATION_DURATION
1127                     interpolator = Interpolators.STANDARD
1128 
1129                     addListener(
1130                         object : AnimatorListenerAdapter() {
1131                             override fun onAnimationEnd(animation: Animator) {
1132                                 currentAnimator = null
1133                             }
1134                         }
1135                     )
1136 
1137                     addUpdateListener { animatedValue ->
1138                         val progress = animatedValue.animatedFraction
1139 
1140                         // Compute new bounds.
1141                         bounds.left = MathUtils.lerp(startLeft, left, progress).roundToInt()
1142                         bounds.top = MathUtils.lerp(startTop, top, progress).roundToInt()
1143                         bounds.right = MathUtils.lerp(startRight, right, progress).roundToInt()
1144                         bounds.bottom = MathUtils.lerp(startBottom, bottom, progress).roundToInt()
1145 
1146                         // Set the new bounds.
1147                         view.left = bounds.left
1148                         view.top = bounds.top
1149                         view.right = bounds.right
1150                         view.bottom = bounds.bottom
1151                     }
1152                 }
1153 
1154             currentAnimator = animator
1155             animator.start()
1156         }
1157     }
1158 
prepareForStackDismissnull1159     fun prepareForStackDismiss() {
1160         if (parentAnimatedDialog == null) {
1161             return
1162         }
1163         parentAnimatedDialog.exitAnimationDisabled = true
1164         parentAnimatedDialog.dialog.hide()
1165         parentAnimatedDialog.prepareForStackDismiss()
1166         parentAnimatedDialog.dialog.dismiss()
1167     }
1168 }
1169