[DO NOT MERGE] Animate dialog stack and use in UserSwitcher am: 327d67cb2a

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/16244707

Change-Id: Ic3b4406d1d64029a7062772f0eb841ec612f6f74
diff --git a/packages/SystemUI/animation/res/values/ids.xml b/packages/SystemUI/animation/res/values/ids.xml
index ef60a24..c4cb89f 100644
--- a/packages/SystemUI/animation/res/values/ids.xml
+++ b/packages/SystemUI/animation/res/values/ids.xml
@@ -16,4 +16,5 @@
 -->
 <resources>
     <item type="id" name="launch_animation_running"/>
+    <item type="id" name="dialog_content_parent" />
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index 413612f..9aad278 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -40,6 +40,7 @@
 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
@@ -86,10 +87,10 @@
         // 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 dialogContentParent = openedDialogs
+        val animatedParent = openedDialogs
             .firstOrNull { it.dialogContentParent == view.parent }
-            ?.dialogContentParent
-        val animateFrom = dialogContentParent ?: view
+        val parentHostDialog = animatedParent?.hostDialog
+        val animateFrom = animatedParent?.dialogContentParent ?: 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) {
@@ -100,12 +101,18 @@
 
         animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
 
-        val launchAnimation = AnimatedDialog(
-            context, launchAnimator, hostDialogProvider, animateFrom,
-            onDialogDismissed = { openedDialogs.remove(it) }, originalDialog = dialog,
-            animateBackgroundBoundsChange)
-        val hostDialog = launchAnimation.hostDialog
-        openedDialogs.add(launchAnimation)
+        val animatedDialog = AnimatedDialog(
+                context,
+                launchAnimator,
+                hostDialogProvider,
+                animateFrom,
+                onDialogDismissed = { openedDialogs.remove(it) },
+                originalDialog = dialog,
+                animateBackgroundBoundsChange,
+                openedDialogs.firstOrNull { it.hostDialog == parentHostDialog }
+        )
+        val hostDialog = animatedDialog.hostDialog
+        openedDialogs.add(animatedDialog)
 
         // If the dialog is dismissed/hidden/shown, then we should actually dismiss/hide/show the
         // host dialog.
@@ -119,15 +126,15 @@
                     // If AOD is disabled the screen will directly becomes black and we won't see
                     // the animation anyways.
                     if (reason == DialogListener.DismissReason.DEVICE_LOCKED) {
-                        launchAnimation.exitAnimationDisabled = true
+                        animatedDialog.exitAnimationDisabled = true
                     }
 
                     hostDialog.dismiss()
                 }
 
                 override fun onHide() {
-                    if (launchAnimation.ignoreNextCallToHide) {
-                        launchAnimation.ignoreNextCallToHide = false
+                    if (animatedDialog.ignoreNextCallToHide) {
+                        animatedDialog.ignoreNextCallToHide = false
                         return
                     }
 
@@ -138,21 +145,44 @@
                     hostDialog.show()
 
                     // We don't actually want to show the original dialog, so hide it.
-                    launchAnimation.ignoreNextCallToHide = true
+                    animatedDialog.ignoreNextCallToHide = true
                     dialog.hide()
                 }
 
                 override fun onSizeChanged() {
-                    launchAnimation.onOriginalDialogSizeChanged()
+                    animatedDialog.onOriginalDialogSizeChanged()
+                }
+
+                override fun prepareForStackDismiss() {
+                    animatedDialog.touchSurface = animatedDialog.prepareForStackDismiss()
                 }
             })
         }
 
-        launchAnimation.start()
+        animatedDialog.start()
         return hostDialog
     }
 
     /**
+     * Launch [dialog] from a [parentHostDialog] as returned by [showFromView]. This will allow
+     * for dismissing the whole stack.
+     *
+     * This will return a new host dialog, with the same caveat as [showFromView].
+     *
+     * @see DialogListener.prepareForStackDismiss
+     */
+    fun showFromDialog(
+        dialog: Dialog,
+        parentHostDialog: 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)
+    }
+
+    /**
      * Ensure that all dialogs currently shown won't animate into their touch surface when
      * dismissed.
      *
@@ -214,6 +244,12 @@
     /** 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()
 }
@@ -224,7 +260,7 @@
     hostDialogProvider: HostDialogProvider,
 
     /** The view that triggered the dialog after being tapped. */
