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> <resources>
<item type="id" name="launch_animation_running"/> <item type="id" name="launch_animation_running"/>
<item type="id" name="dialog_content_parent" />
</resources> </resources>

View File

@@ -40,6 +40,7 @@ import android.widget.FrameLayout
import kotlin.math.roundToInt import kotlin.math.roundToInt
private const val TAG = "DialogLaunchAnimator" private const val TAG = "DialogLaunchAnimator"
private val DIALOG_CONTENT_PARENT_ID = R.id.dialog_content_parent
/** /**
* A class that allows dialogs to be started in a seamless way from a view that is transforming * A class that allows dialogs to be started in a seamless way from a view that is transforming
@@ -86,10 +87,10 @@ class DialogLaunchAnimator(
// If the parent of the view we are launching from is the background of some other animated // 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 // 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). // this case, we also animate the parent (which is the dialog background).
val dialogContentParent = openedDialogs val animatedParent = openedDialogs
.firstOrNull { it.dialogContentParent == view.parent } .firstOrNull { it.dialogContentParent == view.parent }
?.dialogContentParent val parentHostDialog = animatedParent?.hostDialog
val animateFrom = dialogContentParent ?: view val animateFrom = animatedParent?.dialogContentParent ?: view
// Make sure we don't run the launch animation from the same view twice at the same time. // Make sure we don't run the launch animation from the same view twice at the same time.
if (animateFrom.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) { if (animateFrom.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) {
@@ -100,12 +101,18 @@ class DialogLaunchAnimator(
animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true) animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
val launchAnimation = AnimatedDialog( val animatedDialog = AnimatedDialog(
context, launchAnimator, hostDialogProvider, animateFrom, context,
onDialogDismissed = { openedDialogs.remove(it) }, originalDialog = dialog, launchAnimator,
animateBackgroundBoundsChange) hostDialogProvider,
val hostDialog = launchAnimation.hostDialog animateFrom,
openedDialogs.add(launchAnimation) 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 // If the dialog is dismissed/hidden/shown, then we should actually dismiss/hide/show the
// host dialog. // host dialog.
@@ -119,15 +126,15 @@ class DialogLaunchAnimator(
// If AOD is disabled the screen will directly becomes black and we won't see // If AOD is disabled the screen will directly becomes black and we won't see
// the animation anyways. // the animation anyways.
if (reason == DialogListener.DismissReason.DEVICE_LOCKED) { if (reason == DialogListener.DismissReason.DEVICE_LOCKED) {
launchAnimation.exitAnimationDisabled = true animatedDialog.exitAnimationDisabled = true
} }
hostDialog.dismiss() hostDialog.dismiss()
} }
override fun onHide() { override fun onHide() {
if (launchAnimation.ignoreNextCallToHide) { if (animatedDialog.ignoreNextCallToHide) {
launchAnimation.ignoreNextCallToHide = false animatedDialog.ignoreNextCallToHide = false
return return
} }
@@ -138,20 +145,43 @@ class DialogLaunchAnimator(
hostDialog.show() hostDialog.show()
// We don't actually want to show the original dialog, so hide it. // We don't actually want to show the original dialog, so hide it.
launchAnimation.ignoreNextCallToHide = true animatedDialog.ignoreNextCallToHide = true
dialog.hide() dialog.hide()
} }
override fun onSizeChanged() { override fun onSizeChanged() {
launchAnimation.onOriginalDialogSizeChanged() animatedDialog.onOriginalDialogSizeChanged()
}
override fun prepareForStackDismiss() {
animatedDialog.touchSurface = animatedDialog.prepareForStackDismiss()
} }
}) })
} }
launchAnimation.start() animatedDialog.start()
return hostDialog 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 * Ensure that all dialogs currently shown won't animate into their touch surface when
* dismissed. * dismissed.
@@ -214,6 +244,12 @@ interface DialogListener {
/** Called when this dialog show() is called. */ /** Called when this dialog show() is called. */
fun onShow() 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. */ /** Called when this dialog size might have changed, e.g. because of configuration changes. */
fun onSizeChanged() fun onSizeChanged()
} }
@@ -224,7 +260,7 @@ private class AnimatedDialog(
hostDialogProvider: HostDialogProvider, hostDialogProvider: HostDialogProvider,
/** The view that triggered the dialog after being tapped. */ /** 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 * A callback that will be called with this [AnimatedDialog] after the dialog was
@@ -236,7 +272,10 @@ private class AnimatedDialog(
private val originalDialog: Dialog, private val originalDialog: Dialog,
/** Whether we should animate the dialog background when its bounds change. */ /** Whether we should animate the dialog background when its bounds change. */
private val animateBackgroundBoundsChange: Boolean private val animateBackgroundBoundsChange: Boolean,
/** Launch animation corresponding to the parent [hostDialog]. */
private val parentAnimatedDialog: AnimatedDialog? = null
) { ) {
/** /**
* The fullscreen dialog to which we will add the content view [originalDialogView] of * 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 * the same size as the original dialog window and to which we will set the original dialog
* window background. * 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] * 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 // Make the touch surface invisible and make sure that it stays invisible as long as the
// dialog is shown or animating. // dialog is shown or animating.
touchSurface.visibility = View.INVISIBLE touchSurface.visibility = View.INVISIBLE
if (touchSurface is LaunchableView) { (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
touchSurface.setShouldBlockVisibilityChanges(true)
}
// Add a pre draw listener to (maybe) start the animation once the touch surface is // Add a pre draw listener to (maybe) start the animation once the touch surface is
// actually invisible. // actually invisible.
@@ -576,9 +615,7 @@ private class AnimatedDialog(
Log.i(TAG, "Skipping animation of dialog into the touch surface") Log.i(TAG, "Skipping animation of dialog into the touch surface")
// Make sure we allow the touch surface to change its visibility again. // Make sure we allow the touch surface to change its visibility again.
if (touchSurface is LaunchableView) { (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
touchSurface.setShouldBlockVisibilityChanges(false)
}
// If the view is invisible it's probably because of us, so we make it visible again. // If the view is invisible it's probably because of us, so we make it visible again.
if (touchSurface.visibility == View.INVISIBLE) { if (touchSurface.visibility == View.INVISIBLE) {
@@ -598,9 +635,7 @@ private class AnimatedDialog(
}, },
onLaunchAnimationEnd = { onLaunchAnimationEnd = {
// Make sure we allow the touch surface to change its visibility again. // Make sure we allow the touch surface to change its visibility again.
if (touchSurface is LaunchableView) { (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
touchSurface.setShouldBlockVisibilityChanges(false)
}
touchSurface.visibility = View.VISIBLE touchSurface.visibility = View.VISIBLE
dialogContentParent.visibility = View.INVISIBLE dialogContentParent.visibility = View.INVISIBLE
@@ -796,4 +831,18 @@ private class AnimatedDialog(
animator.start() 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.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 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.plugins.FalsingManager;
import com.android.systemui.qs.PseudoGridView; import com.android.systemui.qs.PseudoGridView;
import com.android.systemui.qs.QSUserSwitcherEvent; 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 com.android.systemui.statusbar.policy.UserSwitcherController;
import java.util.function.Consumer;
import javax.inject.Inject; import javax.inject.Inject;
/** /**
@@ -77,7 +79,7 @@ public class UserDetailView extends PseudoGridView {
private View mCurrentUserView; private View mCurrentUserView;
private final UiEventLogger mUiEventLogger; private final UiEventLogger mUiEventLogger;
private final FalsingManager mFalsingManager; private final FalsingManager mFalsingManager;
private Consumer<UserSwitcherController.UserRecord> mClickCallback; private @Nullable UserSwitchDialogController.DialogShower mDialogShower;
@Inject @Inject
public Adapter(Context context, UserSwitcherController controller, public Adapter(Context context, UserSwitcherController controller,
@@ -95,8 +97,17 @@ public class UserDetailView extends PseudoGridView {
return createUserDetailItemView(convertView, parent, item); 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, public UserDetailItemView createUserDetailItemView(View convertView, ViewGroup parent,
@@ -172,10 +183,7 @@ public class UserDetailView extends PseudoGridView {
} }
view.setActivated(true); view.setActivated(true);
} }
onUserListItemClicked(tag); onUserListItemClicked(tag, mDialogShower);
}
if (mClickCallback != null) {
mClickCallback.accept(tag);
} }
} }

View File

@@ -16,7 +16,9 @@
package com.android.systemui.qs.user package com.android.systemui.qs.user
import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.provider.Settings import android.provider.Settings
import android.view.View import android.view.View
@@ -84,12 +86,26 @@ class UserSwitchDialogController @VisibleForTesting constructor(
doneButton.setOnClickListener { dismiss() } doneButton.setOnClickListener { dismiss() }
val adapter = userDetailViewAdapterProvider.get() val adapter = userDetailViewAdapterProvider.get()
adapter.injectCallback {
dismiss()
}
adapter.linkToViewGroup(grid) 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 @Override
public void hide() { public void hide() {
super.hide(); super.hide();

View File

@@ -55,6 +55,8 @@ import android.view.ViewGroup;
import android.view.WindowManagerGlobal; import android.view.WindowManagerGlobal;
import android.widget.BaseAdapter; import android.widget.BaseAdapter;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEventLogger; 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.plugins.qs.DetailAdapter;
import com.android.systemui.qs.QSUserSwitcherEvent; import com.android.systemui.qs.QSUserSwitcherEvent;
import com.android.systemui.qs.tiles.UserDetailView; 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.settings.UserTracker;
import com.android.systemui.statusbar.phone.NotificationShadeWindowView; import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -460,7 +463,7 @@ public class UserSwitcherController implements Dumpable {
} }
@VisibleForTesting @VisibleForTesting
void onUserListItemClicked(UserRecord record) { void onUserListItemClicked(UserRecord record, DialogShower dialogShower) {
int id; int id;
if (record.isGuest && record.info == null) { if (record.isGuest && record.info == null) {
// No guest user. Create one. // No guest user. Create one.
@@ -472,7 +475,7 @@ public class UserSwitcherController implements Dumpable {
mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD); mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD);
id = guestId; id = guestId;
} else if (record.isAddUser) { } else if (record.isAddUser) {
showAddUserDialog(); showAddUserDialog(dialogShower);
return; return;
} else { } else {
id = record.info.id; id = record.info.id;
@@ -481,7 +484,7 @@ public class UserSwitcherController implements Dumpable {
int currUserId = mUserTracker.getUserId(); int currUserId = mUserTracker.getUserId();
if (currUserId == id) { if (currUserId == id) {
if (record.isGuest) { if (record.isGuest) {
showExitGuestDialog(id); showExitGuestDialog(id, dialogShower);
} }
return; 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 // If switching from guest, we want to bring up the guest exit dialog instead of switching
UserInfo currUserInfo = mUserManager.getUserInfo(currUserId); UserInfo currUserInfo = mUserManager.getUserInfo(currUserId);
if (currUserInfo != null && currUserInfo.isGuest()) { if (currUserInfo != null && currUserInfo.isGuest()) {
showExitGuestDialog(currUserId, record.resolveId()); showExitGuestDialog(currUserId, record.resolveId(), dialogShower);
return; 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); 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; int newId = UserHandle.USER_SYSTEM;
if (mResumeUserOnGuestLogout && mLastNonGuestUser != UserHandle.USER_SYSTEM) { if (mResumeUserOnGuestLogout && mLastNonGuestUser != UserHandle.USER_SYSTEM) {
UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser); UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser);
@@ -519,23 +526,31 @@ public class UserSwitcherController implements Dumpable {
newId = info.id; 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()) { if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
mExitGuestDialog.cancel(); mExitGuestDialog.cancel();
} }
mExitGuestDialog = new ExitGuestDialog(mContext, id, targetId); 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()) { if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
mAddUserDialog.cancel(); mAddUserDialog.cancel();
} }
mAddUserDialog = new AddUserDialog(mContext); mAddUserDialog = new AddUserDialog(mContext);
mAddUserDialog.show(); if (dialogShower != null) {
dialogShower.showDialog(mAddUserDialog);
} else {
mAddUserDialog.show();
}
} }
private void listenForCallState() { private void listenForCallState() {
@@ -868,9 +883,17 @@ public class UserSwitcherController implements Dumpable {
/** /**
* It handles click events on user list items. * 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) { public void onUserListItemClicked(UserRecord record) {
mController.onUserListItemClicked(record); onUserListItemClicked(record, null);
} }
public String getName(Context context, UserRecord item) { public String getName(Context context, UserRecord item) {
@@ -1156,7 +1179,7 @@ public class UserSwitcherController implements Dumpable {
cancel(); cancel();
} else { } else {
mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE); mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE);
dismiss(); dismissStack();
removeGuestUser(mGuestId, mTargetId); removeGuestUser(mGuestId, mTargetId);
} }
} }
@@ -1187,7 +1210,7 @@ public class UserSwitcherController implements Dumpable {
if (which == BUTTON_NEGATIVE) { if (which == BUTTON_NEGATIVE) {
cancel(); cancel();
} else { } else {
dismiss(); dismissStack();
if (ActivityManager.isUserAMonkey()) { if (ActivityManager.isUserAMonkey()) {
return; return;
} }

View File

@@ -16,6 +16,7 @@ import com.android.systemui.animation.DialogListener.DismissReason
import junit.framework.Assert.assertEquals import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue import junit.framework.Assert.assertTrue
import org.junit.After
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -28,24 +29,22 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
private val dialogLaunchAnimator = private val dialogLaunchAnimator =
DialogLaunchAnimator(context, launchAnimator, hostDialogprovider) DialogLaunchAnimator(context, launchAnimator, hostDialogprovider)
private val attachedViews = mutableSetOf<View>()
@After
fun tearDown() {
runOnMainThreadAndWaitForIdleSync {
attachedViews.forEach {
ViewUtils.detachView(it)
}
}
}
@Test @Test
fun testShowDialogFromView() { fun testShowDialogFromView() {
// Show the dialog. showFromView() must be called on the main thread with a dialog created // Show the dialog. showFromView() must be called on the main thread with a dialog created
// on the main thread too. // on the main thread too.
val (dialog, hostDialog) = runOnMainThreadAndWaitForIdleSync { val (dialog, hostDialog) = createDialogAndHostDialog()
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
}
// Only the host dialog is actually showing. // Only the host dialog is actually showing.
assertTrue(hostDialog.isShowing) assertTrue(hostDialog.isShowing)
@@ -100,6 +99,51 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
assertTrue(dialog.onStopCalled) 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 { private fun <T : Any> runOnMainThreadAndWaitForIdleSync(f: () -> T): T {
lateinit var result: T lateinit var result: T
context.mainExecutor.execute { context.mainExecutor.execute {
@@ -198,6 +242,11 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
notifyListeners { onShow() } notifyListeners { onShow() }
} }
fun dismissStack() {
notifyListeners { prepareForStackDismiss() }
dismiss()
}
private fun notifyListeners(notify: DialogListener.() -> Unit) { private fun notifyListeners(notify: DialogListener.() -> Unit) {
for (listener in HashSet(listeners)) { for (listener in HashSet(listeners)) {
listener.notify() listener.notify()

View File

@@ -16,6 +16,7 @@
package com.android.systemui.qs.user package com.android.systemui.qs.user
import android.app.Dialog
import android.content.Intent import android.content.Intent
import android.provider.Settings import android.provider.Settings
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
@@ -27,7 +28,7 @@ import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.PseudoGridView import com.android.systemui.qs.PseudoGridView
import com.android.systemui.qs.tiles.UserDetailView import com.android.systemui.qs.tiles.UserDetailView
import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.eq
@@ -39,15 +40,13 @@ import org.mockito.ArgumentMatcher
import org.mockito.Captor import org.mockito.Captor
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import org.mockito.Mockito.any import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyInt
import org.mockito.Mockito.argThat import org.mockito.Mockito.argThat
import org.mockito.Mockito.inOrder import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import java.util.function.Consumer
@SmallTest @SmallTest
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
@@ -71,6 +70,8 @@ class UserSwitchDialogControllerTest : SysuiTestCase() {
private lateinit var gridView: PseudoGridView private lateinit var gridView: PseudoGridView
@Mock @Mock
private lateinit var dialogLaunchAnimator: DialogLaunchAnimator private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
@Mock
private lateinit var hostDialog: Dialog
@Captor @Captor
private lateinit var clickCaptor: ArgumentCaptor<View.OnClickListener> private lateinit var clickCaptor: ArgumentCaptor<View.OnClickListener>
@@ -85,6 +86,8 @@ class UserSwitchDialogControllerTest : SysuiTestCase() {
`when`(dialog.grid).thenReturn(gridView) `when`(dialog.grid).thenReturn(gridView)
`when`(launchView.context).thenReturn(mContext) `when`(launchView.context).thenReturn(mContext)
`when`(dialogLaunchAnimator.showFromView(any(), any(), anyBoolean()))
.thenReturn(hostDialog)
controller = UserSwitchDialogController( controller = UserSwitchDialogController(
{ userDetailViewAdapter }, { userDetailViewAdapter },
@@ -188,15 +191,15 @@ class UserSwitchDialogControllerTest : SysuiTestCase() {
} }
@Test @Test
fun callbackFromDetailView_dismissesDialog() { fun callbackFromDialogShower_dismissesDialog() {
val captor = argumentCaptor<Consumer<UserSwitcherController.UserRecord>>() val captor = argumentCaptor<UserSwitchDialogController.DialogShower>()
controller.showDialog(launchView) 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> { 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.ActivityStarter
import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.QSUserSwitcherEvent import com.android.systemui.qs.QSUserSwitcherEvent
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.phone.NotificationShadeWindowView
import com.android.systemui.telephony.TelephonyListenerManager import com.android.systemui.telephony.TelephonyListenerManager
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertFalse
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -85,6 +88,8 @@ class UserSwitcherControllerTest : SysuiTestCase() {
@Mock private lateinit var dumpManager: DumpManager @Mock private lateinit var dumpManager: DumpManager
@Mock private lateinit var interactionJankMonitor: InteractionJankMonitor @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor
@Mock private lateinit var latencyTracker: LatencyTracker @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 testableLooper: TestableLooper
private lateinit var uiBgExecutor: FakeExecutor private lateinit var uiBgExecutor: FakeExecutor
private lateinit var uiEventLogger: UiEventLoggerFake private lateinit var uiEventLogger: UiEventLoggerFake
@@ -98,6 +103,8 @@ class UserSwitcherControllerTest : SysuiTestCase() {
private val guestId = 1234 private val guestId = 1234
private val guestInfo = UserInfo(guestId, "Guest", null, private val guestInfo = UserInfo(guestId, "Guest", null,
UserInfo.FLAG_FULL or UserInfo.FLAG_GUEST, UserManager.USER_TYPE_FULL_GUEST) 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 @Before
fun setUp() { fun setUp() {
@@ -114,6 +121,7 @@ class UserSwitcherControllerTest : SysuiTestCase() {
mock(FingerprintManager::class.java)) mock(FingerprintManager::class.java))
`when`(userManager.canAddMoreUsers()).thenReturn(true) `when`(userManager.canAddMoreUsers()).thenReturn(true)
`when`(notificationShadeWindowView.context).thenReturn(context)
userSwitcherController = UserSwitcherController( userSwitcherController = UserSwitcherController(
context, context,
@@ -139,6 +147,26 @@ class UserSwitcherControllerTest : SysuiTestCase() {
userSwitcherController.mPauseRefreshUsers = true userSwitcherController.mPauseRefreshUsers = true
picture = UserIcons.convertToBitmap(context.getDrawable(R.drawable.ic_avatar_user)) 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 @Test
@@ -156,7 +184,7 @@ class UserSwitcherControllerTest : SysuiTestCase() {
`when`(userManager.createGuest(any(), anyString())).thenReturn(guestInfo) `when`(userManager.createGuest(any(), anyString())).thenReturn(guestInfo)
userSwitcherController.onUserListItemClicked(emptyGuestUserRecord) userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, null)
testableLooper.processAllMessages() testableLooper.processAllMessages()
verify(interactionJankMonitor).begin(any()) verify(interactionJankMonitor).begin(any())
verify(latencyTracker).onActionStart(LatencyTracker.ACTION_USER_SWITCH) verify(latencyTracker).onActionStart(LatencyTracker.ACTION_USER_SWITCH)
@@ -165,6 +193,26 @@ class UserSwitcherControllerTest : SysuiTestCase() {
assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_ADD.id, uiEventLogger.eventId(0)) 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 @Test
fun testRemoveGuest_removeButtonPressed_isLogged() { fun testRemoveGuest_removeButtonPressed_isLogged() {
val currentGuestUserRecord = UserSwitcherController.UserRecord( val currentGuestUserRecord = UserSwitcherController.UserRecord(
@@ -178,7 +226,7 @@ class UserSwitcherControllerTest : SysuiTestCase() {
`when`(userTracker.userId).thenReturn(guestInfo.id) `when`(userTracker.userId).thenReturn(guestInfo.id)
`when`(userTracker.userInfo).thenReturn(guestInfo) `when`(userTracker.userInfo).thenReturn(guestInfo)
userSwitcherController.onUserListItemClicked(currentGuestUserRecord) userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
assertNotNull(userSwitcherController.mExitGuestDialog) assertNotNull(userSwitcherController.mExitGuestDialog)
userSwitcherController.mExitGuestDialog userSwitcherController.mExitGuestDialog
.getButton(DialogInterface.BUTTON_POSITIVE).performClick() .getButton(DialogInterface.BUTTON_POSITIVE).performClick()
@@ -187,6 +235,46 @@ class UserSwitcherControllerTest : SysuiTestCase() {
assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE.id, uiEventLogger.eventId(0)) 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 @Test
fun testRemoveGuest_cancelButtonPressed_isNotLogged() { fun testRemoveGuest_cancelButtonPressed_isNotLogged() {
val currentGuestUserRecord = UserSwitcherController.UserRecord( val currentGuestUserRecord = UserSwitcherController.UserRecord(
@@ -200,7 +288,7 @@ class UserSwitcherControllerTest : SysuiTestCase() {
`when`(userTracker.userId).thenReturn(guestId) `when`(userTracker.userId).thenReturn(guestId)
`when`(userTracker.userInfo).thenReturn(guestInfo) `when`(userTracker.userInfo).thenReturn(guestInfo)
userSwitcherController.onUserListItemClicked(currentGuestUserRecord) userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
assertNotNull(userSwitcherController.mExitGuestDialog) assertNotNull(userSwitcherController.mExitGuestDialog)
userSwitcherController.mExitGuestDialog userSwitcherController.mExitGuestDialog
.getButton(DialogInterface.BUTTON_NEGATIVE).performClick() .getButton(DialogInterface.BUTTON_NEGATIVE).performClick()
@@ -226,7 +314,7 @@ class UserSwitcherControllerTest : SysuiTestCase() {
eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt())) eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt()))
.thenReturn(1) .thenReturn(1)
userSwitcherController.onUserListItemClicked(currentGuestUserRecord) userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
// Simulate a user switch event // Simulate a user switch event
val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId) 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())) eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt()))
.thenReturn(1) .thenReturn(1)
userSwitcherController.onUserListItemClicked(currentGuestUserRecord) userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
// Simulate a user switch event // Simulate a user switch event
val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId) val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId)