diff options
| author | 2021-12-01 13:14:47 +0000 | |
|---|---|---|
| committer | 2021-12-01 13:14:47 +0000 | |
| commit | 9f02d2fd201831f0d24f7e0756dbe784a11027a7 (patch) | |
| tree | 8b579d502bca45fc225bfb08bfa545c1889d0e1a | |
| parent | f5fe79dedc4742e60a6727fefb7280cfeb5d1238 (diff) | |
| parent | 4538223f856fc1d5ee6d246c47110cf8487b9454 (diff) | |
Merge changes from topic "dialog-refactor" into sc-v2-dev
* changes:
Animate the cast dialog
Refactor DialogLaunchAnimator and remove host dialog (2/2)
Refactor DialogLaunchAnimator and remove host dialog (1/2)
19 files changed, 389 insertions, 714 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/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java index 0427e38aa811..b83dc52240b3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java @@ -31,7 +31,6 @@ import android.util.Log; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup; -import android.view.WindowManager.LayoutParams; import android.widget.Button; import androidx.annotation.Nullable; @@ -40,6 +39,7 @@ import com.android.internal.app.MediaRouteDialogPresenter; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.R; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; @@ -76,6 +76,7 @@ public class CastTile extends QSTileImpl<BooleanState> { private final CastDetailAdapter mDetailAdapter; private final KeyguardStateController mKeyguard; private final NetworkController mNetworkController; + private final DialogLaunchAnimator mDialogLaunchAnimator; private final Callback mCallback = new Callback(); private Dialog mDialog; private boolean mWifiConnected; @@ -94,7 +95,8 @@ public class CastTile extends QSTileImpl<BooleanState> { CastController castController, KeyguardStateController keyguardStateController, NetworkController networkController, - HotspotController hotspotController + HotspotController hotspotController, + DialogLaunchAnimator dialogLaunchAnimator ) { super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); @@ -102,6 +104,7 @@ public class CastTile extends QSTileImpl<BooleanState> { mDetailAdapter = new CastDetailAdapter(); mKeyguard = keyguardStateController; mNetworkController = networkController; + mDialogLaunchAnimator = dialogLaunchAnimator; mController.observe(this, mCallback); mKeyguard.observe(this, mCallback); mNetworkController.observe(this, mSignalCallback); @@ -153,9 +156,15 @@ public class CastTile extends QSTileImpl<BooleanState> { List<CastDevice> activeDevices = getActiveDevices(); if (willPopDetail()) { - mActivityStarter.postQSRunnableDismissingKeyguard(() -> { - showDetail(true); - }); + if (!mKeyguard.isShowing()) { + showDetail(view); + } else { + mActivityStarter.postQSRunnableDismissingKeyguard(() -> { + // Dismissing the keyguard will collapse the shade, so we don't animate from the + // view here as it would not look good. + showDetail(null /* view */); + }); + } } else { mController.stopCasting(activeDevices.get(0)); } @@ -184,19 +193,29 @@ public class CastTile extends QSTileImpl<BooleanState> { @Override public void showDetail(boolean show) { + showDetail(null /* view */); + } + + private void showDetail(@Nullable View view) { mUiHandler.post(() -> { mDialog = MediaRouteDialogPresenter.createDialog(mContext, ROUTE_TYPE_REMOTE_DISPLAY, v -> { + mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations(); mDialog.dismiss(); mActivityStarter .postStartActivityDismissingKeyguard(getLongClickIntent(), 0); }); - mDialog.getWindow().setType(LayoutParams.TYPE_KEYGUARD_DIALOG); SystemUIDialog.setShowForAllUsers(mDialog, true); SystemUIDialog.registerDismissListener(mDialog); SystemUIDialog.setWindowOnTop(mDialog); - mUiHandler.post(() -> mDialog.show()); - mHost.collapsePanels(); + + mUiHandler.post(() -> { + if (view != null) { + mDialogLaunchAnimator.showFromView(mDialog, view); + } else { + mDialog.show(); + } + }); }); } 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/tiles/CastTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java index 5a49337fe640..b40a20c27820 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java @@ -38,6 +38,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.MetricsLogger; import com.android.systemui.SysuiTestCase; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -90,6 +91,8 @@ public class CastTileTest extends SysuiTestCase { private HotspotController.Callback mHotspotCallback; @Mock private QSLogger mQSLogger; + @Mock + private DialogLaunchAnimator mDialogLaunchAnimator; private TestableLooper mTestableLooper; private CastTile mCastTile; @@ -113,7 +116,8 @@ public class CastTileTest extends SysuiTestCase { mController, mKeyguard, mNetworkController, - mHotspotController + mHotspotController, + mDialogLaunchAnimator ); mCastTile.initialize(); @@ -241,6 +245,7 @@ public class CastTileTest extends SysuiTestCase { List<CastDevice> devices = new ArrayList<>(); devices.add(device); when(mController.getCastDevices()).thenReturn(devices); + when(mKeyguard.isShowing()).thenReturn(true); enableWifiAndProcessMessages(); mCastTile.handleClick(null /* view */); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt index 13df04ccef6f..9936d4951d00 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt @@ -96,8 +96,6 @@ class DndTileTest : SysuiTestCase() { whenever(qsHost.userId).thenReturn(DEFAULT_USER) whenever(qsHost.uiEventLogger).thenReturn(uiEventLogger) - whenever(dialogLaunchAnimator.showFromView(any(), any(), anyBoolean())) - .thenReturn(hostDialog) val wrappedContext = object : ContextWrapper(context) { override fun getSharedPreferences(file: File?, mode: Int): SharedPreferences { 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. |