Refactor DialogLaunchAnimator and remove host dialog (1/2)

This CL is a refactoring of the DialogLaunchAnimator and a major change
in how it works internally.

Before this CL, when DialogLaunchAnimator.show(originalDialog, ...) was
called, it would create a fullscreen dialog (called hostDialog) to
which we would add the content view stolen from the originalDialog (that
would then be hidden). We would then run the launch animation in the
host dialog and listen for show(), hide() or dismiss() calls to the
originalDialog to know when we should show(), hide() or dismiss() the
hostDialog.

The main reason we did that was because there was no way to override the
dismiss() behavior of the originalDialog, which we need to do given that
we want to animate the dialog into the view that showed the dialog
before actually dismissing it.

This approach had multiple downsides:
 - We were showing the dialog content view inside a different window
   than the one in which it was created, therefore any change made to or
   any listener added to the originalDialog window was lost.
 - Code calling DialogLaunchAnimator.showFromView needed to know that
   their dialog content will be moved to another window, which is
   unexpected and can lead to subtle bugs.
 - We were waiting for 2 dialogs to be shown instead of 1 before being
   able to start the animation.

This CL does what we should have done since the beginning: it adds a
hidden API to Dialog so that we can override what happens when
Dialog.dismiss() is called. This allows the DialogLaunchAnimator to run
the exit animation into the view that triggered the dialog before
actually dismissing it. The only modification that
DialogLaunchAnimator.show(originalDialog, ...) now does to the
originalDialog is making its window fullscreen and inserting two views
between the originalDialog DecorView and its children. The first
inserted view is a fullscreen transparent background used to dismiss the
dialog when the user taps outside the dialog content. The second
inserted view is a View that we size and position the same way that the
DecorView was before we made it fullscreen, and to which we set the
originalDialog background. This view now serves as a "fake window" with
the same size, background and position as the original window
(DecorView).

This CL improves the time to start the launch animation by 20-25%
(measured in a totally unrigorous logcat way).

Bug: 193634619
Test: atest DialogLaunchAnimatorTest
Change-Id: If09eda7b06e83b3ed7714cec97afef08b3d9fd3e
This commit is contained in:
Jordan Demeulenaere
2021-11-23 14:44:18 +01:00
parent a5f83144a3
commit c424d4801f
16 changed files with 349 additions and 696 deletions

View File

@@ -151,6 +151,9 @@ public class Dialog implements DialogInterface, Window.Callback,
private final Runnable mDismissAction = this::dismissDialog; 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. * Creates a dialog window that uses the default dialog theme.
* <p> * <p>
@@ -370,6 +373,11 @@ public class Dialog implements DialogInterface, Window.Callback,
*/ */
@Override @Override
public void dismiss() { public void dismiss() {
if (mDismissOverride != null) {
mDismissOverride.run();
return;
}
if (Looper.myLooper() == mHandler.getLooper()) { if (Looper.myLooper() == mHandler.getLooper()) {
dismissDialog(); dismissDialog();
} else { } else {
@@ -1354,6 +1362,21 @@ public class Dialog implements DialogInterface, Window.Callback,
mDismissMessage = msg; 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 */ /** @hide */
public boolean takeCancelAndDismissListeners(@Nullable String msg, public boolean takeCancelAndDismissListeners(@Nullable String msg,
@Nullable OnCancelListener cancel, @Nullable OnDismissListener dismiss) { @Nullable OnCancelListener cancel, @Nullable OnDismissListener dismiss) {

View File

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

View File

@@ -15,15 +15,10 @@
limitations under the License. limitations under the License.
--> -->
<resources> <resources>
<style name="HostDialogTheme"> <!-- An animation used by DialogLaunchAnimator to make a dialog appear instantly (to animate -->
<item name="android:windowAnimationStyle">@style/Animation.HostDialog</item> <!-- in-window) and disappear by fading out (when the exit into view is disabled). -->
<item name="android:windowIsFloating">false</item> <style name="Animation.LaunchAnimation" parent="@android:style/Animation">
<item name="android:backgroundDimEnabled">true</item> <item name="android:windowEnterAnimation">@anim/launch_dialog_enter</item>
<item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:windowExitAnimation">@anim/launch_dialog_exit</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>
</style> </style>
</resources> </resources>

View File

@@ -24,22 +24,19 @@ import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.Rect import android.graphics.Rect
import android.os.Looper import android.os.Looper
import android.service.dreams.IDreamManager
import android.util.Log import android.util.Log
import android.util.MathUtils import android.util.MathUtils
import android.view.GhostView import android.view.GhostView
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewTreeObserver.OnPreDrawListener import android.view.ViewTreeObserver.OnPreDrawListener
import android.view.WindowInsets
import android.view.WindowManager 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 android.widget.FrameLayout
import kotlin.math.roundToInt import kotlin.math.roundToInt
private const val TAG = "DialogLaunchAnimator" 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 * 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( class DialogLaunchAnimator(
private val context: Context, private val context: Context,
private val launchAnimator: LaunchAnimator, private val launchAnimator: LaunchAnimator,
private val hostDialogProvider: HostDialogProvider private val dreamManager: IDreamManager
) { ) {
private companion object { private companion object {
private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running
@@ -62,41 +59,38 @@ class DialogLaunchAnimator(
private val openedDialogs = hashSetOf<AnimatedDialog>() private val openedDialogs = hashSetOf<AnimatedDialog>()
/** /**
* Show [dialog] by expanding it from [view]. If [animateBackgroundBoundsChange] is true, then * Show [dialog] by expanding it from [view]. If [view] is a view inside another dialog that was
* the background of the dialog will be animated when the dialog bounds change. * 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 * If [animateBackgroundBoundsChange] is true, then the background of the dialog will be
* attached to a different dialog (and thus a different window) which means that the actual * animated when the dialog bounds change.
* 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 * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be
* dismiss, hide or show the dialog. * made fullscreen and 2 views will be inserted between the dialog DecorView and its children.
*/ */
@JvmOverloads @JvmOverloads
fun showFromView( fun showFromView(
dialog: Dialog, dialog: Dialog,
view: View, view: View,
animateBackgroundBoundsChange: Boolean = false animateBackgroundBoundsChange: Boolean = false
): Dialog { ) {
if (Looper.myLooper() != Looper.getMainLooper()) { if (Looper.myLooper() != Looper.getMainLooper()) {
throw IllegalStateException( throw IllegalStateException(
"showFromView must be called from the main thread and dialog must be created in " + "showFromView must be called from the main thread and dialog must be created in " +
"the main thread") "the main thread")
} }
// If the parent of the view we are launching from is the background of some other animated // If the view we are launching from belongs to another dialog, then this means the caller
// dialog, then this means the caller intent is to launch a dialog from another dialog. In // intent is to launch a dialog from another dialog.
// this case, we also animate the parent (which is the dialog background). val animatedParent = openedDialogs
val animatedParent = openedDialogs.firstOrNull { .firstOrNull { it.dialog.window.decorView.viewRootImpl == view.viewRootImpl }
it.dialogContentWithBackground == view || it.dialogContentWithBackground == view.parent val animateFrom = animatedParent?.dialogContentWithBackground ?: view
}
val dialogContentWithBackground = animatedParent?.dialogContentWithBackground
val animateFrom = dialogContentWithBackground ?: view
// Make sure we don't run the launch animation from the same view twice at the same time. // 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) { if (animateFrom.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) {
Log.e(TAG, "Not running dialog launch animation as there is already one running") Log.e(TAG, "Not running dialog launch animation as there is already one running")
dialog.show() dialog.show()
return dialog return
} }
animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true) animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
@@ -104,82 +98,36 @@ class DialogLaunchAnimator(
val animatedDialog = AnimatedDialog( val animatedDialog = AnimatedDialog(
context, context,
launchAnimator, launchAnimator,
hostDialogProvider, dreamManager,
animateFrom, animateFrom,
onDialogDismissed = { openedDialogs.remove(it) }, onDialogDismissed = { openedDialogs.remove(it) },
originalDialog = dialog, dialog = dialog,
animateBackgroundBoundsChange, animateBackgroundBoundsChange,
animatedParent animatedParent
) )
val hostDialog = animatedDialog.hostDialog
openedDialogs.add(animatedDialog) 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() animatedDialog.start()
return hostDialog
} }
/** /**
* Launch [dialog] from a [parentHostDialog] as returned by [showFromView]. This will allow * Launch [dialog] from [another dialog][animateFrom] that was shown using [showFromView]. This
* for dismissing the whole stack. * will allow for dismissing the whole stack.
* *
* This will return a new host dialog, with the same caveat as [showFromView]. * @see dismissStack
*
* @see DialogListener.prepareForStackDismiss
*/ */
fun showFromDialog( fun showFromDialog(
dialog: Dialog, dialog: Dialog,
parentHostDialog: Dialog, animateFrom: Dialog,
animateBackgroundBoundsChange: Boolean = false animateBackgroundBoundsChange: Boolean = false
): Dialog { ) {
val view = parentHostDialog.findViewById<ViewGroup>(DIALOG_CONTENT_PARENT_ID) val view = openedDialogs
?.getChildAt(0) .firstOrNull { it.dialog == animateFrom }
?: throw IllegalStateException("No dialog content parent found in host dialog") ?.dialogContentWithBackground
return showFromView(dialog, view, animateBackgroundBoundsChange) ?: 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() { fun disableAllCurrentDialogsExitAnimations() {
openedDialogs.forEach { it.exitAnimationDisabled = true } openedDialogs.forEach { it.exitAnimationDisabled = true }
} }
}
interface HostDialogProvider {
/** /**
* Create a host dialog that will be used to host a launch animation. This host dialog must: * Dismiss [dialog]. If it was launched from another dialog using [showFromView], also dismiss
* 1. call [onCreateCallback] in its onCreate() method, e.g. right after calling * the stack of dialogs, animating back to the original touchSurface.
* 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.
*/ */
fun createHostDialog( fun dismissStack(dialog: Dialog) {
context: Context, openedDialogs
theme: Int, .firstOrNull { it.dialog == dialog }
onCreateCallback: () -> Unit, ?.let { it.touchSurface = it.prepareForStackDismiss() }
dismissOverride: (() -> Unit) -> Unit dialog.dismiss()
): 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,
} }
/** 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 class AnimatedDialog(
private val context: Context, private val context: Context,
private val launchAnimator: LaunchAnimator, private val launchAnimator: LaunchAnimator,
hostDialogProvider: HostDialogProvider, private val dreamManager: IDreamManager,
/** The view that triggered the dialog after being tapped. */ /** The view that triggered the dialog after being tapped. */
var touchSurface: View, var touchSurface: View,
@@ -268,36 +170,33 @@ private class AnimatedDialog(
*/ */
private val onDialogDismissed: (AnimatedDialog) -> Unit, private val onDialogDismissed: (AnimatedDialog) -> Unit,
/** The original dialog whose content will be shown and animate in/out in [hostDialog]. */ /** The dialog to show and animate. */
private val originalDialog: Dialog, val dialog: Dialog,
/** Whether we should animate the dialog background when its bounds change. */ /** Whether we should animate the dialog background when its bounds change. */
private val animateBackgroundBoundsChange: Boolean, private val animateBackgroundBoundsChange: Boolean,
/** Launch animation corresponding to the parent [hostDialog]. */ /** Launch animation corresponding to the parent [AnimatedDialog]. */
private val parentAnimatedDialog: AnimatedDialog? = null private val parentAnimatedDialog: AnimatedDialog? = null
) { ) {
/** /**
* The fullscreen dialog to which we will add the content view [originalDialogView] of * The DecorView of this dialog window.
* [originalDialog]. *
*/ * Note that we access this DecorView lazily to avoid accessing it before the dialog is created,
val hostDialog = hostDialogProvider.createHostDialog( * which can sometimes cause crashes (e.g. with the Cast dialog).
context, R.style.HostDialogTheme, this::onHostDialogCreated, this::onHostDialogDismissed) */
private val decorView by lazy { dialog.window!!.decorView as ViewGroup }
/** The root content view of [hostDialog]. */
private val hostDialogRoot = FrameLayout(context)
/** /**
* The dialog content with its background. When animating a fullscreen dialog, this is just the * 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) * 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 * 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 var dialogContentWithBackground: ViewGroup? = null
/** /**
* The background color of [originalDialogView], taking into consideration the [originalDialog] * The background color of [dialog], taking into consideration its window background color.
* window background color.
*/ */
private var originalDialogBackgroundColor = Color.BLACK private var originalDialogBackgroundColor = Color.BLACK
@@ -310,75 +209,182 @@ private class AnimatedDialog(
private var isDismissing = false private var isDismissing = false
private var dismissRequested = false private var dismissRequested = false
var ignoreNextCallToHide = false
var exitAnimationDisabled = false var exitAnimationDisabled = false
private var isTouchSurfaceGhostDrawn = false private var isTouchSurfaceGhostDrawn = false
private var isOriginalDialogViewLaidOut = 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() AnimatedBoundsLayoutListener()
} else { } else {
null 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() { fun start() {
// Show the host (fullscreen) dialog, to which we will add the stolen dialog view. // Create the dialog so that its onCreate() method is called, which usually sets the dialog
hostDialog.show() // content.
dialog.create()
// Steal the dialog view. We do that by showing it but preventing it from drawing, then val window = dialog.window!!
// hiding it as soon as its content is available. val isWindowFullScreen =
stealOriginalDialogContentView(then = this::showDialogFromView) 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() { // Animate that view with the background. Throw if we didn't find one, because otherwise
// Make the dialog fullscreen with a transparent background. // it's not clear what we should animate.
hostDialog.setContentView( viewGroupWithBackground
hostDialogRoot, ?: throw IllegalStateException("Unable to find ViewGroup with background")
ViewGroup.LayoutParams( } else {
ViewGroup.LayoutParams.MATCH_PARENT, // We will make the dialog window (and therefore its DecorView) fullscreen to make it
ViewGroup.LayoutParams.MATCH_PARENT // 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 val dialogContentWithBackground = FrameLayout(context)
?: throw IllegalStateException("There is no window associated to the host dialog") dialogContentWithBackground.background = decorView.background
window.setBackgroundDrawableResource(android.R.color.transparent)
// If we are using gesture navigation, then we can overlay the navigation/task bars with // Make the window background transparent. Note that setting the window (or DecorView)
// the host dialog. // background drawable to null leads to issues with background color (not being
val navigationMode = context.resources.getInteger( // transparent) or with insets that are not refreshed. Therefore we need to set it to
com.android.internal.R.integer.config_navBarInteractionMode) // something not null, hence we are using android.R.color.transparent here.
if (navigationMode == WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL) { window.setBackgroundDrawableResource(android.R.color.transparent)
window.attributes.fitInsetsTypes = window.attributes.fitInsetsTypes and
WindowInsets.Type.navigationBars().inv() // Close the dialog when clicking outside of it.
window.addFlags(FLAG_LAYOUT_IN_SCREEN or FLAG_LAYOUT_INSET_DECOR) fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() }
window.setDecorFitsSystemWindows(false) 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. // Disable the dim. We will enable it once we start the animation.
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) 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 // 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 // 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 // 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 // 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). // 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 { override fun onPreDraw(): Boolean {
hostDialogRoot.viewTreeObserver.removeOnPreDrawListener(this) decorView.viewTreeObserver.removeOnPreDrawListener(this)
addTemporaryTouchSurfaceGhost() addTemporaryTouchSurfaceGhost()
return true return true
} }
}) })
hostDialogRoot.invalidate() decorView.invalidate()
} }
private fun addTemporaryTouchSurfaceGhost() { private fun addTemporaryTouchSurfaceGhost() {
// Create a ghost of the touch surface (which will make the touch surface invisible) and add // 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 // it to the dialog. We will wait for this ghost to be drawn before starting the animation.
// animation. val ghost = GhostView.addGhost(touchSurface, decorView)
val ghost = GhostView.addGhost(touchSurface, hostDialogRoot)
// The ghost of the touch surface was just created, so the touch surface was made invisible. // 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. // We make it visible again until the ghost is actually drawn.
@@ -414,142 +420,6 @@ private class AnimatedDialog(
touchSurface.invalidate() 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? { private fun findFirstViewGroupWithBackground(view: View): ViewGroup? {
if (view !is ViewGroup) { if (view !is ViewGroup) {
return null return null
@@ -569,26 +439,13 @@ private class AnimatedDialog(
return null 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() { private fun maybeStartLaunchAnimation() {
if (!isTouchSurfaceGhostDrawn || !isOriginalDialogViewLaidOut) { if (!isTouchSurfaceGhostDrawn || !isOriginalDialogViewLaidOut) {
return return
} }
// Show the background dim. // Show the background dim.
hostDialog.window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) dialog.window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
startAnimation( startAnimation(
isLaunching = true, isLaunching = true,
@@ -610,7 +467,7 @@ private class AnimatedDialog(
// dismiss was called during the animation, dismiss again now to actually // dismiss was called during the animation, dismiss again now to actually
// dismiss. // dismiss.
if (dismissRequested) { if (dismissRequested) {
hostDialog.dismiss() dialog.dismiss()
} }
// If necessary, we animate the dialog background when its bounds change. We do it // 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()) { if (Looper.myLooper() != Looper.getMainLooper()) {
context.mainExecutor.execute { onHostDialogDismissed(actualDismiss) } context.mainExecutor.execute { onDialogDismissed() }
return return
} }
@@ -641,23 +498,29 @@ private class AnimatedDialog(
} }
isDismissing = true isDismissing = true
hideDialogIntoView { instantDismiss: Boolean -> hideDialogIntoView { animationRan: Boolean ->
if (instantDismiss) { if (animationRan) {
originalDialog.hide() // Instantly dismiss the dialog if we ran the animation into view. If it was
hostDialog.hide() // skipped, dismiss() will run the window animation (which fades out the dialog).
dialog.hide()
} }
originalDialog.dismiss() dialog.setDismissOverride(null)
actualDismiss() dialog.dismiss()
} }
} }
/** /**
* Hide the dialog into the touch surface and call [dismissDialogs] when the animation is done * Hide the dialog into the touch surface and call [onAnimationFinished] when the animation is
* (passing instantDismiss=true) or if it's skipped (passing instantDismiss=false) to actually * done (passing animationRan=true) or if it's skipped (passing animationRan=false) to actually
* dismiss the dialogs. * 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()) { if (!shouldAnimateDialogIntoView()) {
Log.i(TAG, "Skipping animation of dialog into the touch surface") Log.i(TAG, "Skipping animation of dialog into the touch surface")
@@ -669,7 +532,7 @@ private class AnimatedDialog(
touchSurface.visibility = View.VISIBLE touchSurface.visibility = View.VISIBLE
} }
dismissDialogs(false /* instantDismiss */) onAnimationFinished(false /* instantDismiss */)
onDialogDismissed(this@AnimatedDialog) onDialogDismissed(this@AnimatedDialog)
return return
} }
@@ -678,7 +541,7 @@ private class AnimatedDialog(
isLaunching = false, isLaunching = false,
onLaunchAnimationStart = { onLaunchAnimationStart = {
// Remove the dim background as soon as we start the animation. // 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 = { onLaunchAnimationEnd = {
// Make sure we allow the touch surface to change its visibility again. // 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 // 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 // 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). // 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 { touchSurface.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
override fun onPreDraw(): Boolean { override fun onPreDraw(): Boolean {
@@ -705,7 +568,7 @@ private class AnimatedDialog(
// Now that the touch surface was drawn, we can remove the temporary ghost // Now that the touch surface was drawn, we can remove the temporary ghost
// and instantly dismiss the dialog. // and instantly dismiss the dialog.
GhostView.removeGhost(touchSurface) GhostView.removeGhost(touchSurface)
dismissDialogs(true /* instantDismiss */) onAnimationFinished(true /* instantDismiss */)
onDialogDismissed(this@AnimatedDialog) onDialogDismissed(this@AnimatedDialog)
return true return true
@@ -721,14 +584,14 @@ private class AnimatedDialog(
onLaunchAnimationStart: () -> Unit = {}, onLaunchAnimationStart: () -> Unit = {},
onLaunchAnimationEnd: () -> 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. // dialog.
val startView = if (isLaunching) touchSurface else dialogContentWithBackground!! val startView = if (isLaunching) touchSurface else dialogContentWithBackground!!
val endView = if (isLaunching) dialogContentWithBackground!! else touchSurface val endView = if (isLaunching) dialogContentWithBackground!! else touchSurface
val startViewController = GhostedViewLaunchAnimatorController(startView) val startViewController = GhostedViewLaunchAnimatorController(startView)
val endViewController = GhostedViewLaunchAnimatorController(endView) val endViewController = GhostedViewLaunchAnimatorController(endView)
startViewController.launchContainer = hostDialogRoot startViewController.launchContainer = decorView
endViewController.launchContainer = hostDialogRoot endViewController.launchContainer = decorView
val endState = endViewController.createAnimatorState() val endState = endViewController.createAnimatorState()
val controller = object : LaunchAnimator.Controller { val controller = object : LaunchAnimator.Controller {
@@ -785,9 +648,15 @@ private class AnimatedDialog(
} }
private fun shouldAnimateDialogIntoView(): Boolean { private fun shouldAnimateDialogIntoView(): Boolean {
// Don't animate if the dialog was previously hidden using hide() (either on the host dialog // Don't animate if the dialog was previously hidden using hide() or if we disabled the exit
// or on the original dialog) or if we disabled the exit animation. // animation.
if (exitAnimationDisabled || !hostDialog.isShowing) { 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 return false
} }
@@ -888,9 +757,9 @@ private class AnimatedDialog(
return touchSurface return touchSurface
} }
parentAnimatedDialog.exitAnimationDisabled = true parentAnimatedDialog.exitAnimationDisabled = true
parentAnimatedDialog.originalDialog.hide() parentAnimatedDialog.dialog.hide()
val view = parentAnimatedDialog.prepareForStackDismiss() 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 // Make the touch surface invisible, so we end up animating to it when we actually
// dismiss the stack // dismiss the stack
view.visibility = View.INVISIBLE view.visibility = View.INVISIBLE

View File

@@ -217,14 +217,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
dismiss(); dismiss();
} }
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (!hasFocus && isShowing()) {
dismiss();
}
}
void onHeaderIconClick() { void onHeaderIconClick() {
} }

View File

@@ -190,14 +190,10 @@ public class DndTile extends QSTileImpl<BooleanState> {
case Settings.Secure.ZEN_DURATION_PROMPT: case Settings.Secure.ZEN_DURATION_PROMPT:
mUiHandler.post(() -> { mUiHandler.post(() -> {
Dialog dialog = makeZenModeDialog(); Dialog dialog = makeZenModeDialog();
SystemUIDialog.registerDismissListener(dialog);
if (view != null) { if (view != null) {
final Dialog hostDialog = mDialogLaunchAnimator.showFromView(dialog, view, false);
mDialogLaunchAnimator.showFromView(dialog, view, false);
setDialogListeners(dialog, hostDialog);
} else { } else {
// If we are not launching with animator, register default
// dismiss listener
SystemUIDialog.registerDismissListener(dialog);
dialog.show(); dialog.show();
} }
}); });
@@ -222,12 +218,6 @@ public class DndTile extends QSTileImpl<BooleanState> {
return dialog; 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 @Override
protected void handleSecondaryClick(@Nullable View view) { protected void handleSecondaryClick(@Nullable View view) {
if (mController.isVolumeRestricted()) { if (mController.isVolumeRestricted()) {

View File

@@ -94,24 +94,24 @@ class UserSwitchDialogController @VisibleForTesting constructor(
adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid)) adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid))
val hostDialog = dialogLaunchAnimator.showFromView(this, view) dialogLaunchAnimator.showFromView(this, view)
adapter.injectDialogShower(DialogShowerImpl(hostDialog, dialogLaunchAnimator)) adapter.injectDialogShower(DialogShowerImpl(this, dialogLaunchAnimator))
} }
} }
private class DialogShowerImpl( private class DialogShowerImpl(
private val hostDialog: Dialog, private val animateFrom: Dialog,
private val dialogLaunchAnimator: DialogLaunchAnimator private val dialogLaunchAnimator: DialogLaunchAnimator
) : DialogInterface by hostDialog, DialogShower { ) : DialogInterface by animateFrom, DialogShower {
override fun showDialog(dialog: Dialog): Dialog { override fun showDialog(dialog: Dialog) {
return dialogLaunchAnimator.showFromDialog( dialogLaunchAnimator.showFromDialog(
dialog, dialog,
parentHostDialog = hostDialog animateFrom = animateFrom
) )
} }
} }
interface DialogShower : DialogInterface { 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.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.os.Handler; import android.os.Handler;
import android.service.dreams.IDreamManager;
import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.animation.ActivityLaunchAnimator; 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.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl; import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl;
import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback; 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.OngoingCallController;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLogger; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLogger;
import com.android.systemui.statusbar.policy.RemoteInputUriController; import com.android.systemui.statusbar.policy.RemoteInputUriController;
import com.android.systemui.statusbar.window.StatusBarWindowController; 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.tracing.ProtoTracer;
import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.time.SystemClock; import com.android.systemui.util.time.SystemClock;
@@ -320,7 +318,7 @@ public interface StatusBarDependenciesModule {
@Provides @Provides
@SysUISingleton @SysUISingleton
static DialogLaunchAnimator provideDialogLaunchAnimator(Context context, static DialogLaunchAnimator provideDialogLaunchAnimator(Context context,
LaunchAnimator launchAnimator) { LaunchAnimator launchAnimator, IDreamManager dreamManager) {
return new DialogLaunchAnimator(context, launchAnimator, new SystemUIHostDialogProvider()); 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.Dependency;
import com.android.systemui.R; 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.broadcast.BroadcastDispatcher;
import com.android.systemui.statusbar.policy.KeyguardStateController; 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. * Base class for dialogs that should appear over panels and keyguard.
* The SystemUIDialog registers a listener for the screen off / close system dialogs broadcast, * The SystemUIDialog registers a listener for the screen off / close system dialogs broadcast,
* and dismisses itself when it receives the broadcast. * and dismisses itself when it receives the broadcast.
*/ */
public class SystemUIDialog extends AlertDialog implements ListenableDialog, public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigChangedCallback {
ViewRootImpl.ConfigChangedCallback {
// TODO(b/203389579): Remove this once the dialog width on large screens has been agreed on. // TODO(b/203389579): Remove this once the dialog width on large screens has been agreed on.
private static final String FLAG_TABLET_DIALOG_WIDTH = private static final String FLAG_TABLET_DIALOG_WIDTH =
"persist.systemui.flag_tablet_dialog_width"; "persist.systemui.flag_tablet_dialog_width";
private final Context mContext; private final Context mContext;
private final DismissReceiver mDismissReceiver; private final DismissReceiver mDismissReceiver;
private final Set<DialogListener> mDialogListeners = new LinkedHashSet<>();
private final Handler mHandler = new Handler(); private final Handler mHandler = new Handler();
private int mLastWidth = Integer.MIN_VALUE; private int mLastWidth = Integer.MIN_VALUE;
@@ -117,10 +108,6 @@ public class SystemUIDialog extends AlertDialog implements ListenableDialog,
mLastWidth = width; mLastWidth = width;
mLastHeight = height; mLastHeight = height;
getWindow().setLayout(width, height); getWindow().setLayout(width, height);
for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) {
listener.onSizeChanged();
}
} }
@Override @Override
@@ -197,60 +184,6 @@ public class SystemUIDialog extends AlertDialog implements ListenableDialog,
ViewRootImpl.removeConfigCallback(this); 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) { public void setShowForAllUsers(boolean show) {
setShowForAllUsers(this, show); setShowForAllUsers(this, show);
} }
@@ -364,11 +297,7 @@ public class SystemUIDialog extends AlertDialog implements ListenableDialog,
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (mDialog instanceof SystemUIDialog) { mDialog.dismiss();
((SystemUIDialog) mDialog).dismiss(DismissReason.DEVICE_LOCKED);
} else {
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.Prefs.Key;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.SystemUISecondaryUserService; import com.android.systemui.SystemUISecondaryUserService;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Background;
@@ -133,6 +134,7 @@ public class UserSwitcherController implements Dumpable {
private final IActivityTaskManager mActivityTaskManager; private final IActivityTaskManager mActivityTaskManager;
private final InteractionJankMonitor mInteractionJankMonitor; private final InteractionJankMonitor mInteractionJankMonitor;
private final LatencyTracker mLatencyTracker; private final LatencyTracker mLatencyTracker;
private final DialogLaunchAnimator mDialogLaunchAnimator;
private ArrayList<UserRecord> mUsers = new ArrayList<>(); private ArrayList<UserRecord> mUsers = new ArrayList<>();
@VisibleForTesting @VisibleForTesting
@@ -180,7 +182,8 @@ public class UserSwitcherController implements Dumpable {
@Background Executor bgExecutor, @Background Executor bgExecutor,
InteractionJankMonitor interactionJankMonitor, InteractionJankMonitor interactionJankMonitor,
LatencyTracker latencyTracker, LatencyTracker latencyTracker,
DumpManager dumpManager) { DumpManager dumpManager,
DialogLaunchAnimator dialogLaunchAnimator) {
mContext = context; mContext = context;
mActivityManager = activityManager; mActivityManager = activityManager;
mUserTracker = userTracker; mUserTracker = userTracker;
@@ -208,6 +211,8 @@ public class UserSwitcherController implements Dumpable {
mHandler = handler; mHandler = handler;
mActivityStarter = activityStarter; mActivityStarter = activityStarter;
mUserManager = userManager; mUserManager = userManager;
mDialogLaunchAnimator = dialogLaunchAnimator;
IntentFilter filter = new IntentFilter(); IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_USER_ADDED); filter.addAction(Intent.ACTION_USER_ADDED);
filter.addAction(Intent.ACTION_USER_REMOVED); filter.addAction(Intent.ACTION_USER_REMOVED);
@@ -1179,7 +1184,7 @@ public class UserSwitcherController implements Dumpable {
cancel(); cancel();
} else { } else {
mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE); mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE);
dismissStack(); mDialogLaunchAnimator.dismissStack(this);
removeGuestUser(mGuestId, mTargetId); removeGuestUser(mGuestId, mTargetId);
} }
} }
@@ -1210,7 +1215,7 @@ public class UserSwitcherController implements Dumpable {
if (which == BUTTON_NEGATIVE) { if (which == BUTTON_NEGATIVE) {
cancel(); cancel();
} else { } else {
dismissStack(); mDialogLaunchAnimator.dismissStack(this);
if (ActivityManager.isUserAMonkey()) { if (ActivityManager.isUserAMonkey()) {
return; return;
} }

View File

@@ -2,35 +2,49 @@ package com.android.systemui.animation
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.service.dreams.IDreamManager
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.testing.TestableLooper import android.testing.TestableLooper
import android.testing.ViewUtils import android.testing.ViewUtils
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.WindowManager import android.view.WindowManager
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.internal.policy.DecorView
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogListener.DismissReason
import junit.framework.Assert.assertEquals import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertNotNull
import junit.framework.Assert.assertTrue import junit.framework.Assert.assertTrue
import org.junit.After import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
@SmallTest @SmallTest
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper @TestableLooper.RunWithLooper
class DialogLaunchAnimatorTest : SysuiTestCase() { class DialogLaunchAnimatorTest : SysuiTestCase() {
private val launchAnimator = LaunchAnimator(context, isForTesting = true) private val launchAnimator = LaunchAnimator(context, isForTesting = true)
private val hostDialogprovider = TestHostDialogProvider() private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
private val dialogLaunchAnimator =
DialogLaunchAnimator(context, launchAnimator, hostDialogprovider)
private val attachedViews = mutableSetOf<View>() 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 @After
fun tearDown() { fun tearDown() {
runOnMainThreadAndWaitForIdleSync { runOnMainThreadAndWaitForIdleSync {
@@ -44,76 +58,66 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
fun testShowDialogFromView() { fun testShowDialogFromView() {
// Show the dialog. showFromView() must be called on the main thread with a dialog created // Show the dialog. showFromView() must be called on the main thread with a dialog created
// on the main thread too. // on the main thread too.
val (dialog, hostDialog) = createDialogAndHostDialog() val dialog = createAndShowDialog()
// Only the host dialog is actually showing. assertTrue(dialog.isShowing)
assertTrue(hostDialog.isShowing)
assertFalse(dialog.isShowing)
// The dialog onStart() method was called but not onStop(). // The dialog is now fullscreen.
assertTrue(dialog.onStartCalled) val window = dialog.window
assertFalse(dialog.onStopCalled) 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. // The single DecorView child is a transparent fullscreen view that will dismiss the dialog
val hostDialogContent = hostDialog.findViewById<ViewGroup>(android.R.id.content) // when clicked.
assertEquals(0, dialog.findViewById<ViewGroup>(android.R.id.content).childCount) assertEquals(1, decorView.childCount)
assertEquals(1, hostDialogContent.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 // The single transparent background child is a fake window with the same size and
// original dialog window. // background as the dialog initially had.
val hostDialogRoot = hostDialogContent.getChildAt(0) as ViewGroup assertEquals(1, transparentBackground.childCount)
assertEquals(1, hostDialogRoot.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 // The dialog content is inside this fake window view.
assertEquals(1, dialogContentParent.childCount) assertNotNull(
assertEquals(TestDialog.DIALOG_WIDTH, dialogContentParent.layoutParams.width) dialogContentWithBackground.findViewByPredicate { it === dialog.contentView })
assertEquals(TestDialog.DIALOG_HEIGHT, dialogContentParent.layoutParams.height)
val dialogContent = dialogContentParent.getChildAt(0) // Clicking the transparent background should dismiss the dialog.
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)
runOnMainThreadAndWaitForIdleSync { runOnMainThreadAndWaitForIdleSync {
// TODO(b/204561691): Remove this call to disableAllCurrentDialogsExitAnimations() and // 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 // make sure that the test still pass on git_master/cf_x86_64_phone-userdebug in
// Forrest. // Forrest.
dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations() dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations()
dialog.dismiss() transparentBackground.performClick()
} }
assertFalse(hostDialog.isShowing)
assertFalse(dialog.isShowing) assertFalse(dialog.isShowing)
assertTrue(hostDialog.wasDismissed)
assertTrue(dialog.onStopCalled)
} }
@Test @Test
fun testStackedDialogsDismissesAll() { fun testStackedDialogsDismissesAll() {
val (_, hostDialogFirst) = createDialogAndHostDialog() val firstDialog = createAndShowDialog()
val (dialogSecond, hostDialogSecond) = createDialogAndHostDialogFromDialog(hostDialogFirst) val secondDialog = createDialogAndShowFromDialog(firstDialog)
assertTrue(firstDialog.isShowing)
assertTrue(secondDialog.isShowing)
runOnMainThreadAndWaitForIdleSync { runOnMainThreadAndWaitForIdleSync {
dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations() dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations()
dialogSecond.dismissStack() dialogLaunchAnimator.dismissStack(secondDialog)
} }
assertTrue(hostDialogSecond.wasDismissed) assertFalse(firstDialog.isShowing)
assertTrue(hostDialogFirst.wasDismissed) assertFalse(secondDialog.isShowing)
} }
private fun createDialogAndHostDialog(): Pair<TestDialog, TestHostDialog> { private fun createAndShowDialog(): TestDialog {
return runOnMainThreadAndWaitForIdleSync { return runOnMainThreadAndWaitForIdleSync {
val touchSurfaceRoot = LinearLayout(context) val touchSurfaceRoot = LinearLayout(context)
val touchSurface = View(context) val touchSurface = View(context)
@@ -125,22 +129,16 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
attachedViews.add(touchSurfaceRoot) attachedViews.add(touchSurfaceRoot)
val dialog = TestDialog(context) val dialog = TestDialog(context)
val hostDialog = dialogLaunchAnimator.showFromView(dialog, touchSurface)
dialogLaunchAnimator.showFromView(dialog, touchSurface) as TestHostDialog dialog
dialog to hostDialog
} }
} }
private fun createDialogAndHostDialogFromDialog( private fun createDialogAndShowFromDialog(animateFrom: Dialog): TestDialog {
hostParent: Dialog
): Pair<TestDialog, TestHostDialog> {
return runOnMainThreadAndWaitForIdleSync { return runOnMainThreadAndWaitForIdleSync {
val dialog = TestDialog(context) val dialog = TestDialog(context)
val hostDialog = dialogLaunchAnimator.showFromDialog( dialogLaunchAnimator.showFromDialog(dialog, animateFrom)
dialog, dialog
hostParent
) as TestHostDialog
dialog to hostDialog
} }
} }
@@ -153,50 +151,14 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
return result return result
} }
private class TestHostDialogProvider : HostDialogProvider { private class TestDialog(context: Context) : Dialog(context) {
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 {
companion object { companion object {
const val DIALOG_WIDTH = 100 const val DIALOG_WIDTH = 100
const val DIALOG_HEIGHT = 200 const val DIALOG_HEIGHT = 200
} }
private val listeners = hashSetOf<DialogListener>()
val contentView = View(context) val contentView = View(context)
var onStartCalled = false val windowBackground = ColorDrawable(Color.RED)
var onStopCalled = false
init { init {
// We need to set the window type for dialogs shown by SysUI, otherwise WM will throw. // 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
window.setLayout(DIALOG_WIDTH, DIALOG_HEIGHT)
setContentView(contentView) setContentView(contentView)
}
override fun onStart() { window.setLayout(DIALOG_WIDTH, DIALOG_HEIGHT)
super.onStart() window.setBackgroundDrawable(windowBackground)
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()
}
} }
} }
} }

View File

@@ -16,7 +16,6 @@
package com.android.systemui.qs.user package com.android.systemui.qs.user
import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.provider.Settings 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.qs.tiles.UserDetailView
import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.mockito.any 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.capture
import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.eq
import org.junit.Before import org.junit.Before
@@ -42,7 +40,6 @@ import org.mockito.ArgumentMatcher
import org.mockito.Captor import org.mockito.Captor
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyInt
import org.mockito.Mockito.argThat import org.mockito.Mockito.argThat
import org.mockito.Mockito.never import org.mockito.Mockito.never
@@ -65,8 +62,6 @@ class UserSwitchDialogControllerTest : SysuiTestCase() {
private lateinit var launchView: View private lateinit var launchView: View
@Mock @Mock
private lateinit var dialogLaunchAnimator: DialogLaunchAnimator private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
@Mock
private lateinit var hostDialog: Dialog
@Captor @Captor
private lateinit var clickCaptor: ArgumentCaptor<DialogInterface.OnClickListener> private lateinit var clickCaptor: ArgumentCaptor<DialogInterface.OnClickListener>
@@ -78,8 +73,6 @@ class UserSwitchDialogControllerTest : SysuiTestCase() {
`when`(launchView.context).thenReturn(mContext) `when`(launchView.context).thenReturn(mContext)
`when`(dialog.context).thenReturn(mContext) `when`(dialog.context).thenReturn(mContext)
`when`(dialogLaunchAnimator.showFromView(any(), any(), anyBoolean()))
.thenReturn(hostDialog)
controller = UserSwitchDialogController( controller = UserSwitchDialogController(
{ userDetailViewAdapter }, { userDetailViewAdapter },
@@ -151,18 +144,6 @@ class UserSwitchDialogControllerTest : SysuiTestCase() {
verify(activityStarter, never()).postStartActivityDismissingKeyguard(any(), anyInt()) 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> { private class IntentMatcher(private val action: String) : ArgumentMatcher<Intent> {
override fun matches(argument: Intent?): Boolean { override fun matches(argument: Intent?): Boolean {
return argument?.action == action 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.GuestResumeSessionReceiver
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dump.DumpManager import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
@@ -94,6 +95,7 @@ class UserSwitcherControllerTest : SysuiTestCase() {
@Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower @Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower
@Mock private lateinit var notificationShadeWindowView: NotificationShadeWindowView @Mock private lateinit var notificationShadeWindowView: NotificationShadeWindowView
@Mock private lateinit var threadedRenderer: ThreadedRenderer @Mock private lateinit var threadedRenderer: ThreadedRenderer
@Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
private lateinit var testableLooper: TestableLooper private lateinit var testableLooper: TestableLooper
private lateinit var uiBgExecutor: FakeExecutor private lateinit var uiBgExecutor: FakeExecutor
private lateinit var uiEventLogger: UiEventLoggerFake private lateinit var uiEventLogger: UiEventLoggerFake
@@ -147,7 +149,8 @@ class UserSwitcherControllerTest : SysuiTestCase() {
uiBgExecutor, uiBgExecutor,
interactionJankMonitor, interactionJankMonitor,
latencyTracker, latencyTracker,
dumpManager) dumpManager,
dialogLaunchAnimator)
userSwitcherController.mPauseRefreshUsers = true userSwitcherController.mPauseRefreshUsers = true
// Since userSwitcherController involves InteractionJankMonitor. // Since userSwitcherController involves InteractionJankMonitor.