Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1b75156d authored by Leon Masopust's avatar Leon Masopust
Browse files

Add sign out option to user switcher

Add an action to the user switcher which allows the user to sign out
using the UserLogoutInteractor (either for the secondary user or for
HSUM devices to the system user). This option will only be displayed
if a logout is possible.

Test: atest UserSwitcherInteractorTest UserSwitcherViewModelTest KeyguardSecurityContainerTest StatusBarUserChipViewModelTest
Test: manual with flag enabled locally
Flag: com.android.systemui.user_switcher_add_sign_out_option
Bug: 381478261
Change-Id: Ie12be49353aa5cdc3eeb4db99cd7508bddbfcfbf
parent 45353fa4
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -507,8 +507,8 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase {
                    0 /* flags */);
            users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */,
                    false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */,
                    false /* isAddSupervisedUser */, null /* enforcedAdmin */,
                    false /* isManageUsers */));
                    false /* isAddSupervisedUser */, false /* isSignOut */,
                    null /* enforcedAdmin */, false /* isManageUsers */));
        }
        return users;
    }
+68 −18
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import android.graphics.drawable.Drawable
import android.os.Process
import android.os.UserHandle
import android.os.UserManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -34,6 +36,7 @@ import com.android.internal.logging.UiEventLogger
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.Flags as AConfigFlags
import com.android.systemui.Flags.FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION
import com.android.systemui.GuestResetOrExitSessionReceiver
import com.android.systemui.GuestResumeSessionReceiver
import com.android.systemui.SysuiTestCase
@@ -68,6 +71,7 @@ import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertNotNull
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runCurrent
@@ -101,10 +105,13 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
    @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
    @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
    @Mock private lateinit var userLogoutInteractor: UserLogoutInteractor

    private val kosmos = testKosmos()
    private val logoutEnabledStateFlow = MutableStateFlow<Boolean>(false)
    private val testScope = kosmos.testScope
    private lateinit var spyContext: Context

    private lateinit var userRepository: FakeUserRepository
    private lateinit var keyguardReply: KeyguardInteractorFactory.WithDependencies
    private lateinit var keyguardRepository: FakeKeyguardRepository
@@ -118,6 +125,8 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
        whenever(manager.getUserIcon(anyInt())).thenReturn(ICON)
        whenever(manager.canAddMoreUsers(any())).thenReturn(true)

        whenever(userLogoutInteractor.isLogoutEnabled).thenReturn(logoutEnabledStateFlow)

        overrideResource(com.android.settingslib.R.drawable.ic_account_circle, GUEST_ICON)
        overrideResource(R.dimen.max_avatar_size, 10)
        overrideResource(
@@ -492,6 +501,42 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
        }
    }

    @Test
    @DisableFlags(FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION)
    fun actions_logoutEnabled_flagDisabled_signOutIsNotShown() {
        createUserInteractor()
        testScope.runTest {
            val userInfos = createUserInfos(count = 1, includeGuest = false)
            userRepository.setUserInfos(userInfos)
            userRepository.setSelectedUserInfo(userInfos[0])
            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
            keyguardRepository.setKeyguardShowing(true)
            logoutEnabledStateFlow.value = true

            val value = collectLastValue(underTest.actions)

            assertThat(value()).isEqualTo(emptyList<UserActionModel>())
        }
    }

    @Test
    @EnableFlags(FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION)
    fun actions_logoutEnabled_flagEnabled_signOutIsShown() {
        createUserInteractor()
        testScope.runTest {
            val userInfos = createUserInfos(count = 1, includeGuest = false)
            userRepository.setUserInfos(userInfos)
            userRepository.setSelectedUserInfo(userInfos[0])
            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
            keyguardRepository.setKeyguardShowing(true)
            logoutEnabledStateFlow.value = true

            val value = collectLastValue(underTest.actions)

            assertThat(value()).isEqualTo(listOf(UserActionModel.SIGN_OUT))
        }
    }

    @Test
    fun executeAction_addUser_dismissesDialogAndStartsActivity() {
        createUserInteractor()
@@ -569,13 +614,22 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
            verify(uiEventLogger, times(1))
                .log(MultiUserActionsEvent.CREATE_GUEST_FROM_USER_SWITCHER)
            assertThat(dialogRequests)
                .contains(
                    ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true),
                )
                .contains(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
            verify(activityManager).switchUser(guestUserInfo.id)
        }
    }

    @Test
    fun executeAction_signOut() {
        createUserInteractor()
        testScope.runTest {
            underTest.executeAction(UserActionModel.SIGN_OUT)
            runCurrent()

            verify(userLogoutInteractor).logOut()
        }
    }

    @Test
    fun selectUser_alreadySelectedGuestReSelected_exitGuestDialog() {
        createUserInteractor()
@@ -739,7 +793,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() {

            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
                spyContext,
                Intent(Intent.ACTION_LOCALE_CHANGED)
                Intent(Intent.ACTION_LOCALE_CHANGED),
            )
            runCurrent()

@@ -972,7 +1026,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
                    50,
                    "Work Profile",
                    /* iconPath= */ "",
                    /* flags= */ UserInfo.FLAG_MANAGED_PROFILE
                    /* flags= */ UserInfo.FLAG_MANAGED_PROFILE,
                )
            )
            userRepository.setUserInfos(userInfos)
@@ -1010,7 +1064,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
            userRepository.setSettings(
                UserSwitcherSettingsModel(
                    isUserSwitcherEnabled = true,
                    isAddUsersFromLockscreen = true
                    isAddUsersFromLockscreen = true,
                )
            )

@@ -1034,7 +1088,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
            userRepository.setSettings(
                UserSwitcherSettingsModel(
                    isUserSwitcherEnabled = true,
                    isAddUsersFromLockscreen = true
                    isAddUsersFromLockscreen = true,
                )
            )

@@ -1068,7 +1122,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
            whenever(
                    manager.hasUserRestrictionForUser(
                        UserManager.DISALLOW_ADD_USER,
                        UserHandle.of(id)
                        UserHandle.of(id),
                    )
                )
                .thenReturn(true)
@@ -1170,7 +1224,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
            whenever(
                    manager.hasUserRestrictionForUser(
                        UserManager.DISALLOW_ADD_USER,
                        UserHandle.of(0)
                        UserHandle.of(0),
                    )
                )
                .thenReturn(true)
@@ -1195,7 +1249,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
                model = model,
                id = index,
                isSelected = index == selectedIndex,
                isGuest = includeGuest && index == count - 1
                isGuest = includeGuest && index == count - 1,
            )
        }
    }
@@ -1263,14 +1317,12 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
        assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled)
    }

    private fun assertRecordForAction(
        record: UserRecord,
        type: UserActionModel,
    ) {
    private fun assertRecordForAction(record: UserRecord, type: UserActionModel) {
        assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE)
        assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER)
        assertThat(record.isAddSupervisedUser)
            .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER)
        assertThat(record.isSignOut).isEqualTo(type === UserActionModel.SIGN_OUT)
    }

    private fun createUserInteractor(startAsProcessUser: Boolean = true) {
@@ -1317,13 +1369,11 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
                featureFlags = kosmos.fakeFeatureFlagsClassic,
                userRestrictionChecker = mock(),
                processWrapper = kosmos.processWrapper,
                userLogoutInteractor = userLogoutInteractor,
            )
    }

    private fun createUserInfos(
        count: Int,
        includeGuest: Boolean,
    ): List<UserInfo> {
    private fun createUserInfos(count: Int, includeGuest: Boolean): List<UserInfo> {
        return (0 until count).map { index ->
            val isGuest = includeGuest && index == count - 1
            createUserInfo(
+13 −8
Original line number Diff line number Diff line
@@ -44,9 +44,12 @@ import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.domain.interactor.GuestUserInteractor
import com.android.systemui.user.domain.interactor.HeadlessSystemUserMode
import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
import com.android.systemui.user.domain.interactor.UserLogoutInteractor
import com.android.systemui.user.domain.interactor.UserSwitcherInteractor
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.toList
@@ -78,13 +81,13 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
    @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
    @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
    @Mock private lateinit var userLogoutInteractor: UserLogoutInteractor

    private lateinit var underTest: StatusBarUserChipViewModel

    private val userRepository = FakeUserRepository()
    private lateinit var guestUserInteractor: GuestUserInteractor
    private lateinit var refreshUsersScheduler: RefreshUsersScheduler

    private val testDispatcher = UnconfinedTestDispatcher()
    private val testScope = TestScope(testDispatcher)

@@ -92,6 +95,9 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
    fun setUp() {
        MockitoAnnotations.initMocks(this)

        val logoutEnabledStateFlow = MutableStateFlow<Boolean>(false)
        whenever(userLogoutInteractor.isLogoutEnabled).thenReturn(logoutEnabledStateFlow)

        doAnswer { invocation ->
                val userId = invocation.arguments[0] as Int
                when (userId) {
@@ -251,9 +257,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
                    headlessSystemUserMode = headlessSystemUserMode,
                    applicationScope = testScope.backgroundScope,
                    telephonyInteractor =
                        TelephonyInteractor(
                            repository = FakeTelephonyRepository(),
                        ),
                        TelephonyInteractor(repository = FakeTelephonyRepository()),
                    broadcastDispatcher = fakeBroadcastDispatcher,
                    keyguardUpdateMonitor = keyguardUpdateMonitor,
                    backgroundDispatcher = testDispatcher,
@@ -263,7 +267,8 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
                    guestUserInteractor = guestUserInteractor,
                    uiEventLogger = uiEventLogger,
                    userRestrictionChecker = mock(),
                    processWrapper = ProcessWrapperFake(activityManager)
                    processWrapper = ProcessWrapperFake(activityManager),
                    userLogoutInteractor = userLogoutInteractor,
                )
        )
    }
@@ -293,7 +298,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
                USER_NAME_0.text!!,
                /* iconPath */ "",
                /* flags */ UserInfo.FLAG_FULL,
                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM
                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM,
            )

        private val USER_1 =
@@ -302,7 +307,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
                USER_NAME_1.text!!,
                /* iconPath */ "",
                /* flags */ UserInfo.FLAG_FULL,
                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM
                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM,
            )

        private val USER_2 =
@@ -311,7 +316,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
                USER_NAME_2.text!!,
                /* iconPath */ "",
                /* flags */ UserInfo.FLAG_FULL,
                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM
                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM,
            )
    }
}
+12 −10
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.domain.interactor.GuestUserInteractor
import com.android.systemui.user.domain.interactor.HeadlessSystemUserMode
import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
import com.android.systemui.user.domain.interactor.UserLogoutInteractor
import com.android.systemui.user.domain.interactor.UserSwitcherInteractor
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
@@ -51,6 +52,7 @@ import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -79,6 +81,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
    @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
    @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
    @Mock private lateinit var userLogoutInteractor: UserLogoutInteractor

    private lateinit var underTest: UserSwitcherViewModel

@@ -94,6 +97,10 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
        whenever(manager.canAddMoreUsers(any())).thenReturn(true)
        whenever(manager.getUserSwitchability(any()))
            .thenReturn(UserManager.SWITCHABILITY_STATUS_OK)

        val logoutEnabledStateFlow = MutableStateFlow<Boolean>(false)
        whenever(userLogoutInteractor.isLogoutEnabled).thenReturn(logoutEnabledStateFlow)

        overrideResource(
            com.android.internal.R.string.config_supervisedUserCreationPackage,
            SUPERVISED_USER_CREATION_PACKAGE,
@@ -113,15 +120,11 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
                            UserInfo.FLAG_ADMIN or
                            UserInfo.FLAG_FULL,
                        UserManager.USER_TYPE_FULL_SYSTEM,
                    ),
                    )
                )
            userRepository.setUserInfos(userInfos)
            userRepository.setSelectedUserInfo(userInfos[0])
            userRepository.setSettings(
                UserSwitcherSettingsModel(
                    isUserSwitcherEnabled = true,
                )
            )
            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
        }

        val refreshUsersScheduler =
@@ -163,9 +166,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
                        headlessSystemUserMode = headlessSystemUserMode,
                        applicationScope = testScope.backgroundScope,
                        telephonyInteractor =
                            TelephonyInteractor(
                                repository = FakeTelephonyRepository(),
                            ),
                            TelephonyInteractor(repository = FakeTelephonyRepository()),
                        broadcastDispatcher = fakeBroadcastDispatcher,
                        keyguardUpdateMonitor = keyguardUpdateMonitor,
                        backgroundDispatcher = testDispatcher,
@@ -175,7 +176,8 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
                        guestUserInteractor = guestUserInteractor,
                        uiEventLogger = uiEventLogger,
                        userRestrictionChecker = mock(),
                        processWrapper = ProcessWrapperFake(activityManager)
                        processWrapper = ProcessWrapperFake(activityManager),
                        userLogoutInteractor = userLogoutInteractor,
                    ),
                guestUserInteractor = guestUserInteractor,
            )
+2 −1
Original line number Diff line number Diff line
@@ -37,7 +37,7 @@ protected constructor(
    protected open val users: List<UserRecord>
        get() = controller.users.filter {
            (!controller.isKeyguardShowing || !it.isRestricted) &&
                (controller.isUserSwitcherEnabled || it.isCurrent)
                (controller.isUserSwitcherEnabled || it.isCurrent || it.isSignOut)
        }

    init {
@@ -109,6 +109,7 @@ protected constructor(
                    item.isAddUser,
                    item.isGuest,
                    item.isAddSupervisedUser,
                    item.isSignOut,
                    isTablet,
                    item.isManageUsers,
                )
Loading