From 327d67cb2a2604b85e1b3e0cb72f957471db88ba Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Fri, 12 Nov 2021 10:47:05 -0500 Subject: [PATCH] [DO NOT MERGE] Animate dialog stack and use in UserSwitcher Allows to dismiss a dialog stack (dialogs launched from dialog content) by dismissing the top one to the original touchSurface. After this, all the intermediate dialogs are dismissed. Use this in the user switcher dialog. When adding users, the dialog that opens to set the name and picture is not part of this stack (it's a separate activity), so the dialog stack is collapsed before launching the activity. Test: atest SystemUITests Bug: 185360864 Change-Id: Ife8e0f9189783b69034ce660d1075c3f76fbbf06 --- .../SystemUI/animation/res/values/ids.xml | 1 + .../animation/DialogLaunchAnimator.kt | 103 +++++++++++++----- .../systemui/qs/tiles/UserDetailView.java | 26 +++-- .../qs/user/UserSwitchDialogController.kt | 24 +++- .../statusbar/phone/SystemUIDialog.java | 13 +++ .../policy/UserSwitcherController.java | 51 ++++++--- .../animation/DialogLaunchAnimatorTest.kt | 77 ++++++++++--- .../qs/user/UserSwitchDialogControllerTest.kt | 21 ++-- .../policy/UserSwitcherControllerTest.kt | 98 ++++++++++++++++- 9 files changed, 332 insertions(+), 82 deletions(-) diff --git a/packages/SystemUI/animation/res/values/ids.xml b/packages/SystemUI/animation/res/values/ids.xml index ef60a248f79ad..c4cb89fecccb8 100644 --- a/packages/SystemUI/animation/res/values/ids.xml +++ b/packages/SystemUI/animation/res/values/ids.xml @@ -16,4 +16,5 @@ --> + \ 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 413612ff9a766..9aad2783ba2d8 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 android.widget.FrameLayout import kotlin.math.roundToInt private const val TAG = "DialogLaunchAnimator" +private val DIALOG_CONTENT_PARENT_ID = R.id.dialog_content_parent /** * A class that allows dialogs to be started in a seamless way from a view that is transforming @@ -86,10 +87,10 @@ class DialogLaunchAnimator( // 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 @@ class DialogLaunchAnimator( 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 @@ class DialogLaunchAnimator( // 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,20 +145,43 @@ class DialogLaunchAnimator( 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(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 @@ interface DialogListener { /** 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 @@ private class AnimatedDialog( 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 class AnimatedDialog( 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 @@ private class AnimatedDialog( * 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 @@ private class AnimatedDialog( // 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 @@ private class AnimatedDialog( 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 @@ private class AnimatedDialog( }, 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 @@ private class AnimatedDialog( 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 821bd5117d185..be9aa0e988767 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.LayoutInflater; 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.R; 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 @@ public class UserDetailView extends PseudoGridView { private View mCurrentUserView; private final UiEventLogger mUiEventLogger; private final FalsingManager mFalsingManager; - private Consumer mClickCallback; + private @Nullable UserSwitchDialogController.DialogShower mDialogShower; @Inject public Adapter(Context context, UserSwitcherController controller, @@ -95,8 +97,17 @@ public class UserDetailView extends PseudoGridView { return createUserDetailItemView(convertView, parent, item); } - public void injectCallback(Consumer 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 @@ public class UserDetailView extends PseudoGridView { } 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 bae7996517c5d..d74a50e24ed3f 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 @@ class UserSwitchDialogController @VisibleForTesting constructor( 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 cf4aaba107cf6..1130ec24108a5 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 @@ public class SystemUIDialog extends AlertDialog implements ListenableDialog, } } + /** + * 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 b630689567cef..fd387ae0a82ec 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.ViewGroup; 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.FalsingManager; 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 @@ public class UserSwitcherController implements Dumpable { } @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 @@ public class UserSwitcherController implements Dumpable { 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 @@ public class UserSwitcherController implements Dumpable { int currUserId = mUserTracker.getUserId(); if (currUserId == id) { if (record.isGuest) { - showExitGuestDialog(id); + showExitGuestDialog(id, dialogShower); } return; } @@ -490,11 +493,15 @@ public class UserSwitcherController implements Dumpable { // 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 @@ public class UserSwitcherController implements Dumpable { } } - 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 @@ public class UserSwitcherController implements Dumpable { 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 @@ public class UserSwitcherController implements Dumpable { /** * 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 @@ public class UserSwitcherController implements Dumpable { cancel(); } else { mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE); - dismiss(); + dismissStack(); removeGuestUser(mGuestId, mTargetId); } } @@ -1187,7 +1210,7 @@ public class UserSwitcherController implements Dumpable { 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 d4c3840356d15..9bd33eb8db6bb 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 com.android.systemui.animation.DialogListener.DismissReason 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 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { private val dialogLaunchAnimator = DialogLaunchAnimator(context, launchAnimator, hostDialogprovider) + private val attachedViews = mutableSetOf() + + @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 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { 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 { + 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 { + return runOnMainThreadAndWaitForIdleSync { + val dialog = TestDialog(context) + val hostDialog = dialogLaunchAnimator.showFromDialog( + dialog, + hostParent + ) as TestHostDialog + dialog to hostDialog + } + } + private fun runOnMainThreadAndWaitForIdleSync(f: () -> T): T { lateinit var result: T context.mainExecutor.execute { @@ -198,6 +242,11 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { 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 7e900c843cc31..ea3a42ce501ca 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.ActivityStarter 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.ArgumentMatcher 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 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { private lateinit var gridView: PseudoGridView @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator + @Mock + private lateinit var hostDialog: Dialog @Captor private lateinit var clickCaptor: ArgumentCaptor @@ -85,6 +86,8 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { `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 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { } @Test - fun callbackFromDetailView_dismissesDialog() { - val captor = argumentCaptor>() + fun callbackFromDialogShower_dismissesDialog() { + val captor = argumentCaptor() 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 { 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 69ab9c51db812..bdd189a9215be 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.dump.DumpManager 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 @@ class UserSwitcherControllerTest : SysuiTestCase() { @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 @@ class UserSwitcherControllerTest : SysuiTestCase() { 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 @@ class UserSwitcherControllerTest : SysuiTestCase() { mock(FingerprintManager::class.java)) `when`(userManager.canAddMoreUsers()).thenReturn(true) + `when`(notificationShadeWindowView.context).thenReturn(context) userSwitcherController = UserSwitcherController( context, @@ -139,6 +147,26 @@ class UserSwitcherControllerTest : SysuiTestCase() { 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 @@ class UserSwitcherControllerTest : SysuiTestCase() { `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) @@ -165,6 +193,26 @@ class UserSwitcherControllerTest : SysuiTestCase() { assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_ADD.id, uiEventLogger.eventId(0)) } + @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( @@ -178,7 +226,7 @@ class UserSwitcherControllerTest : SysuiTestCase() { `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() @@ -187,6 +235,46 @@ class UserSwitcherControllerTest : SysuiTestCase() { assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE.id, uiEventLogger.eventId(0)) } + @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( @@ -200,7 +288,7 @@ class UserSwitcherControllerTest : SysuiTestCase() { `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 @@ class UserSwitcherControllerTest : SysuiTestCase() { 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 @@ class UserSwitcherControllerTest : SysuiTestCase() { 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)