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)
This commit is contained in:
Jordan Demeulenaere
2021-12-01 13:14:47 +00:00
committed by Android (Google) Code Review
19 changed files with 382 additions and 707 deletions

View File

@@ -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) {

View File

@@ -16,5 +16,4 @@
-->
<resources>
<item type="id" name="launch_animation_running"/>
<item type="id" name="dialog_content_parent" />
</resources>

View File

@@ -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>

View File

@@ -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()
}
})
}
animatedDialog.start()
return hostDialog
}
/**
* Launch [dialog] from a [parentHostDialog] as returned by [showFromView]. This will allow
* for dismissing the whole stack.
* Launch [dialog] from [another dialog][animateFrom] that was shown using [showFromView]. This
* will allow for dismissing the whole stack.
*
* This will return a new host dialog, with the same caveat as [showFromView].
*
* @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()
// 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)
}
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
}
}
private fun onHostDialogCreated() {
// Make the dialog fullscreen with a transparent background.
hostDialog.setContentView(
hostDialogRoot,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
// 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 window = hostDialog.window
?: throw IllegalStateException("There is no window associated to the host dialog")
window.setBackgroundDrawableResource(android.R.color.transparent)
val dialogContentWithBackground = FrameLayout(context)
dialogContentWithBackground.background = decorView.background
// 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)
// 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
fullscreenTransparentBackground.addView(
dialogContentWithBackground,
FrameLayout.LayoutParams(
window.attributes.width,
window.attributes.height,
window.attributes.gravity
)
)
// 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

View File

@@ -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() {
}

View File

@@ -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();
}
});
});
}

View File

@@ -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()) {

View File

@@ -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)
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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;
}

View File

@@ -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()
val dialog = createAndShowDialog()
// Only the host dialog is actually showing.
assertTrue(hostDialog.isShowing)
assertFalse(dialog.isShowing)
assertTrue(dialog.isShowing)
// The dialog onStart() method was called but not onStop().
assertTrue(dialog.onStartCalled)
assertFalse(dialog.onStopCalled)
// 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 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 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 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)
// 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)
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)
// The dialog content is inside this fake window view.
assertNotNull(
dialogContentWithBackground.findViewByPredicate { it === dialog.contentView })
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)
// 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)
}
}
}

View File

@@ -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 */);

View File

@@ -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 {

View File

@@ -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

View File

@@ -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.