-    private val touchSurface: View,
+    var touchSurface: View,
 
     /**
      * A callback that will be called with this [AnimatedDialog] after the dialog was
@@ -236,7 +272,10 @@
     private val originalDialog: Dialog,
 
     /** 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]. */
+    private val parentAnimatedDialog: AnimatedDialog? = null
 ) {
     /**
      * The fullscreen dialog to which we will add the content view [originalDialogView] of
@@ -253,7 +292,9 @@
      * the same size as the original dialog window and to which we will set the original dialog
      * window background.
      */
-    val dialogContentParent = FrameLayout(context)
+    val dialogContentParent = FrameLayout(context).apply {
+        id = DIALOG_CONTENT_PARENT_ID
+    }
 
     /**
      * The background color of [originalDialogView], taking into consideration the [originalDialog]
@@ -359,9 +400,7 @@
         // Make the touch surface invisible and make sure that it stays invisible as long as the
         // dialog is shown or animating.
         touchSurface.visibility = View.INVISIBLE
-        if (touchSurface is LaunchableView) {
-            touchSurface.setShouldBlockVisibilityChanges(true)
-        }
+        (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
 
         // Add a pre draw listener to (maybe) start the animation once the touch surface is
         // actually invisible.
@@ -576,9 +615,7 @@
             Log.i(TAG, "Skipping animation of dialog into the touch surface")
 
             // Make sure we allow the touch surface to change its visibility again.
-            if (touchSurface is LaunchableView) {
-                touchSurface.setShouldBlockVisibilityChanges(false)
-            }
+            (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
 
             // If the view is invisible it's probably because of us, so we make it visible again.
             if (touchSurface.visibility == View.INVISIBLE) {
@@ -598,9 +635,7 @@
             },
             onLaunchAnimationEnd = {
                 // Make sure we allow the touch surface to change its visibility again.
-                if (touchSurface is LaunchableView) {
-                    touchSurface.setShouldBlockVisibilityChanges(false)
-                }
+                (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
 
                 touchSurface.visibility = View.VISIBLE
                 dialogContentParent.visibility = View.INVISIBLE
@@ -796,4 +831,18 @@
             animator.start()
         }
     }
+
+    fun prepareForStackDismiss(): View {
+        if (parentAnimatedDialog == null) {
+            return touchSurface
+        }
+        parentAnimatedDialog.exitAnimationDisabled = true
+        parentAnimatedDialog.originalDialog.hide()
+        val view = parentAnimatedDialog.prepareForStackDismiss()
+        parentAnimatedDialog.originalDialog.dismiss()
+        // Make the touch surface invisible, so we end up animating to it when we actually
+        // dismiss the stack
+        view.visibility = View.INVISIBLE
+        return view
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index 821bd51..be9aa0e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -28,6 +28,8 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.Nullable;
+
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -37,10 +39,10 @@
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.qs.PseudoGridView;
 import com.android.systemui.qs.QSUserSwitcherEvent;
+import com.android.systemui.qs.user.UserSwitchDialogController;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 
-import java.util.function.Consumer;
-
 import javax.inject.Inject;
 
 /**
@@ -77,7 +79,7 @@
         private View mCurrentUserView;
         private final UiEventLogger mUiEventLogger;
         private final FalsingManager mFalsingManager;
-        private Consumer<UserSwitcherController.UserRecord> mClickCallback;
+        private @Nullable UserSwitchDialogController.DialogShower mDialogShower;
 
         @Inject
         public Adapter(Context context, UserSwitcherController controller,
@@ -95,8 +97,17 @@
             return createUserDetailItemView(convertView, parent, item);
         }
 
-        public void injectCallback(Consumer<UserSwitcherController.UserRecord> clickCallback) {
-            mClickCallback = clickCallback;
+        /**
+         * If this adapter is inside a dialog, passing a
+         * {@link UserSwitchDialogController.DialogShower} will help animate to and from the parent
+         * dialog. This will also allow for dismissing the whole stack of dialogs in a single
+         * animation.
+         *
+         * @param shower
+         * @see SystemUIDialog#dismissStack()
+         */
+        public void injectDialogShower(UserSwitchDialogController.DialogShower shower) {
+            mDialogShower = shower;
         }
 
         public UserDetailItemView createUserDetailItemView(View convertView, ViewGroup parent,
@@ -172,10 +183,7 @@
                     }
                     view.setActivated(true);
                 }
