diff options
| author | 2021-11-23 14:44:18 +0100 | |
|---|---|---|
| committer | 2021-12-01 10:53:23 +0100 | |
| commit | c424d4801f7e1757e5669253e9ba9a229895cecf (patch) | |
| tree | 16df49bf0ecfda201c15f5af059fa2612319bf6a | |
| parent | a5f83144a308b4bd0b276bf62605d87682b7adaa (diff) | |
Refactor DialogLaunchAnimator and remove host dialog (1/2)
This CL is a refactoring of the DialogLaunchAnimator and a major change
in how it works internally.
Before this CL, when DialogLaunchAnimator.show(originalDialog, ...) was
called, it would create a fullscreen dialog (called hostDialog) to
which we would add the content view stolen from the originalDialog (that
would then be hidden). We would then run the launch animation in the
host dialog and listen for show(), hide() or dismiss() calls to the
originalDialog to know when we should show(), hide() or dismiss() the
hostDialog.
The main reason we did that was because there was no way to override the
dismiss() behavior of the originalDialog, which we need to do given that
we want to animate the dialog into the view that showed the dialog
before actually dismissing it.
This approach had multiple downsides:
- We were showing the dialog content view inside a different window
than the one in which it was created, therefore any change made to or
any listener added to the originalDialog window was lost.
- Code calling DialogLaunchAnimator.showFromView needed to know that
their dialog content will be moved to another window, which is
unexpected and can lead to subtle bugs.
- We were waiting for 2 dialogs to be shown instead of 1 before being
able to start the animation.
This CL does what we should have done since the beginning: it adds a
hidden API to Dialog so that we can override what happens when
Dialog.dismiss() is called. This allows the DialogLaunchAnimator to run
the exit animation into the view that triggered the dialog before
actually dismissing it. The only modification that
DialogLaunchAnimator.show(originalDialog, ...) now does to the
originalDialog is making its window fullscreen and inserting two views
between the originalDialog DecorView and its children. The first
inserted view is a fullscreen transparent background used to dismiss the
dialog when the user taps outside the dialog content. The second
inserted view is a View that we size and position the same way that the
DecorView was before we made it fullscreen, and to which we set the
originalDialog background. This view now serves as a "fake window" with
the same size, background and position as the original window
(DecorView).
This CL improves the time to start the launch animation by 20-25%
(measured in a totally unrigorous logcat way).
Bug: 193634619
Test: atest DialogLaunchAnimatorTest
Change-Id: If09eda7b06e83b3ed7714cec97afef08b3d9fd3e
16 files changed, 356 insertions, 703 deletions
diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index 9833ed60fe46..306035341ea3 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -151,6 +151,9 @@ public class Dialog implements DialogInterface, Window.Callback, private final Runnable mDismissAction = this::dismissDialog; + /** A {@link Runnable} to run instead of dismissing when {@link #dismiss()} is called. */ + private Runnable mDismissOverride; + /** * Creates a dialog window that uses the default dialog theme. * <p> @@ -370,6 +373,11 @@ public class Dialog implements DialogInterface, Window.Callback, */ @Override public void dismiss() { + if (mDismissOverride != null) { + mDismissOverride.run(); + return; + } + if (Looper.myLooper() == mHandler.getLooper()) { dismissDialog(); } else { @@ -1354,6 +1362,21 @@ public class Dialog implements DialogInterface, Window.Callback, mDismissMessage = msg; } + /** + * Set a {@link Runnable} to run when this dialog is dismissed instead of directly dismissing + * it. This allows to animate the dialog in its window before dismissing it. + * + * Note that {@code override} should always end up calling this method with {@code null} + * followed by a call to {@link #dismiss() dismiss} to actually dismiss the dialog. + * + * @see #dismiss() + * + * @hide + */ + public void setDismissOverride(@Nullable Runnable override) { + mDismissOverride = override; + } + /** @hide */ public boolean takeCancelAndDismissListeners(@Nullable String msg, @Nullable OnCancelListener cancel, @Nullable OnDismissListener dismiss) { diff --git a/packages/SystemUI/animation/res/anim/launch_host_dialog_enter.xml b/packages/SystemUI/animation/res/anim/launch_dialog_enter.xml index c6b87d38f7da..c6b87d38f7da 100644 --- a/packages/SystemUI/animation/res/anim/launch_host_dialog_enter.xml +++ b/packages/SystemUI/animation/res/anim/launch_dialog_enter.xml diff --git a/packages/SystemUI/animation/res/anim/launch_host_dialog_exit.xml b/packages/SystemUI/animation/res/anim/launch_dialog_exit.xml index a0f441eaeed4..a0f441eaeed4 100644 --- a/packages/SystemUI/animation/res/anim/launch_host_dialog_exit.xml +++ b/packages/SystemUI/animation/res/anim/launch_dialog_exit.xml diff --git a/packages/SystemUI/animation/res/values/ids.xml b/packages/SystemUI/animation/res/values/ids.xml index c4cb89fecccb..ef60a248f79a 100644 --- a/packages/SystemUI/animation/res/values/ids.xml +++ b/packages/SystemUI/animation/res/values/ids.xml @@ -16,5 +16,4 @@ --> <resources> <item type="id" name="launch_animation_running"/> - <item type="id" name="dialog_content_parent" /> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/animation/res/values/styles.xml b/packages/SystemUI/animation/res/values/styles.xml index ad06c9192bc3..3b3f7f6128fa 100644 --- a/packages/SystemUI/animation/res/values/styles.xml +++ b/packages/SystemUI/animation/res/values/styles.xml @@ -15,15 +15,10 @@ limitations under the License. --> <resources> - <style name="HostDialogTheme"> - <item name="android:windowAnimationStyle">@style/Animation.HostDialog</item> - <item name="android:windowIsFloating">false</item> - <item name="android:backgroundDimEnabled">true</item> - <item name="android:navigationBarColor">@android:color/transparent</item> - </style> - - <style name="Animation.HostDialog" parent="@android:style/Animation"> - <item name="android:windowEnterAnimation">@anim/launch_host_dialog_enter</item> - <item name="android:windowExitAnimation">@anim/launch_host_dialog_exit</item> + <!-- An animation used by DialogLaunchAnimator to make a dialog appear instantly (to animate --> + <!-- in-window) and disappear by fading out (when the exit into view is disabled). --> + <style name="Animation.LaunchAnimation" parent="@android:style/Animation"> + <item name="android:windowEnterAnimation">@anim/launch_dialog_enter</item> + <item name="android:windowExitAnimation">@anim/launch_dialog_exit</item> </style> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index e5726b08aff4..de82ebdc6b1c 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -24,22 +24,19 @@ import android.content.Context import android.graphics.Color import android.graphics.Rect import android.os.Looper +import android.service.dreams.IDreamManager import android.util.Log import android.util.MathUtils import android.view.GhostView import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewTreeObserver.OnPreDrawListener -import android.view.WindowInsets import android.view.WindowManager -import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR -import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN -import android.view.WindowManagerPolicyConstants import android.widget.FrameLayout import kotlin.math.roundToInt private const val TAG = "DialogLaunchAnimator" -private val DIALOG_CONTENT_PARENT_ID = R.id.dialog_content_parent /** * A class that allows dialogs to be started in a seamless way from a view that is transforming @@ -48,7 +45,7 @@ private val DIALOG_CONTENT_PARENT_ID = R.id.dialog_content_parent class DialogLaunchAnimator( private val context: Context, private val launchAnimator: LaunchAnimator, - private val hostDialogProvider: HostDialogProvider + private val dreamManager: IDreamManager ) { private companion object { private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running @@ -62,41 +59,38 @@ class DialogLaunchAnimator( private val openedDialogs = hashSetOf<AnimatedDialog>() /** - * Show [dialog] by expanding it from [view]. If [animateBackgroundBoundsChange] is true, then - * the background of the dialog will be animated when the dialog bounds change. + * Show [dialog] by expanding it from [view]. If [view] is a view inside another dialog that was + * shown using this method, then we will animate from that dialog instead. * - * Caveats: When calling this function, the dialog content view will actually be stolen and - * attached to a different dialog (and thus a different window) which means that the actual - * dialog window will never be drawn. Moreover, unless [dialog] is a [ListenableDialog], you - * must call dismiss(), hide() and show() on the [Dialog] returned by this function to actually - * dismiss, hide or show the dialog. + * If [animateBackgroundBoundsChange] is true, then the background of the dialog will be + * animated when the dialog bounds change. + * + * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be + * made fullscreen and 2 views will be inserted between the dialog DecorView and its children. */ @JvmOverloads fun showFromView( dialog: Dialog, view: View, animateBackgroundBoundsChange: Boolean = false - ): Dialog { + ) { if (Looper.myLooper() != Looper.getMainLooper()) { throw IllegalStateException( "showFromView must be called from the main thread and dialog must be created in " + "the main thread") } - // If the parent of the view we are launching from is the background of some other animated - // dialog, then this means the caller intent is to launch a dialog from another dialog. In - // this case, we also animate the parent (which is the dialog background). - val animatedParent = openedDialogs.firstOrNull { - it.dialogContentWithBackground == view || it.dialogContentWithBackground == view.parent - } - val dialogContentWithBackground = animatedParent?.dialogContentWithBackground - val animateFrom = dialogContentWithBackground ?: view + // If the view we are launching from belongs to another dialog, then this means the caller + // intent is to launch a dialog from another dialog. + val animatedParent = openedDialogs + .firstOrNull { it.dialog.window.decorView.viewRootImpl == view.viewRootImpl } + val animateFrom = animatedParent?.dialogContentWithBackground ?: view // Make sure we don't run the launch animation from the same view twice at the same time. if (animateFrom.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) { Log.e(TAG, "Not running dialog launch animation as there is already one running") dialog.show() - return dialog + return } animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true) @@ -104,82 +98,36 @@ class DialogLaunchAnimator( val animatedDialog = AnimatedDialog( context, launchAnimator, - hostDialogProvider, + dreamManager, animateFrom, onDialogDismissed = { openedDialogs.remove(it) }, - originalDialog = dialog, + dialog = dialog, animateBackgroundBoundsChange, animatedParent ) - val hostDialog = animatedDialog.hostDialog - openedDialogs.add(animatedDialog) - - // If the dialog is dismissed/hidden/shown, then we should actually dismiss/hide/show the - // host dialog. - if (dialog is ListenableDialog) { - dialog.addListener(object : DialogListener { - override fun onDismiss(reason: DialogListener.DismissReason) { - dialog.removeListener(this) - - // We disable the exit animation if we are dismissing the dialog because the - // device is being locked, otherwise the animation looks bad if AOD is enabled. - // If AOD is disabled the screen will directly becomes black and we won't see - // the animation anyways. - if (reason == DialogListener.DismissReason.DEVICE_LOCKED) { - animatedDialog.exitAnimationDisabled = true - } - - hostDialog.dismiss() - } - - override fun onHide() { - if (animatedDialog.ignoreNextCallToHide) { - animatedDialog.ignoreNextCallToHide = false - return - } - - hostDialog.hide() - } - - override fun onShow() { - hostDialog.show() - - // We don't actually want to show the original dialog, so hide it. - animatedDialog.ignoreNextCallToHide = true - dialog.hide() - } - - override fun onSizeChanged() { - animatedDialog.onOriginalDialogSizeChanged() - } - - override fun prepareForStackDismiss() { - animatedDialog.touchSurface = animatedDialog.prepareForStackDismiss() - } - }) - } + openedDialogs.add(animatedDialog) animatedDialog.start() - return hostDialog } /** - * Launch [dialog] from a [parentHostDialog] as returned by [showFromView]. This will allow - * for dismissing the whole stack. - * - * This will return a new host dialog, with the same caveat as [showFromView]. + * Launch [dialog] from [another dialog][animateFrom] that was shown using [showFromView]. This + * will allow for dismissing the whole stack. * - * @see DialogListener.prepareForStackDismiss + * @see dismissStack */ fun showFromDialog( dialog: Dialog, - parentHostDialog: Dialog, + animateFrom: Dialog, animateBackgroundBoundsChange: Boolean = false - ): Dialog { - val view = parentHostDialog.findViewById<ViewGroup>(DIALOG_CONTENT_PARENT_ID) - ?.getChildAt(0) - ?: throw IllegalStateException("No dialog content parent found in host dialog") - return showFromView(dialog, view, animateBackgroundBoundsChange) + ) { + val view = openedDialogs + .firstOrNull { it.dialog == animateFrom } + ?.dialogContentWithBackground + ?: throw IllegalStateException( + "The animateFrom dialog was not animated using " + + "DialogLaunchAnimator.showFrom(View|Dialog)") + showFromView(dialog, view, animateBackgroundBoundsChange) } /** @@ -195,69 +143,23 @@ class DialogLaunchAnimator( fun disableAllCurrentDialogsExitAnimations() { openedDialogs.forEach { it.exitAnimationDisabled = true } } -} -interface HostDialogProvider { /** - * Create a host dialog that will be used to host a launch animation. This host dialog must: - * 1. call [onCreateCallback] in its onCreate() method, e.g. right after calling - * super.onCreate(). - * 2. call [dismissOverride] instead of doing any dismissing logic. The actual dismissing - * logic should instead be done inside the lambda passed to [dismissOverride], which will - * be called after the exit animation. - * 3. Be full screen, i.e. have a window matching its parent size. - * - * See SystemUIHostDialogProvider for an example of implementation. + * Dismiss [dialog]. If it was launched from another dialog using [showFromView], also dismiss + * the stack of dialogs, animating back to the original touchSurface. */ - fun createHostDialog( - context: Context, - theme: Int, - onCreateCallback: () -> Unit, - dismissOverride: (() -> Unit) -> Unit - ): Dialog -} - -/** A dialog to/from which we can add/remove listeners. */ -interface ListenableDialog { - /** Add [listener] to the listeners. */ - fun addListener(listener: DialogListener) - - /** Remove [listener] from the listeners. */ - fun removeListener(listener: DialogListener) -} - -interface DialogListener { - /** The reason why a dialog was dismissed. */ - enum class DismissReason { - UNKNOWN, - - /** The device was locked, which dismissed this dialog. */ - DEVICE_LOCKED, + fun dismissStack(dialog: Dialog) { + openedDialogs + .firstOrNull { it.dialog == dialog } + ?.let { it.touchSurface = it.prepareForStackDismiss() } + dialog.dismiss() } - - /** Called when this dialog dismiss() is called. */ - fun onDismiss(reason: DismissReason) - - /** Called when this dialog hide() is called. */ - fun onHide() - - /** Called when this dialog show() is called. */ - fun onShow() - - /** - * Call before dismissing a stack of dialogs (dialogs launched from dialogs), so the topmost - * can animate directly into the original `touchSurface`. - */ - fun prepareForStackDismiss() - - /** Called when this dialog size might have changed, e.g. because of configuration changes. */ - fun onSizeChanged() } private class AnimatedDialog( private val context: Context, private val launchAnimator: LaunchAnimator, - hostDialogProvider: HostDialogProvider, + private val dreamManager: IDreamManager, /** The view that triggered the dialog after being tapped. */ var touchSurface: View, @@ -268,36 +170,33 @@ private class AnimatedDialog( */ private val onDialogDismissed: (AnimatedDialog) -> Unit, - /** The original dialog whose content will be shown and animate in/out in [hostDialog]. */ - private val originalDialog: Dialog, + /** The dialog to show and animate. */ + val dialog: Dialog, /** Whether we should animate the dialog background when its bounds change. */ private val animateBackgroundBoundsChange: Boolean, - /** Launch animation corresponding to the parent [hostDialog]. */ + /** Launch animation corresponding to the parent [AnimatedDialog]. */ private val parentAnimatedDialog: AnimatedDialog? = null ) { /** - * The fullscreen dialog to which we will add the content view [originalDialogView] of - * [originalDialog]. - */ - val hostDialog = hostDialogProvider.createHostDialog( - context, R.style.HostDialogTheme, this::onHostDialogCreated, this::onHostDialogDismissed) - - /** The root content view of [hostDialog]. */ - private val hostDialogRoot = FrameLayout(context) + * The DecorView of this dialog window. + * + * Note that we access this DecorView lazily to avoid accessing it before the dialog is created, + * which can sometimes cause crashes (e.g. with the Cast dialog). + */ + private val decorView by lazy { dialog.window!!.decorView as ViewGroup } /** * The dialog content with its background. When animating a fullscreen dialog, this is just the * first ViewGroup of the dialog that has a background. When animating a normal (not fullscreen) * dialog, this is an additional view that serves as a fake window that will have the same size - * as the original dialog window and to which we will set the original dialog window background. + * as the dialog window initially had and to which we will set the dialog window background. */ var dialogContentWithBackground: ViewGroup? = null /** - * The background color of [originalDialogView], taking into consideration the [originalDialog] - * window background color. + * The background color of [dialog], taking into consideration its window background color. */ private var originalDialogBackgroundColor = Color.BLACK @@ -310,75 +209,182 @@ private class AnimatedDialog( private var isDismissing = false private var dismissRequested = false - var ignoreNextCallToHide = false var exitAnimationDisabled = false private var isTouchSurfaceGhostDrawn = false private var isOriginalDialogViewLaidOut = false - private var backgroundLayoutListener = if (animateBackgroundBoundsChange) { + + /** A layout listener to animate the dialog height change. */ + private val backgroundLayoutListener = if (animateBackgroundBoundsChange) { AnimatedBoundsLayoutListener() } else { null } + /* + * A layout listener in case the dialog (window) size changes (for instance because of a + * configuration change) to ensure that the dialog stays full width. + */ + private var decorViewLayoutListener: View.OnLayoutChangeListener? = null + fun start() { - // Show the host (fullscreen) dialog, to which we will add the stolen dialog view. - hostDialog.show() + // Create the dialog so that its onCreate() method is called, which usually sets the dialog + // content. + dialog.create() + + val window = dialog.window!! + val isWindowFullScreen = + window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT + val dialogContentWithBackground = if (isWindowFullScreen) { + // If the dialog window is already fullscreen, then we look for the first ViewGroup that + // has a background (and is not the DecorView, which always has a background) and + // animate towards that ViewGroup given that this is probably what represents the actual + // dialog view. + var viewGroupWithBackground: ViewGroup? = null + for (i in 0 until decorView.childCount) { + viewGroupWithBackground = findFirstViewGroupWithBackground(decorView.getChildAt(i)) + if (viewGroupWithBackground != null) { + break + } + } - // Steal the dialog view. We do that by showing it but preventing it from drawing, then - // hiding it as soon as its content is available. - stealOriginalDialogContentView(then = this::showDialogFromView) - } + // Animate that view with the background. Throw if we didn't find one, because otherwise + // it's not clear what we should animate. + viewGroupWithBackground + ?: throw IllegalStateException("Unable to find ViewGroup with background") + } else { + // We will make the dialog window (and therefore its DecorView) fullscreen to make it + // possible to animate outside its bounds. + // + // Before that, we add a new View as a child of the DecorView with the same size and + // gravity as that DecorView, then we add all original children of the DecorView to that + // new View. Finally we remove the background of the DecorView and add it to the new + // View, then we make the DecorView fullscreen. This new View now acts as a fake (non + // fullscreen) window. + // + // On top of that, we also add a fullscreen transparent background between the DecorView + // and the view that we added so that we can dismiss the dialog when this view is + // clicked. This is necessary because DecorView overrides onTouchEvent and therefore we + // can't set the click listener directly on the (now fullscreen) DecorView. + val fullscreenTransparentBackground = FrameLayout(context) + decorView.addView( + fullscreenTransparentBackground, + 0 /* index */, + FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + ) + + val dialogContentWithBackground = FrameLayout(context) + dialogContentWithBackground.background = decorView.background + + // Make the window background transparent. Note that setting the window (or DecorView) + // background drawable to null leads to issues with background color (not being + // transparent) or with insets that are not refreshed. Therefore we need to set it to + // something not null, hence we are using android.R.color.transparent here. + window.setBackgroundDrawableResource(android.R.color.transparent) + + // Close the dialog when clicking outside of it. + fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() } + dialogContentWithBackground.isClickable = true - private fun onHostDialogCreated() { - // Make the dialog fullscreen with a transparent background. - hostDialog.setContentView( - hostDialogRoot, - ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT + fullscreenTransparentBackground.addView( + dialogContentWithBackground, + FrameLayout.LayoutParams( + window.attributes.width, + window.attributes.height, + window.attributes.gravity + ) ) - ) - val window = hostDialog.window - ?: throw IllegalStateException("There is no window associated to the host dialog") - window.setBackgroundDrawableResource(android.R.color.transparent) - - // If we are using gesture navigation, then we can overlay the navigation/task bars with - // the host dialog. - val navigationMode = context.resources.getInteger( - com.android.internal.R.integer.config_navBarInteractionMode) - if (navigationMode == WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL) { - window.attributes.fitInsetsTypes = window.attributes.fitInsetsTypes and - WindowInsets.Type.navigationBars().inv() - window.addFlags(FLAG_LAYOUT_IN_SCREEN or FLAG_LAYOUT_INSET_DECOR) - window.setDecorFitsSystemWindows(false) + // Move all original children of the DecorView to the new View we just added. + for (i in 1 until decorView.childCount) { + val view = decorView.getChildAt(1) + decorView.removeViewAt(1) + dialogContentWithBackground.addView(view) + } + + // Make the window fullscreen and add a layout listener to ensure it stays fullscreen. + window.setLayout(MATCH_PARENT, MATCH_PARENT) + decorViewLayoutListener = View.OnLayoutChangeListener { + v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> + if (window.attributes.width != MATCH_PARENT || + window.attributes.height != MATCH_PARENT) { + // The dialog size changed, copy its size to dialogContentWithBackground and + // make the dialog window full screen again. + val layoutParams = dialogContentWithBackground.layoutParams + layoutParams.width = window.attributes.width + layoutParams.height = window.attributes.height + dialogContentWithBackground.layoutParams = layoutParams + window.setLayout(MATCH_PARENT, MATCH_PARENT) + } + } + decorView.addOnLayoutChangeListener(decorViewLayoutListener) + + dialogContentWithBackground } + this.dialogContentWithBackground = dialogContentWithBackground + + val background = dialogContentWithBackground.background + originalDialogBackgroundColor = + GhostedViewLaunchAnimatorController.findGradientDrawable(background) + ?.color + ?.defaultColor ?: Color.BLACK + + // Make the background view invisible until we start the animation. + dialogContentWithBackground.visibility = View.INVISIBLE + + // Make sure the dialog is visible instantly and does not do any window animation. + window.attributes.windowAnimations = R.style.Animation_LaunchAnimation + + // Start the animation once the background view is properly laid out. + dialogContentWithBackground.addOnLayoutChangeListener(object : View.OnLayoutChangeListener { + override fun onLayoutChange( + v: View, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + dialogContentWithBackground.removeOnLayoutChangeListener(this) + + isOriginalDialogViewLaidOut = true + maybeStartLaunchAnimation() + } + }) // Disable the dim. We will enable it once we start the animation. window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + // Override the dialog dismiss() so that we can animate the exit before actually dismissing + // the dialog. + dialog.setDismissOverride(this::onDialogDismissed) + + // Show the dialog. + dialog.show() + // Add a temporary touch surface ghost as soon as the window is ready to draw. This - // temporary ghost will be drawn together with the touch surface, but in the host dialog + // temporary ghost will be drawn together with the touch surface, but in the dialog // window. Once it is drawn, we will make the touch surface invisible, and then start the // animation. We do all this synchronization to avoid flicker that would occur if we made // the touch surface invisible too early (before its ghost is drawn), leading to one or more // frames with a hole instead of the touch surface (or its ghost). - hostDialogRoot.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener { + decorView.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener { override fun onPreDraw(): Boolean { - hostDialogRoot.viewTreeObserver.removeOnPreDrawListener(this) + decorView.viewTreeObserver.removeOnPreDrawListener(this) addTemporaryTouchSurfaceGhost() return true } }) - hostDialogRoot.invalidate() + decorView.invalidate() } private fun addTemporaryTouchSurfaceGhost() { // Create a ghost of the touch surface (which will make the touch surface invisible) and add - // it to the host dialog. We will wait for this ghost to be drawn before starting the - // animation. - val ghost = GhostView.addGhost(touchSurface, hostDialogRoot) + // it to the dialog. We will wait for this ghost to be drawn before starting the animation. + val ghost = GhostView.addGhost(touchSurface, decorView) // The ghost of the touch surface was just created, so the touch surface was made invisible. // We make it visible again until the ghost is actually drawn. @@ -414,142 +420,6 @@ private class AnimatedDialog( touchSurface.invalidate() } - /** Get the content view of [originalDialog] and pass it to [then]. */ - private fun stealOriginalDialogContentView(then: (View) -> Unit) { - // The original dialog content view will be attached to android.R.id.content when the dialog - // is shown, so we show the dialog and add an observer to get the view but also prevents the - // original dialog from being drawn. - val androidContent = originalDialog.findViewById<ViewGroup>(android.R.id.content) - ?: throw IllegalStateException("Dialog does not have any android.R.id.content view") - - androidContent.viewTreeObserver.addOnPreDrawListener( - object : OnPreDrawListener { - override fun onPreDraw(): Boolean { - if (androidContent.childCount == 1) { - androidContent.viewTreeObserver.removeOnPreDrawListener(this) - - // Hide the animated dialog. Because of the dialog listener set up - // earlier, this would also hide the host dialog, but in this case we - // need to keep the host dialog visible. - ignoreNextCallToHide = true - originalDialog.hide() - - then(androidContent.getChildAt(0)) - return false - } - - // Never draw the original dialog content. - return false - } - }) - originalDialog.show() - } - - private fun showDialogFromView(dialogView: View) { - // Close the dialog when clicking outside of it. - hostDialogRoot.setOnClickListener { hostDialog.dismiss() } - dialogView.isClickable = true - - // Remove the original dialog view from its parent. - (dialogView.parent as? ViewGroup)?.removeView(dialogView) - - val originalDialogWindow = originalDialog.window!! - val isOriginalWindowFullScreen = - originalDialogWindow.attributes.width == ViewGroup.LayoutParams.MATCH_PARENT && - originalDialogWindow.attributes.height == ViewGroup.LayoutParams.MATCH_PARENT - if (isOriginalWindowFullScreen) { - // If the original dialog window is fullscreen, then we look for the first ViewGroup - // that has a background and animate towards that ViewGroup given that this is probably - // what represents the actual dialog view. - dialogContentWithBackground = findFirstViewGroupWithBackground(dialogView) - ?: throw IllegalStateException("Unable to find ViewGroup with background") - - hostDialogRoot.addView( - dialogView, - - FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - ) - } else { - // Add a parent view to the original dialog view to which we will set the original - // dialog window background. This View serves as a fake window with background, so that - // we are sure that we don't override the original dialog content view paddings with the - // window background that usually has insets. - dialogContentWithBackground = FrameLayout(context).apply { - id = DIALOG_CONTENT_PARENT_ID - - // TODO(b/193634619): Support dialog windows without background. - background = originalDialogWindow.decorView?.background - ?: throw IllegalStateException( - "Dialogs with no backgrounds on window are not supported") - - addView( - dialogView, - - // It should match its parent size, which is sized the same as the original - // dialog window. - FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - ) - } - - // Add the parent (that has the background) to the host window. - hostDialogRoot.addView( - dialogContentWithBackground, - - // We give it the size and gravity of its original dialog window. - FrameLayout.LayoutParams( - originalDialogWindow.attributes.width, - originalDialogWindow.attributes.height, - originalDialogWindow.attributes.gravity - ) - ) - } - - val dialogContentWithBackground = this.dialogContentWithBackground!! - - // Make the dialog and its background invisible for now, to make sure it's not drawn yet. - dialogContentWithBackground.visibility = View.INVISIBLE - - val background = dialogContentWithBackground.background!! - originalDialogBackgroundColor = - GhostedViewLaunchAnimatorController.findGradientDrawable(background) - ?.color - ?.defaultColor ?: Color.BLACK - - if (isOriginalWindowFullScreen) { - // If the original window is full screen, the ViewGroup with background might already be - // correctly laid out. Make sure we relayout and that the layout listener below is still - // called. - dialogContentWithBackground.layout(0, 0, 0, 0) - dialogContentWithBackground.requestLayout() - } - - // Start the animation when the dialog is laid out in the center of the host dialog. - dialogContentWithBackground.addOnLayoutChangeListener(object : View.OnLayoutChangeListener { - override fun onLayoutChange( - view: View, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int - ) { - dialogContentWithBackground.removeOnLayoutChangeListener(this) - - isOriginalDialogViewLaidOut = true - maybeStartLaunchAnimation() - } - }) - } - private fun findFirstViewGroupWithBackground(view: View): ViewGroup? { if (view !is ViewGroup) { return null @@ -569,26 +439,13 @@ private class AnimatedDialog( return null } - fun onOriginalDialogSizeChanged() { - // The dialog is the single child of the root. - if (hostDialogRoot.childCount != 1) { - return - } - - val dialogView = hostDialogRoot.getChildAt(0) - val layoutParams = dialogView.layoutParams as? FrameLayout.LayoutParams ?: return - layoutParams.width = originalDialog.window.attributes.width - layoutParams.height = originalDialog.window.attributes.height - dialogView.layoutParams = layoutParams - } - private fun maybeStartLaunchAnimation() { if (!isTouchSurfaceGhostDrawn || !isOriginalDialogViewLaidOut) { return } // Show the background dim. - hostDialog.window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + dialog.window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) startAnimation( isLaunching = true, @@ -610,7 +467,7 @@ private class AnimatedDialog( // dismiss was called during the animation, dismiss again now to actually // dismiss. if (dismissRequested) { - hostDialog.dismiss() + dialog.dismiss() } // If necessary, we animate the dialog background when its bounds change. We do it @@ -624,9 +481,9 @@ private class AnimatedDialog( ) } - private fun onHostDialogDismissed(actualDismiss: () -> Unit) { + private fun onDialogDismissed() { if (Looper.myLooper() != Looper.getMainLooper()) { - context.mainExecutor.execute { onHostDialogDismissed(actualDismiss) } + context.mainExecutor.execute { onDialogDismissed() } return } @@ -641,23 +498,29 @@ private class AnimatedDialog( } isDismissing = true - hideDialogIntoView { instantDismiss: Boolean -> - if (instantDismiss) { - originalDialog.hide() - hostDialog.hide() + hideDialogIntoView { animationRan: Boolean -> + if (animationRan) { + // Instantly dismiss the dialog if we ran the animation into view. If it was + // skipped, dismiss() will run the window animation (which fades out the dialog). + dialog.hide() } - originalDialog.dismiss() - actualDismiss() + dialog.setDismissOverride(null) + dialog.dismiss() } } /** - * Hide the dialog into the touch surface and call [dismissDialogs] when the animation is done - * (passing instantDismiss=true) or if it's skipped (passing instantDismiss=false) to actually - * dismiss the dialogs. + * Hide the dialog into the touch surface and call [onAnimationFinished] when the animation is + * done (passing animationRan=true) or if it's skipped (passing animationRan=false) to actually + * dismiss the dialog. */ - private fun hideDialogIntoView(dismissDialogs: (Boolean) -> Unit) { + private fun hideDialogIntoView(onAnimationFinished: (Boolean) -> Unit) { + // Remove the layout change listener we have added to the DecorView earlier. + if (decorViewLayoutListener != null) { + decorView.removeOnLayoutChangeListener(decorViewLayoutListener) + } + if (!shouldAnimateDialogIntoView()) { Log.i(TAG, "Skipping animation of dialog into the touch surface") @@ -669,7 +532,7 @@ private class AnimatedDialog( touchSurface.visibility = View.VISIBLE } - dismissDialogs(false /* instantDismiss */) + onAnimationFinished(false /* instantDismiss */) onDialogDismissed(this@AnimatedDialog) return } @@ -678,7 +541,7 @@ private class AnimatedDialog( isLaunching = false, onLaunchAnimationStart = { // Remove the dim background as soon as we start the animation. - hostDialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) }, onLaunchAnimationEnd = { // Make sure we allow the touch surface to change its visibility again. @@ -696,7 +559,7 @@ private class AnimatedDialog( // The animated ghost was just removed. We create a temporary ghost that will be // removed only once we draw the touch surface, to avoid flickering that would // happen when removing the ghost too early (before the touch surface is drawn). - GhostView.addGhost(touchSurface, hostDialogRoot) + GhostView.addGhost(touchSurface, decorView) touchSurface.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener { override fun onPreDraw(): Boolean { @@ -705,7 +568,7 @@ private class AnimatedDialog( // Now that the touch surface was drawn, we can remove the temporary ghost // and instantly dismiss the dialog. GhostView.removeGhost(touchSurface) - dismissDialogs(true /* instantDismiss */) + onAnimationFinished(true /* instantDismiss */) onDialogDismissed(this@AnimatedDialog) return true @@ -721,14 +584,14 @@ private class AnimatedDialog( onLaunchAnimationStart: () -> Unit = {}, onLaunchAnimationEnd: () -> Unit = {} ) { - // Create 2 ghost controllers to animate both the dialog and the touch surface in the host + // Create 2 ghost controllers to animate both the dialog and the touch surface in the // dialog. val startView = if (isLaunching) touchSurface else dialogContentWithBackground!! val endView = if (isLaunching) dialogContentWithBackground!! else touchSurface val startViewController = GhostedViewLaunchAnimatorController(startView) val endViewController = GhostedViewLaunchAnimatorController(endView) - startViewController.launchContainer = hostDialogRoot - endViewController.launchContainer = hostDialogRoot + startViewController.launchContainer = decorView + endViewController.launchContainer = decorView val endState = endViewController.createAnimatorState() val controller = object : LaunchAnimator.Controller { @@ -785,9 +648,15 @@ private class AnimatedDialog( } private fun shouldAnimateDialogIntoView(): Boolean { - // Don't animate if the dialog was previously hidden using hide() (either on the host dialog - // or on the original dialog) or if we disabled the exit animation. - if (exitAnimationDisabled || !hostDialog.isShowing) { + // Don't animate if the dialog was previously hidden using hide() or if we disabled the exit + // animation. + if (exitAnimationDisabled || !dialog.isShowing) { + return false + } + + // If we are dreaming, the dialog was probably closed because of that so we don't animate + // into the touchSurface. + if (dreamManager.isDreaming) { return false } @@ -888,9 +757,9 @@ private class AnimatedDialog( return touchSurface } parentAnimatedDialog.exitAnimationDisabled = true - parentAnimatedDialog.originalDialog.hide() + parentAnimatedDialog.dialog.hide() val view = parentAnimatedDialog.prepareForStackDismiss() - parentAnimatedDialog.originalDialog.dismiss() + parentAnimatedDialog.dialog.dismiss() // Make the touch surface invisible, so we end up animating to it when we actually // dismiss the stack view.visibility = View.INVISIBLE diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index 26ce645eefc5..03a097746ba9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -217,14 +217,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements dismiss(); } - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - if (!hasFocus && isShowing()) { - dismiss(); - } - } - void onHeaderIconClick() { } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java index 3163c5f5a3c9..20805a141312 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java @@ -190,14 +190,10 @@ public class DndTile extends QSTileImpl<BooleanState> { case Settings.Secure.ZEN_DURATION_PROMPT: mUiHandler.post(() -> { Dialog dialog = makeZenModeDialog(); + SystemUIDialog.registerDismissListener(dialog); if (view != null) { - final Dialog hostDialog = - mDialogLaunchAnimator.showFromView(dialog, view, false); - setDialogListeners(dialog, hostDialog); + mDialogLaunchAnimator.showFromView(dialog, view, false); } else { - // If we are not launching with animator, register default - // dismiss listener - SystemUIDialog.registerDismissListener(dialog); dialog.show(); } }); @@ -222,12 +218,6 @@ public class DndTile extends QSTileImpl<BooleanState> { return dialog; } - private void setDialogListeners(Dialog zenModeDialog, Dialog hostDialog) { - // Zen mode dialog is never hidden. - SystemUIDialog.registerDismissListener(zenModeDialog, hostDialog::dismiss); - zenModeDialog.setOnCancelListener(dialog -> hostDialog.cancel()); - } - @Override protected void handleSecondaryClick(@Nullable View view) { if (mController.isVolumeRestricted()) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt index 00e04540fd94..7c8f4b15d3a3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt @@ -94,24 +94,24 @@ class UserSwitchDialogController @VisibleForTesting constructor( adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid)) - val hostDialog = dialogLaunchAnimator.showFromView(this, view) - adapter.injectDialogShower(DialogShowerImpl(hostDialog, dialogLaunchAnimator)) + dialogLaunchAnimator.showFromView(this, view) + adapter.injectDialogShower(DialogShowerImpl(this, dialogLaunchAnimator)) } } private class DialogShowerImpl( - private val hostDialog: Dialog, + private val animateFrom: Dialog, private val dialogLaunchAnimator: DialogLaunchAnimator - ) : DialogInterface by hostDialog, DialogShower { - override fun showDialog(dialog: Dialog): Dialog { - return dialogLaunchAnimator.showFromDialog( + ) : DialogInterface by animateFrom, DialogShower { + override fun showDialog(dialog: Dialog) { + dialogLaunchAnimator.showFromDialog( dialog, - parentHostDialog = hostDialog + animateFrom = animateFrom ) } } interface DialogShower : DialogInterface { - fun showDialog(dialog: Dialog): Dialog + fun showDialog(dialog: Dialog) } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index 74ea19f4ca22..1d921702e632 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -20,6 +20,7 @@ import android.app.IActivityManager; import android.app.NotificationManager; import android.content.Context; import android.os.Handler; +import android.service.dreams.IDreamManager; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.animation.ActivityLaunchAnimator; @@ -67,13 +68,10 @@ import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl; import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback; -import com.android.systemui.statusbar.phone.SystemUIHostDialogProvider; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLogger; import com.android.systemui.statusbar.policy.RemoteInputUriController; import com.android.systemui.statusbar.window.StatusBarWindowController; -import com.android.systemui.statusbar.window.StatusBarWindowModule; -import com.android.systemui.statusbar.window.StatusBarWindowView; import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.time.SystemClock; @@ -320,7 +318,7 @@ public interface StatusBarDependenciesModule { @Provides @SysUISingleton static DialogLaunchAnimator provideDialogLaunchAnimator(Context context, - LaunchAnimator launchAnimator) { - return new DialogLaunchAnimator(context, launchAnimator, new SystemUIHostDialogProvider()); + LaunchAnimator launchAnimator, IDreamManager dreamManager) { + return new DialogLaunchAnimator(context, launchAnimator, dreamManager); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index ed52a81751dd..43264b600a0e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -40,30 +40,21 @@ import androidx.annotation.Nullable; import com.android.systemui.Dependency; import com.android.systemui.R; -import com.android.systemui.animation.DialogListener; -import com.android.systemui.animation.DialogListener.DismissReason; -import com.android.systemui.animation.ListenableDialog; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.statusbar.policy.KeyguardStateController; -import java.util.LinkedHashSet; -import java.util.Set; - - /** * Base class for dialogs that should appear over panels and keyguard. * The SystemUIDialog registers a listener for the screen off / close system dialogs broadcast, * and dismisses itself when it receives the broadcast. */ -public class SystemUIDialog extends AlertDialog implements ListenableDialog, - ViewRootImpl.ConfigChangedCallback { +public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigChangedCallback { // TODO(b/203389579): Remove this once the dialog width on large screens has been agreed on. private static final String FLAG_TABLET_DIALOG_WIDTH = "persist.systemui.flag_tablet_dialog_width"; private final Context mContext; private final DismissReceiver mDismissReceiver; - private final Set<DialogListener> mDialogListeners = new LinkedHashSet<>(); private final Handler mHandler = new Handler(); private int mLastWidth = Integer.MIN_VALUE; @@ -117,10 +108,6 @@ public class SystemUIDialog extends AlertDialog implements ListenableDialog, mLastWidth = width; mLastHeight = height; getWindow().setLayout(width, height); - - for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) { - listener.onSizeChanged(); - } } @Override @@ -197,60 +184,6 @@ public class SystemUIDialog extends AlertDialog implements ListenableDialog, ViewRootImpl.removeConfigCallback(this); } - @Override - public void addListener(DialogListener listener) { - mDialogListeners.add(listener); - } - - @Override - public void removeListener(DialogListener listener) { - mDialogListeners.remove(listener); - } - - @Override - public void dismiss() { - dismiss(DismissReason.UNKNOWN); - } - - private void dismiss(DismissReason reason) { - super.dismiss(); - - for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) { - listener.onDismiss(reason); - } - } - - /** - * Dismiss this dialog. If it was launched from another dialog using - * {@link com.android.systemui.animation.DialogLaunchAnimator#showFromView} with a - * non-{@code null} {@code parentHostDialog} parameter, also dismisses the stack of dialogs, - * animating back to the original touchSurface. - */ - public void dismissStack() { - for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) { - listener.prepareForStackDismiss(); - } - dismiss(); - } - - @Override - public void hide() { - super.hide(); - - for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) { - listener.onHide(); - } - } - - @Override - public void show() { - super.show(); - - for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) { - listener.onShow(); - } - } - public void setShowForAllUsers(boolean show) { setShowForAllUsers(this, show); } @@ -364,11 +297,7 @@ public class SystemUIDialog extends AlertDialog implements ListenableDialog, @Override public void onReceive(Context context, Intent intent) { - if (mDialog instanceof SystemUIDialog) { - ((SystemUIDialog) mDialog).dismiss(DismissReason.DEVICE_LOCKED); - } else { - mDialog.dismiss(); - } + mDialog.dismiss(); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIHostDialogProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIHostDialogProvider.kt deleted file mode 100644 index 4f18f8c597b2..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIHostDialogProvider.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.android.systemui.statusbar.phone - -import android.app.Dialog -import android.content.Context -import android.os.Bundle -import android.view.ViewGroup -import com.android.systemui.animation.HostDialogProvider - -/** An implementation of [HostDialogProvider] to be used when animating SysUI dialogs. */ -class SystemUIHostDialogProvider : HostDialogProvider { - override fun createHostDialog( - context: Context, - theme: Int, - onCreateCallback: () -> Unit, - dismissOverride: (() -> Unit) -> Unit - ): Dialog { - return SystemUIHostDialog(context, theme, onCreateCallback, dismissOverride) - } - - /** - * This host dialog is a SystemUIDialog so that it's displayed above all SystemUI windows. Note - * that it is not automatically dismissed when the device is locked, but only when the hosted - * (original) dialog is dismissed. That way, the behavior of the dialog (dismissed when locking - * or not) is consistent with when the dialog is shown with or without the dialog animator. - */ - private class SystemUIHostDialog( - context: Context, - theme: Int, - private val onCreateCallback: () -> Unit, - private val dismissOverride: (() -> Unit) -> Unit - ) : SystemUIDialog(context, theme, false /* dismissOnDeviceLock */) { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - onCreateCallback() - } - - override fun dismiss() { - dismissOverride { - super.dismiss() - } - } - - override fun getWidth(): Int { - return ViewGroup.LayoutParams.MATCH_PARENT - } - - override fun getHeight(): Int { - return ViewGroup.LayoutParams.MATCH_PARENT - } - } -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java index fd387ae0a82e..36e56f967424 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -69,6 +69,7 @@ import com.android.systemui.Prefs; import com.android.systemui.Prefs.Key; import com.android.systemui.R; import com.android.systemui.SystemUISecondaryUserService; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -133,6 +134,7 @@ public class UserSwitcherController implements Dumpable { private final IActivityTaskManager mActivityTaskManager; private final InteractionJankMonitor mInteractionJankMonitor; private final LatencyTracker mLatencyTracker; + private final DialogLaunchAnimator mDialogLaunchAnimator; private ArrayList<UserRecord> mUsers = new ArrayList<>(); @VisibleForTesting @@ -180,7 +182,8 @@ public class UserSwitcherController implements Dumpable { @Background Executor bgExecutor, InteractionJankMonitor interactionJankMonitor, LatencyTracker latencyTracker, - DumpManager dumpManager) { + DumpManager dumpManager, + DialogLaunchAnimator dialogLaunchAnimator) { mContext = context; mActivityManager = activityManager; mUserTracker = userTracker; @@ -208,6 +211,8 @@ public class UserSwitcherController implements Dumpable { mHandler = handler; mActivityStarter = activityStarter; mUserManager = userManager; + mDialogLaunchAnimator = dialogLaunchAnimator; + IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_ADDED); filter.addAction(Intent.ACTION_USER_REMOVED); @@ -1179,7 +1184,7 @@ public class UserSwitcherController implements Dumpable { cancel(); } else { mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE); - dismissStack(); + mDialogLaunchAnimator.dismissStack(this); removeGuestUser(mGuestId, mTargetId); } } @@ -1210,7 +1215,7 @@ public class UserSwitcherController implements Dumpable { if (which == BUTTON_NEGATIVE) { cancel(); } else { - dismissStack(); + mDialogLaunchAnimator.dismissStack(this); if (ActivityManager.isUserAMonkey()) { return; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt index 9bd33eb8db6b..f9ad740f86df 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt @@ -2,35 +2,49 @@ package com.android.systemui.animation import android.app.Dialog import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable import android.os.Bundle +import android.service.dreams.IDreamManager import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.testing.ViewUtils import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.WindowManager import android.widget.LinearLayout import androidx.test.filters.SmallTest +import com.android.internal.policy.DecorView import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.DialogListener.DismissReason import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertNotNull import junit.framework.Assert.assertTrue import org.junit.After +import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class DialogLaunchAnimatorTest : SysuiTestCase() { private val launchAnimator = LaunchAnimator(context, isForTesting = true) - private val hostDialogprovider = TestHostDialogProvider() - private val dialogLaunchAnimator = - DialogLaunchAnimator(context, launchAnimator, hostDialogprovider) - + private lateinit var dialogLaunchAnimator: DialogLaunchAnimator private val attachedViews = mutableSetOf<View>() + @Mock lateinit var dreamManager: IDreamManager + @get:Rule val rule = MockitoJUnit.rule() + + @Before + fun setUp() { + dialogLaunchAnimator = DialogLaunchAnimator(context, launchAnimator, dreamManager) + } + @After fun tearDown() { runOnMainThreadAndWaitForIdleSync { @@ -44,76 +58,66 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { fun testShowDialogFromView() { // Show the dialog. showFromView() must be called on the main thread with a dialog created // on the main thread too. - val (dialog, hostDialog) = createDialogAndHostDialog() - - // Only the host dialog is actually showing. - assertTrue(hostDialog.isShowing) - assertFalse(dialog.isShowing) - - // The dialog onStart() method was called but not onStop(). - assertTrue(dialog.onStartCalled) - assertFalse(dialog.onStopCalled) - - // The dialog content has been stolen and is shown inside the host dialog. - val hostDialogContent = hostDialog.findViewById<ViewGroup>(android.R.id.content) - assertEquals(0, dialog.findViewById<ViewGroup>(android.R.id.content).childCount) - assertEquals(1, hostDialogContent.childCount) - - // The original dialog content is added to another view that is the same size as the - // original dialog window. - val hostDialogRoot = hostDialogContent.getChildAt(0) as ViewGroup - assertEquals(1, hostDialogRoot.childCount) - - val dialogContentParent = hostDialogRoot.getChildAt(0) as ViewGroup - assertEquals(1, dialogContentParent.childCount) - assertEquals(TestDialog.DIALOG_WIDTH, dialogContentParent.layoutParams.width) - assertEquals(TestDialog.DIALOG_HEIGHT, dialogContentParent.layoutParams.height) - - val dialogContent = dialogContentParent.getChildAt(0) - assertEquals(dialog.contentView, dialogContent) - assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, dialogContent.layoutParams.width) - assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, dialogContent.layoutParams.height) - - // Hiding/showing/dismissing the dialog should hide/show/dismiss the host dialog given that - // it's a ListenableDialog. - runOnMainThreadAndWaitForIdleSync { dialog.hide() } - assertFalse(hostDialog.isShowing) - assertFalse(dialog.isShowing) - - runOnMainThreadAndWaitForIdleSync { dialog.show() } - assertTrue(hostDialog.isShowing) - assertFalse(dialog.isShowing) - - assertFalse(dialog.onStopCalled) + val dialog = createAndShowDialog() + + assertTrue(dialog.isShowing) + + // The dialog is now fullscreen. + val window = dialog.window + val decorView = window.decorView as DecorView + assertEquals(MATCH_PARENT, window.attributes.width) + assertEquals(MATCH_PARENT, window.attributes.height) + assertEquals(MATCH_PARENT, decorView.layoutParams.width) + assertEquals(MATCH_PARENT, decorView.layoutParams.height) + + // The single DecorView child is a transparent fullscreen view that will dismiss the dialog + // when clicked. + assertEquals(1, decorView.childCount) + val transparentBackground = decorView.getChildAt(0) as ViewGroup + assertEquals(MATCH_PARENT, transparentBackground.layoutParams.width) + assertEquals(MATCH_PARENT, transparentBackground.layoutParams.height) + + // The single transparent background child is a fake window with the same size and + // background as the dialog initially had. + assertEquals(1, transparentBackground.childCount) + val dialogContentWithBackground = transparentBackground.getChildAt(0) as ViewGroup + assertEquals(TestDialog.DIALOG_WIDTH, dialogContentWithBackground.layoutParams.width) + assertEquals(TestDialog.DIALOG_HEIGHT, dialogContentWithBackground.layoutParams.height) + assertEquals(dialog.windowBackground, dialogContentWithBackground.background) + + // The dialog content is inside this fake window view. + assertNotNull( + dialogContentWithBackground.findViewByPredicate { it === dialog.contentView }) + + // Clicking the transparent background should dismiss the dialog. runOnMainThreadAndWaitForIdleSync { // TODO(b/204561691): Remove this call to disableAllCurrentDialogsExitAnimations() and // make sure that the test still pass on git_master/cf_x86_64_phone-userdebug in // Forrest. dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations() - dialog.dismiss() + transparentBackground.performClick() } - assertFalse(hostDialog.isShowing) assertFalse(dialog.isShowing) - assertTrue(hostDialog.wasDismissed) - assertTrue(dialog.onStopCalled) } @Test fun testStackedDialogsDismissesAll() { - val (_, hostDialogFirst) = createDialogAndHostDialog() - val (dialogSecond, hostDialogSecond) = createDialogAndHostDialogFromDialog(hostDialogFirst) + val firstDialog = createAndShowDialog() + val secondDialog = createDialogAndShowFromDialog(firstDialog) + assertTrue(firstDialog.isShowing) + assertTrue(secondDialog.isShowing) runOnMainThreadAndWaitForIdleSync { dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations() - dialogSecond.dismissStack() + dialogLaunchAnimator.dismissStack(secondDialog) } - assertTrue(hostDialogSecond.wasDismissed) - assertTrue(hostDialogFirst.wasDismissed) + assertFalse(firstDialog.isShowing) + assertFalse(secondDialog.isShowing) } - private fun createDialogAndHostDialog(): Pair<TestDialog, TestHostDialog> { + private fun createAndShowDialog(): TestDialog { return runOnMainThreadAndWaitForIdleSync { val touchSurfaceRoot = LinearLayout(context) val touchSurface = View(context) @@ -125,22 +129,16 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { attachedViews.add(touchSurfaceRoot) val dialog = TestDialog(context) - val hostDialog = - dialogLaunchAnimator.showFromView(dialog, touchSurface) as TestHostDialog - dialog to hostDialog + dialogLaunchAnimator.showFromView(dialog, touchSurface) + dialog } } - private fun createDialogAndHostDialogFromDialog( - hostParent: Dialog - ): Pair<TestDialog, TestHostDialog> { + private fun createDialogAndShowFromDialog(animateFrom: Dialog): TestDialog { return runOnMainThreadAndWaitForIdleSync { val dialog = TestDialog(context) - val hostDialog = dialogLaunchAnimator.showFromDialog( - dialog, - hostParent - ) as TestHostDialog - dialog to hostDialog + dialogLaunchAnimator.showFromDialog(dialog, animateFrom) + dialog } } @@ -153,50 +151,14 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { return result } - private class TestHostDialogProvider : HostDialogProvider { - override fun createHostDialog( - context: Context, - theme: Int, - onCreateCallback: () -> Unit, - dismissOverride: (() -> Unit) -> Unit - ): Dialog = TestHostDialog(context, onCreateCallback, dismissOverride) - } - - private class TestHostDialog( - context: Context, - private val onCreateCallback: () -> Unit, - private val dismissOverride: (() -> Unit) -> Unit - ) : Dialog(context) { - var wasDismissed = false - - init { - // We need to set the window type for dialogs shown by SysUI, otherwise WM will throw. - window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - onCreateCallback() - } - - override fun dismiss() { - dismissOverride { - super.dismiss() - wasDismissed = true - } - } - } - - private class TestDialog(context: Context) : Dialog(context), ListenableDialog { + private class TestDialog(context: Context) : Dialog(context) { companion object { const val DIALOG_WIDTH = 100 const val DIALOG_HEIGHT = 200 } - private val listeners = hashSetOf<DialogListener>() val contentView = View(context) - var onStartCalled = false - var onStopCalled = false + val windowBackground = ColorDrawable(Color.RED) init { // We need to set the window type for dialogs shown by SysUI, otherwise WM will throw. @@ -205,52 +167,10 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - window.setLayout(DIALOG_WIDTH, DIALOG_HEIGHT) setContentView(contentView) - } - - override fun onStart() { - super.onStart() - onStartCalled = true - } - - override fun onStop() { - super.onStart() - onStopCalled = true - } - - override fun addListener(listener: DialogListener) { - listeners.add(listener) - } - - override fun removeListener(listener: DialogListener) { - listeners.remove(listener) - } - - override fun dismiss() { - super.dismiss() - notifyListeners { onDismiss(DismissReason.UNKNOWN) } - } - - override fun hide() { - super.hide() - notifyListeners { onHide() } - } - override fun show() { - super.show() - notifyListeners { onShow() } - } - - fun dismissStack() { - notifyListeners { prepareForStackDismiss() } - dismiss() - } - - private fun notifyListeners(notify: DialogListener.() -> Unit) { - for (listener in HashSet(listeners)) { - listener.notify() - } + window.setLayout(DIALOG_WIDTH, DIALOG_HEIGHT) + window.setBackgroundDrawable(windowBackground) } } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt index 3c4a557eac10..b7fdc1a6cb0d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.qs.user -import android.app.Dialog import android.content.DialogInterface import android.content.Intent import android.provider.Settings @@ -31,7 +30,6 @@ import com.android.systemui.qs.PseudoGridView import com.android.systemui.qs.tiles.UserDetailView import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import org.junit.Before @@ -42,7 +40,6 @@ import org.mockito.ArgumentMatcher import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.`when` -import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.anyInt import org.mockito.Mockito.argThat import org.mockito.Mockito.never @@ -65,8 +62,6 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { private lateinit var launchView: View @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator - @Mock - private lateinit var hostDialog: Dialog @Captor private lateinit var clickCaptor: ArgumentCaptor<DialogInterface.OnClickListener> @@ -78,8 +73,6 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { `when`(launchView.context).thenReturn(mContext) `when`(dialog.context).thenReturn(mContext) - `when`(dialogLaunchAnimator.showFromView(any(), any(), anyBoolean())) - .thenReturn(hostDialog) controller = UserSwitchDialogController( { userDetailViewAdapter }, @@ -151,18 +144,6 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { verify(activityStarter, never()).postStartActivityDismissingKeyguard(any(), anyInt()) } - @Test - fun callbackFromDialogShower_dismissesDialog() { - val captor = argumentCaptor<UserSwitchDialogController.DialogShower>() - - controller.showDialog(launchView) - verify(userDetailViewAdapter).injectDialogShower(capture(captor)) - - captor.value.dismiss() - - verify(hostDialog).dismiss() - } - private class IntentMatcher(private val action: String) : ArgumentMatcher<Intent> { override fun matches(argument: Intent?): Boolean { return argument?.action == action diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt index 724f841922ff..a4bf14254e2c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt @@ -40,6 +40,7 @@ import com.android.internal.util.UserIcons import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.ActivityStarter @@ -94,6 +95,7 @@ class UserSwitcherControllerTest : SysuiTestCase() { @Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower @Mock private lateinit var notificationShadeWindowView: NotificationShadeWindowView @Mock private lateinit var threadedRenderer: ThreadedRenderer + @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator private lateinit var testableLooper: TestableLooper private lateinit var uiBgExecutor: FakeExecutor private lateinit var uiEventLogger: UiEventLoggerFake @@ -147,7 +149,8 @@ class UserSwitcherControllerTest : SysuiTestCase() { uiBgExecutor, interactionJankMonitor, latencyTracker, - dumpManager) + dumpManager, + dialogLaunchAnimator) userSwitcherController.mPauseRefreshUsers = true // Since userSwitcherController involves InteractionJankMonitor. |