Merge "[DO NOT MERGE] Animate dialog stack and use in UserSwitcher" into sc-v2-dev

This commit is contained in:
Fabian Kozynski
2021-11-17 18:09:50 +00:00
committed by Android (Google) Code Review
9 changed files with 332 additions and 82 deletions

View File

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

View File

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

View File

@@ -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<UserSwitcherController.UserRecord> 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<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 @@ public class UserDetailView extends PseudoGridView {
}
view.setActivated(true);
}
onUserListItemClicked(tag);
}
if (mClickCallback != null) {
mClickCallback.accept(tag);
onUserListItemClicked(tag, mDialogShower);
}
}

View File

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

View File

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

View File

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

View File

@@ -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<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 @@ 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<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 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
notifyListeners { onShow() }
}
fun dismissStack() {
notifyListeners { prepareForStackDismiss() }
dismiss()
}
private fun notifyListeners(notify: DialogListener.() -> Unit) {
for (listener in HashSet(listeners)) {
listener.notify()

View File

@@ -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<View.OnClickListener>
@@ -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<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> {

View File

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