-                onUserListItemClicked(tag);
-            }
-            if (mClickCallback != null) {
-                mClickCallback.accept(tag);
+                onUserListItemClicked(tag, mDialogShower);
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
index bae7996..d74a50e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
@@ -16,7 +16,9 @@
 
 package com.android.systemui.qs.user
 
+import android.app.Dialog
 import android.content.Context
+import android.content.DialogInterface
 import android.content.Intent
 import android.provider.Settings
 import android.view.View
@@ -84,12 +86,26 @@
             doneButton.setOnClickListener { dismiss() }
 
             val adapter = userDetailViewAdapterProvider.get()
-            adapter.injectCallback {
-                dismiss()
-            }
             adapter.linkToViewGroup(grid)
 
-            dialogLaunchAnimator.showFromView(this, view)
+            val hostDialog = dialogLaunchAnimator.showFromView(this, view)
+            adapter.injectDialogShower(DialogShowerImpl(hostDialog, dialogLaunchAnimator))
         }
     }
+
+    private class DialogShowerImpl(
+        private val hostDialog: Dialog,
+        private val dialogLaunchAnimator: DialogLaunchAnimator
+    ) : DialogInterface by hostDialog, DialogShower {
+        override fun showDialog(dialog: Dialog): Dialog {
+            return dialogLaunchAnimator.showFromDialog(
+                dialog,
+                parentHostDialog = hostDialog
+            )
+        }
+    }
+
+    interface DialogShower : DialogInterface {
+        fun showDialog(dialog: Dialog): Dialog
+    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index cf4aaba..1130ec2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -218,6 +218,19 @@
         }
     }
 
