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)