Merge "[DO NOT MERGE] Animate dialog stack and use in UserSwitcher" into sc-v2-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
4ad4975b28
@@ -16,4 +16,5 @@
|
||||
-->
|
||||
<resources>
|
||||
<item type="id" name="launch_animation_running"/>
|
||||
<item type="id" name="dialog_content_parent" />
|
||||
</resources>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user