+    /**
+     * 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();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index b630689..fd387ae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -55,6 +55,8 @@
 import android.view.WindowManagerGlobal;
 import android.widget.BaseAdapter;
 
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.UiEventLogger;
@@ -77,6 +79,7 @@
 import com.android.systemui.plugins.qs.DetailAdapter;
 import com.android.systemui.qs.QSUserSwitcherEvent;
 import com.android.systemui.qs.tiles.UserDetailView;
+import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -460,7 +463,7 @@
     }
 
     @VisibleForTesting
-    void onUserListItemClicked(UserRecord record) {
+    void onUserListItemClicked(UserRecord record, DialogShower dialogShower) {
         int id;
         if (record.isGuest && record.info == null) {
             // No guest user. Create one.
@@ -472,7 +475,7 @@
             mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD);
             id = guestId;
         } else if (record.isAddUser) {
-            showAddUserDialog();
+            showAddUserDialog(dialogShower);
             return;
         } else {
             id = record.info.id;
@@ -481,7 +484,7 @@
         int currUserId = mUserTracker.getUserId();
         if (currUserId == id) {
             if (record.isGuest) {
-                showExitGuestDialog(id);
+                showExitGuestDialog(id, dialogShower);
             }
             return;
         }
@@ -490,11 +493,15 @@
             // If switching from guest, we want to bring up the guest exit dialog instead of switching
             UserInfo currUserInfo = mUserManager.getUserInfo(currUserId);
             if (currUserInfo != null && currUserInfo.isGuest()) {
-                showExitGuestDialog(currUserId, record.resolveId());
+                showExitGuestDialog(currUserId, record.resolveId(), dialogShower);
                 return;
             }
         }
-
+        if (dialogShower != null) {
+            // If we haven't morphed into another dialog, it means we have just switched users.
+            // Then, dismiss the dialog.
+            dialogShower.dismiss();
+        }
         switchToUserId(id);
     }
 
@@ -511,7 +518,7 @@
         }
     }
 
-    protected void showExitGuestDialog(int id) {
+    private void showExitGuestDialog(int id, DialogShower dialogShower) {
         int newId = UserHandle.USER_SYSTEM;
         if (mResumeUserOnGuestLogout && mLastNonGuestUser != UserHandle.USER_SYSTEM) {
             UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser);
@@ -519,23 +526,31 @@
                 newId = info.id;
             }
         }
-        showExitGuestDialog(id, newId);
+        showExitGuestDialog(id, newId, dialogShower);
     }
 
-    protected void showExitGuestDialog(int id, int targetId) {
+    private void showExitGuestDialog(int id, int targetId, DialogShower dialogShower) {
         if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
             mExitGuestDialog.cancel();
         }
         mExitGuestDialog = new ExitGuestDialog(mContext, id, targetId);
-        mExitGuestDialog.show();
+        if (dialogShower != null) {
+            dialogShower.showDialog(mExitGuestDialog);
+        } else {
+            mExitGuestDialog.show();
+        }
     }
 
-    public void showAddUserDialog() {
+    private void showAddUserDialog(DialogShower dialogShower) {
         if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
             mAddUserDialog.cancel();
         }
         mAddUserDialog = new AddUserDialog(mContext);
-        mAddUserDialog.show();
+        if (dialogShower != null) {
+            dialogShower.showDialog(mAddUserDialog);
+        } else {
+            mAddUserDialog.show();
+        }
     }
 
     private void listenForCallState() {
@@ -868,9 +883,17 @@
 
         /**
          * It handles click events on user list items.
+         *
+         * If the user switcher is hosted in a dialog, passing a non-null {@link DialogShower}
+         * will allow animation to and from the parent dialog.
+         *
          */
+        public void onUserListItemClicked(UserRecord record, @Nullable DialogShower dialogShower) {
+            mController.onUserListItemClicked(record, dialogShower);
+        }
+
         public void onUserListItemClicked(UserRecord record) {
-            mController.onUserListItemClicked(record);
+            onUserListItemClicked(record, null);
         }
 
         public String getName(Context context, UserRecord item) {
@@ -1156,7 +1179,7 @@
                 cancel();
             } else {
                 mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE);
-                dismiss();
+                dismissStack();
                 removeGuestUser(mGuestId, mTargetId);
             }
         }
@@ -1187,7 +1210,7 @@
             if (which == BUTTON_NEGATIVE) {
                 cancel();
             } else {
-                dismiss();
+                dismissStack();
                 if (ActivityManager.isUserAMonkey()) {
                     return;
                 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
index d4c3840..9bd33eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
@@ -16,6 +16,7 @@
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
+import org.junit.After
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -28,24 +29,22 @@
     private val dialogLaunchAnimator =
         DialogLaunchAnimator(context, launchAnimator, hostDialogprovider)
 
+    private val attachedViews = mutableSetOf<View>()
+
+    @After
+    fun tearDown() {
+        runOnMainThreadAndWaitForIdleSync {
+            attachedViews.forEach {
+                ViewUtils.detachView(it)
+            }
+        }
+    }
+
     @Test
     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) = runOnMainThreadAndWaitForIdleSync {
-            val touchSurfaceRoot = LinearLayout(context)
-            val touchSurface = View(context)
-            touchSurfaceRoot.addView(touchSurface)
-
-            // We need to attach the root to the window manager otherwise the exit animation will
-            // be skipped
-            ViewUtils.attachView(touchSurfaceRoot)
-
-            val dialog = TestDialog(context)
-            val hostDialog =
-                dialogLaunchAnimator.showFromView(dialog, touchSurface) as TestHostDialog
-            dialog to hostDialog
-        }
+        val (dialog, hostDialog) = createDialogAndHostDialog()
 
         // Only the host dialog is actually showing.
         assertTrue(hostDialog.isShowing)
@@ -100,6 +99,51 @@
         assertTrue(dialog.onStopCalled)
     }
 
+    @Test
+    fun testStackedDialogsDismissesAll() {
+        val (_, hostDialogFirst) = createDialogAndHostDialog()
+        val (dialogSecond, hostDialogSecond) = createDialogAndHostDialogFromDialog(hostDialogFirst)
+
+        runOnMainThreadAndWaitForIdleSync {
+            dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations()
+            dialogSecond.dismissStack()
+        }
+
+        assertTrue(hostDialogSecond.wasDismissed)
+        assertTrue(hostDialogFirst.wasDismissed)
+    }
+
+    private fun createDialogAndHostDialog(): Pair<TestDialog, TestHostDialog> {
+        return runOnMainThreadAndWaitForIdleSync {
+            val touchSurfaceRoot = LinearLayout(context)
+            val touchSurface = View(context)
+            touchSurfaceRoot.addView(touchSurface)
+
+            // We need to attach the root to the window manager otherwise the exit animation will
+            // be skipped
+            ViewUtils.attachView(touchSurfaceRoot)
+            attachedViews.add(touchSurfaceRoot)
+
+            val dialog = TestDialog(context)
+            val hostDialog =
+                    dialogLaunchAnimator.showFromView(dialog, touchSurface) as TestHostDialog
+            dialog to hostDialog
+        }
+    }
+
+    private fun createDialogAndHostDialogFromDialog(
+        hostParent: Dialog
+    ): Pair<TestDialog, TestHostDialog> {
+        return runOnMainThreadAndWaitForIdleSync {
+            val dialog = TestDialog(context)
+            val hostDialog = dialogLaunchAnimator.showFromDialog(
+                    dialog,
+                    hostParent
+            ) as TestHostDialog
+            dialog to hostDialog
+        }
+    }
+
     private fun <T : Any> runOnMainThreadAndWaitForIdleSync(f: () -> T): T {
         lateinit var result: T
         context.mainExecutor.execute {
@@ -198,6 +242,11 @@
             notifyListeners { onShow() }
         }
 
+        fun dismissStack() {
+            notifyListeners { prepareForStackDismiss() }
+            dismiss()
+        }
+
         private fun notifyListeners(notify: DialogListener.() -> Unit) {
             for (listener in HashSet(listeners)) {
                 listener.notify()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
index 7e900c8..ea3a42c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.user
 
+import android.app.Dialog
 import android.content.Intent
 import android.provider.Settings
 import android.testing.AndroidTestingRunner
@@ -27,7 +28,7 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.qs.PseudoGridView
 import com.android.systemui.qs.tiles.UserDetailView
-import com.android.systemui.statusbar.policy.UserSwitcherController
+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
@@ -39,15 +40,13 @@
 import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
-import org.mockito.Mockito.any
+import org.mockito.Mockito.anyBoolean
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.argThat
 import org.mockito.Mockito.inOrder
-import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
-import java.util.function.Consumer
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -71,6 +70,8 @@
     private lateinit var gridView: PseudoGridView
     @Mock
     private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
+    @Mock
+    private lateinit var hostDialog: Dialog
     @Captor
     private lateinit var clickCaptor: ArgumentCaptor<View.OnClickListener>
 
@@ -85,6 +86,8 @@
         `when`(dialog.grid).thenReturn(gridView)
 
         `when`(launchView.context).thenReturn(mContext)
+        `when`(dialogLaunchAnimator.showFromView(any(), any(), anyBoolean()))
+                .thenReturn(hostDialog)
 
         controller = UserSwitchDialogController(
                 { userDetailViewAdapter },
@@ -188,15 +191,15 @@
     }
 
     @Test
-    fun callbackFromDetailView_dismissesDialog() {
-        val captor = argumentCaptor<Consumer<UserSwitcherController.UserRecord>>()
+    fun callbackFromDialogShower_dismissesDialog() {
+        val captor = argumentCaptor<UserSwitchDialogController.DialogShower>()
 
         controller.showDialog(launchView)
-        verify(userDetailViewAdapter).injectCallback(capture(captor))
+        verify(userDetailViewAdapter).injectDialogShower(capture(captor))
 
-        captor.value.accept(mock(UserSwitcherController.UserRecord::class.java))
+        captor.value.dismiss()
 
-        verify(dialog).dismiss()
+        verify(hostDialog).dismiss()
     }
 
     private class IntentMatcher(private val action: String) : ArgumentMatcher<Intent> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
index 69ab9c5..bdd189a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
@@ -44,13 +44,16 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.qs.QSUserSwitcherEvent
+import com.android.systemui.qs.user.UserSwitchDialogController
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.phone.NotificationShadeWindowView
 import com.android.systemui.telephony.TelephonyListenerManager
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.settings.SecureSettings
 import com.android.systemui.util.time.FakeSystemClock
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertFalse
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -85,6 +88,8 @@
     @Mock private lateinit var dumpManager: DumpManager
     @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor
     @Mock private lateinit var latencyTracker: LatencyTracker
+    @Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower
+    @Mock private lateinit var notificationShadeWindowView: NotificationShadeWindowView
     private lateinit var testableLooper: TestableLooper
     private lateinit var uiBgExecutor: FakeExecutor
     private lateinit var uiEventLogger: UiEventLoggerFake
@@ -98,6 +103,8 @@
     private val guestId = 1234
     private val guestInfo = UserInfo(guestId, "Guest", null,
             UserInfo.FLAG_FULL or UserInfo.FLAG_GUEST, UserManager.USER_TYPE_FULL_GUEST)
+    private val secondaryUser =
+            UserInfo(10, "Secondary", null, 0, UserManager.USER_TYPE_FULL_SECONDARY)
 
     @Before
     fun setUp() {
@@ -114,6 +121,7 @@
                 mock(FingerprintManager::class.java))
 
         `when`(userManager.canAddMoreUsers()).thenReturn(true)
+        `when`(notificationShadeWindowView.context).thenReturn(context)
 
         userSwitcherController = UserSwitcherController(
                 context,
@@ -139,6 +147,26 @@
         userSwitcherController.mPauseRefreshUsers = true
 
         picture = UserIcons.convertToBitmap(context.getDrawable(R.drawable.ic_avatar_user))
+        userSwitcherController.init(notificationShadeWindowView)
+    }
+
+    @Test
+    fun testSwitchUser_parentDialogDismissed() {
+        val otherUserRecord = UserSwitcherController.UserRecord(
+                secondaryUser,
+                picture,
+                false /* guest */,
+                false /* current */,
+                false /* isAddUser */,
+                false /* isRestricted */,
+                true /* isSwitchToEnabled */)
+        `when`(userTracker.userId).thenReturn(ownerId)
+        `when`(userTracker.userInfo).thenReturn(ownerInfo)
+
+        userSwitcherController.onUserListItemClicked(otherUserRecord, dialogShower)
+        testableLooper.processAllMessages()
+
+        verify(dialogShower).dismiss()
     }
 
     @Test
@@ -156,7 +184,7 @@
 
         `when`(userManager.createGuest(any(), anyString())).thenReturn(guestInfo)
 
-        userSwitcherController.onUserListItemClicked(emptyGuestUserRecord)
+        userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, null)
         testableLooper.processAllMessages()
         verify(interactionJankMonitor).begin(any())
         verify(latencyTracker).onActionStart(LatencyTracker.ACTION_USER_SWITCH)
@@ -166,6 +194,26 @@
     }
 
     @Test
+    fun testAddGuest_parentDialogDismissed() {
+        val emptyGuestUserRecord = UserSwitcherController.UserRecord(
+                null,
+                null,
+                true /* guest */,
+                false /* current */,
+                false /* isAddUser */,
+                false /* isRestricted */,
+                true /* isSwitchToEnabled */)
+        `when`(userTracker.userId).thenReturn(ownerId)
+        `when`(userTracker.userInfo).thenReturn(ownerInfo)
+
+        `when`(userManager.createGuest(any(), anyString())).thenReturn(guestInfo)
+
+        userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, dialogShower)
+        testableLooper.processAllMessages()
+        verify(dialogShower).dismiss()
+    }
+
+    @Test
     fun testRemoveGuest_removeButtonPressed_isLogged() {
         val currentGuestUserRecord = UserSwitcherController.UserRecord(
                 guestInfo,
@@ -178,7 +226,7 @@
         `when`(userTracker.userId).thenReturn(guestInfo.id)
         `when`(userTracker.userInfo).thenReturn(guestInfo)
 
-        userSwitcherController.onUserListItemClicked(currentGuestUserRecord)
+        userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
         assertNotNull(userSwitcherController.mExitGuestDialog)
         userSwitcherController.mExitGuestDialog
                 .getButton(DialogInterface.BUTTON_POSITIVE).performClick()
@@ -188,6 +236,46 @@
     }
 
     @Test
+    fun testRemoveGuest_removeButtonPressed_dialogDismissed() {
+        val currentGuestUserRecord = UserSwitcherController.UserRecord(
+                guestInfo,
+                picture,
+                true /* guest */,
+                true /* current */,
+                false /* isAddUser */,
+                false /* isRestricted */,
+                true /* isSwitchToEnabled */)
+        `when`(userTracker.userId).thenReturn(guestInfo.id)
+        `when`(userTracker.userInfo).thenReturn(guestInfo)
+
+        userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
+        assertNotNull(userSwitcherController.mExitGuestDialog)
+        userSwitcherController.mExitGuestDialog
+                .getButton(DialogInterface.BUTTON_POSITIVE).performClick()
+        testableLooper.processAllMessages()
+        assertFalse(userSwitcherController.mExitGuestDialog.isShowing)
+    }
+
+    @Test
+    fun testRemoveGuest_dialogShowerUsed() {
+        val currentGuestUserRecord = UserSwitcherController.UserRecord(
+                guestInfo,
+                picture,
+                true /* guest */,
+                true /* current */,
+                false /* isAddUser */,
+                false /* isRestricted */,
+                true /* isSwitchToEnabled */)
+        `when`(userTracker.userId).thenReturn(guestInfo.id)
+        `when`(userTracker.userInfo).thenReturn(guestInfo)
+
+        userSwitcherController.onUserListItemClicked(currentGuestUserRecord, dialogShower)
+        assertNotNull(userSwitcherController.mExitGuestDialog)
+        testableLooper.processAllMessages()
+        verify(dialogShower).showDialog(userSwitcherController.mExitGuestDialog)
+    }
+
+    @Test
     fun testRemoveGuest_cancelButtonPressed_isNotLogged() {
         val currentGuestUserRecord = UserSwitcherController.UserRecord(
                 guestInfo,
@@ -200,7 +288,7 @@
         `when`(userTracker.userId).thenReturn(guestId)
         `when`(userTracker.userInfo).thenReturn(guestInfo)
 
-        userSwitcherController.onUserListItemClicked(currentGuestUserRecord)
+        userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
         assertNotNull(userSwitcherController.mExitGuestDialog)
         userSwitcherController.mExitGuestDialog
                 .getButton(DialogInterface.BUTTON_NEGATIVE).performClick()
@@ -226,7 +314,7 @@
                 eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt()))
                 .thenReturn(1)
 
-        userSwitcherController.onUserListItemClicked(currentGuestUserRecord)
+        userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
 
         // Simulate a user switch event
         val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId)
@@ -260,7 +348,7 @@
                 eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt()))
                 .thenReturn(1)
 
-        userSwitcherController.onUserListItemClicked(currentGuestUserRecord)
+        userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
 
         // Simulate a user switch event
         val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId)