diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt new file mode 100644 index 0000000000000..1b4746a99f8fe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import android.os.UserHandle +import android.os.UserManager +import com.android.systemui.user.data.repository.UserRepository + +/** Utilities related to user management actions. */ +object UserActionsUtil { + + /** Returns `true` if it's possible to add a guest user to the device; `false` otherwise. */ + fun canCreateGuest( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + if (!isUserSwitcherEnabled) { + return false + } + + return currentUserCanCreateUsers(manager, repository) || + anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled) + } + + /** Returns `true` if it's possible to add a user to the device; `false` otherwise. */ + fun canCreateUser( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + if (!isUserSwitcherEnabled) { + return false + } + + if ( + !currentUserCanCreateUsers(manager, repository) && + !anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled) + ) { + return false + } + + return manager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY) + } + + /** + * Returns `true` if it's possible to add a supervised user to the device; `false` otherwise. + */ + fun canCreateSupervisedUser( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + supervisedUserPackageName: String? + ): Boolean { + if (supervisedUserPackageName.isNullOrEmpty()) { + return false + } + + return canCreateUser( + manager, + repository, + isUserSwitcherEnabled, + isAddUsersFromLockScreenEnabled + ) + } + + /** + * Returns `true` if the current user is allowed to add users to the device; `false` otherwise. + */ + private fun currentUserCanCreateUsers( + manager: UserManager, + repository: UserRepository, + ): Boolean { + val currentUser = repository.getSelectedUserInfo() + if (!currentUser.isAdmin && currentUser.id != UserHandle.USER_SYSTEM) { + return false + } + + return systemCanCreateUsers(manager) + } + + /** Returns `true` if the system can add users to the device; `false` otherwise. */ + private fun systemCanCreateUsers( + manager: UserManager, + ): Boolean { + return !manager.hasBaseUserRestriction(UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM) + } + + /** Returns `true` if it's allowed to add users to the device at all; `false` otherwise. */ + private fun anyoneCanCreateUsers( + manager: UserManager, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + return systemCanCreateUsers(manager) && isAddUsersFromLockScreenEnabled + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index 3c5b9697c0137..b923850e4cbfe 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -17,94 +17,573 @@ package com.android.systemui.user.domain.interactor +import android.annotation.SuppressLint +import android.annotation.UserIdInt +import android.app.ActivityManager +import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.content.pm.UserInfo +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.RemoteException +import android.os.UserHandle +import android.os.UserManager import android.provider.Settings +import android.util.Log +import com.android.internal.util.UserIcons +import com.android.systemui.R +import com.android.systemui.SystemUISecondaryUserService +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.user.domain.model.ShowDialogRequestModel import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.kotlin.pairwise +import java.util.Collections +import java.util.WeakHashMap import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext /** Encapsulates business logic to interact with user data and systems. */ @SysUISingleton class UserInteractor @Inject constructor( - repository: UserRepository, + @Application private val applicationContext: Context, + private val repository: UserRepository, private val controller: UserSwitcherController, private val activityStarter: ActivityStarter, - keyguardInteractor: KeyguardInteractor, + private val keyguardInteractor: KeyguardInteractor, + private val featureFlags: FeatureFlags, + private val manager: UserManager, + @Application private val applicationScope: CoroutineScope, + telephonyInteractor: TelephonyInteractor, + broadcastDispatcher: BroadcastDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val activityManager: ActivityManager, + private val refreshUsersScheduler: RefreshUsersScheduler, + private val guestUserInteractor: GuestUserInteractor, ) { + /** + * Defines interface for classes that can be notified when the state of users on the device is + * changed. + */ + fun interface UserCallback { + /** Notifies that the state of users on the device has changed. */ + fun onUserStateChanged() + } + + private val isNewImpl: Boolean + get() = featureFlags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER) + + private val supervisedUserPackageName: String? + get() = + applicationContext.getString( + com.android.internal.R.string.config_supervisedUserCreationPackage + ) + + private val callbacks = Collections.newSetFromMap(WeakHashMap()) + /** List of current on-device users to select from. */ - val users: Flow> = repository.users + val users: Flow> + get() = + if (isNewImpl) { + combine( + repository.userInfos, + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { userInfos, selectedUserInfo, settings -> + toUserModels( + userInfos = userInfos, + selectedUserId = selectedUserInfo.id, + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, + ) + } + } else { + repository.users + } /** The currently-selected user. */ - val selectedUser: Flow = repository.selectedUser + val selectedUser: Flow + get() = + if (isNewImpl) { + combine( + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { selectedUserInfo, settings -> + val selectedUserId = selectedUserInfo.id + checkNotNull( + toUserModel( + userInfo = selectedUserInfo, + selectedUserId = selectedUserId, + canSwitchUsers = canSwitchUsers(selectedUserId), + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, + ) + ) + } + } else { + repository.selectedUser + } /** List of user-switcher related actions that are available. */ - val actions: Flow> = - combine( - repository.isActionableWhenLocked, - keyguardInteractor.isKeyguardShowing, - ) { isActionableWhenLocked, isLocked -> - isActionableWhenLocked || !isLocked - } - .flatMapLatest { isActionable -> - if (isActionable) { - repository.actions.map { actions -> - actions + - if (actions.isNotEmpty()) { - // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because - // that's a user - // switcher specific action that is not known to the our data source - // or other - // features. - listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - } else { - // If no actions, don't add the navigate action. - emptyList() - } + val actions: Flow> + get() = + if (isNewImpl) { + combine( + repository.userInfos, + repository.userSwitcherSettings, + keyguardInteractor.isKeyguardShowing, + ) { userInfos, settings, isDeviceLocked -> + buildList { + val hasGuestUser = userInfos.any { it.isGuest } + if ( + !hasGuestUser && + (guestUserInteractor.isGuestUserAutoCreated || + UserActionsUtil.canCreateGuest( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + )) + ) { + add(UserActionModel.ENTER_GUEST_MODE) + } + + if (isDeviceLocked && !settings.isAddUsersFromLockscreen) { + // The device is locked and our setting to allow actions that add users + // from the lock-screen is not enabled. The guest action from above is + // always allowed, even when the device is locked, but the various "add + // user" actions below are not. We can finish building the list here. + return@buildList + } + + if ( + UserActionsUtil.canCreateUser( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + ) + ) { + add(UserActionModel.ADD_USER) + } + + if ( + UserActionsUtil.canCreateSupervisedUser( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + supervisedUserPackageName, + ) + ) { + add(UserActionModel.ADD_SUPERVISED_USER) + } } - } else { - // If not actionable it means that we're not allowed to show actions when locked - // and we - // are locked. Therefore, we should show no actions. - flowOf(emptyList()) } + } else { + combine( + repository.isActionableWhenLocked, + keyguardInteractor.isKeyguardShowing, + ) { isActionableWhenLocked, isLocked -> + isActionableWhenLocked || !isLocked + } + .flatMapLatest { isActionable -> + if (isActionable) { + repository.actions.map { actions -> + actions + + if (actions.isNotEmpty()) { + // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT + // because that's a user switcher specific action that is + // not known to the our data source or other features. + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + } else { + // If no actions, don't add the navigate action. + emptyList() + } + } + } else { + // If not actionable it means that we're not allowed to show actions + // when + // locked and we are locked. Therefore, we should show no actions. + flowOf(emptyList()) + } + } } /** Whether the device is configured to always have a guest user available. */ - val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated + val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated /** Whether the guest user is currently being reset. */ - val isGuestUserResetting: Boolean = repository.isGuestUserResetting + val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting + + private val _dialogShowRequests = MutableStateFlow(null) + val dialogShowRequests: Flow = _dialogShowRequests.asStateFlow() + + private val _dialogDismissRequests = MutableStateFlow(null) + val dialogDismissRequests: Flow = _dialogDismissRequests.asStateFlow() + + val isSimpleUserSwitcher: Boolean + get() = + if (isNewImpl) { + repository.isSimpleUserSwitcher() + } else { + error("Not supported in the old implementation!") + } + + fun addCallback(callback: UserCallback) { + callbacks.add(callback) + } + + fun removeCallback(callback: UserCallback) { + callbacks.remove(callback) + } + + fun onDialogShown() { + _dialogShowRequests.value = null + } + + fun onDialogDismissed() { + _dialogDismissRequests.value = null + } + + private fun showDialog(request: ShowDialogRequestModel) { + _dialogShowRequests.value = request + } + + private fun dismissDialog() { + _dialogDismissRequests.value = Unit + } + + init { + if (isNewImpl) { + refreshUsersScheduler.refreshIfNotPaused() + telephonyInteractor.callState + .distinctUntilChanged() + .onEach { refreshUsersScheduler.refreshIfNotPaused() } + .launchIn(applicationScope) + + combine( + broadcastDispatcher.broadcastFlow( + filter = + IntentFilter().apply { + addAction(Intent.ACTION_USER_ADDED) + addAction(Intent.ACTION_USER_REMOVED) + addAction(Intent.ACTION_USER_INFO_CHANGED) + addAction(Intent.ACTION_USER_SWITCHED) + addAction(Intent.ACTION_USER_STOPPED) + addAction(Intent.ACTION_USER_UNLOCKED) + }, + user = UserHandle.SYSTEM, + map = { intent, _ -> intent }, + ), + repository.selectedUserInfo.pairwise(null), + ) { intent, selectedUserChange -> + Pair(intent, selectedUserChange.previousValue) + } + .onEach { (intent, previousSelectedUser) -> + onBroadcastReceived(intent, previousSelectedUser) + } + .launchIn(applicationScope) + } + } + + fun onDeviceBootCompleted() { + guestUserInteractor.onDeviceBootCompleted() + } /** Switches to the user with the given user ID. */ fun selectUser( - userId: Int, + newlySelectedUserId: Int, ) { - controller.onUserSelected(userId, /* dialogShower= */ null) + if (isNewImpl) { + val currentlySelectedUserInfo = repository.getSelectedUserInfo() + if ( + newlySelectedUserId == currentlySelectedUserInfo.id && + currentlySelectedUserInfo.isGuest + ) { + // Here when clicking on the currently-selected guest user to leave guest mode + // and return to the previously-selected non-guest user. + showDialog( + ShowDialogRequestModel.ShowExitGuestDialog( + guestUserId = currentlySelectedUserInfo.id, + targetUserId = repository.lastSelectedNonGuestUserId, + isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + onExitGuestUser = this::exitGuestUser, + ) + ) + return + } + + if (currentlySelectedUserInfo.isGuest) { + // Here when switching from guest to a non-guest user. + showDialog( + ShowDialogRequestModel.ShowExitGuestDialog( + guestUserId = currentlySelectedUserInfo.id, + targetUserId = newlySelectedUserId, + isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + onExitGuestUser = this::exitGuestUser, + ) + ) + return + } + + switchUser(newlySelectedUserId) + } else { + controller.onUserSelected(newlySelectedUserId, /* dialogShower= */ null) + } } /** Executes the given action. */ fun executeAction(action: UserActionModel) { - when (action) { - UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null) - UserActionModel.ADD_USER -> controller.showAddUserDialog(null) - UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity() - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> - activityStarter.startActivity( - Intent(Settings.ACTION_USER_SETTINGS), - /* dismissShade= */ false, - ) + if (isNewImpl) { + when (action) { + UserActionModel.ENTER_GUEST_MODE -> + guestUserInteractor.createAndSwitchTo( + this::showDialog, + this::dismissDialog, + this::selectUser, + ) + UserActionModel.ADD_USER -> { + val currentUser = repository.getSelectedUserInfo() + showDialog( + ShowDialogRequestModel.ShowAddUserDialog( + userHandle = currentUser.userHandle, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral, + ) + ) + } + UserActionModel.ADD_SUPERVISED_USER -> + activityStarter.startActivity( + Intent() + .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) + .setPackage(supervisedUserPackageName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + /* dismissShade= */ false, + ) + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> + activityStarter.startActivity( + Intent(Settings.ACTION_USER_SETTINGS), + /* dismissShade= */ false, + ) + } + } else { + when (action) { + UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null) + UserActionModel.ADD_USER -> controller.showAddUserDialog(null) + UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity() + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> + activityStarter.startActivity( + Intent(Settings.ACTION_USER_SETTINGS), + /* dismissShade= */ false, + ) + } } } + + private fun exitGuestUser( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + forceRemoveGuestOnExit: Boolean, + ) { + guestUserInteractor.exit( + guestUserId = guestUserId, + targetUserId = targetUserId, + forceRemoveGuestOnExit = forceRemoveGuestOnExit, + showDialog = this::showDialog, + dismissDialog = this::dismissDialog, + switchUser = this::switchUser, + ) + } + + private fun switchUser(userId: Int) { + // TODO(b/246631653): track jank and lantecy like in the old impl. + refreshUsersScheduler.pause() + try { + activityManager.switchUser(userId) + } catch (e: RemoteException) { + Log.e(TAG, "Couldn't switch user.", e) + } + } + + private suspend fun onBroadcastReceived( + intent: Intent, + previousUserInfo: UserInfo?, + ) { + val shouldRefreshAllUsers = + when (intent.action) { + Intent.ACTION_USER_SWITCHED -> { + dismissDialog() + val selectedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) + if (previousUserInfo?.id != selectedUserId) { + callbacks.forEach { it.onUserStateChanged() } + restartSecondaryService(selectedUserId) + } + if (guestUserInteractor.isGuestUserAutoCreated) { + guestUserInteractor.guaranteePresent() + } + true + } + Intent.ACTION_USER_INFO_CHANGED -> true + Intent.ACTION_USER_UNLOCKED -> { + // If we unlocked the system user, we should refresh all users. + intent.getIntExtra( + Intent.EXTRA_USER_HANDLE, + UserHandle.USER_NULL, + ) == UserHandle.USER_SYSTEM + } + else -> true + } + + if (shouldRefreshAllUsers) { + refreshUsersScheduler.unpauseAndRefresh() + } + } + + private fun restartSecondaryService(@UserIdInt userId: Int) { + val intent = Intent(applicationContext, SystemUISecondaryUserService::class.java) + // Disconnect from the old secondary user's service + val secondaryUserId = repository.secondaryUserId + if (secondaryUserId != UserHandle.USER_NULL) { + applicationContext.stopServiceAsUser( + intent, + UserHandle.of(secondaryUserId), + ) + repository.secondaryUserId = UserHandle.USER_NULL + } + + // Connect to the new secondary user's service (purely to ensure that a persistent + // SystemUI application is created for that user) + if (userId != UserHandle.USER_SYSTEM) { + applicationContext.startServiceAsUser( + intent, + UserHandle.of(userId), + ) + repository.secondaryUserId = userId + } + } + + private suspend fun toUserModels( + userInfos: List, + selectedUserId: Int, + isUserSwitcherEnabled: Boolean, + ): List { + val canSwitchUsers = canSwitchUsers(selectedUserId) + + return userInfos + // The guest user should go in the last position. + .sortedBy { it.isGuest } + .mapNotNull { userInfo -> + toUserModel( + userInfo = userInfo, + selectedUserId = selectedUserId, + canSwitchUsers = canSwitchUsers, + isUserSwitcherEnabled = isUserSwitcherEnabled, + ) + } + } + + private suspend fun toUserModel( + userInfo: UserInfo, + selectedUserId: Int, + canSwitchUsers: Boolean, + isUserSwitcherEnabled: Boolean, + ): UserModel? { + val userId = userInfo.id + val isSelected = userId == selectedUserId + + return when { + // When the user switcher is not enabled in settings, we only show the primary user. + !isUserSwitcherEnabled && !userInfo.isPrimary -> null + + // We avoid showing disabled users. + !userInfo.isEnabled -> null + userInfo.isGuest -> + UserModel( + id = userId, + name = Text.Loaded(userInfo.name), + image = + getUserImage( + isGuest = true, + userId = userId, + ), + isSelected = isSelected, + isSelectable = canSwitchUsers, + isGuest = true, + ) + userInfo.supportsSwitchToByUser() -> + UserModel( + id = userId, + name = Text.Loaded(userInfo.name), + image = + getUserImage( + isGuest = false, + userId = userId, + ), + isSelected = isSelected, + isSelectable = canSwitchUsers || isSelected, + isGuest = false, + ) + else -> null + } + } + + private suspend fun canSwitchUsers(selectedUserId: Int): Boolean { + return withContext(backgroundDispatcher) { + manager.getUserSwitchability(UserHandle.of(selectedUserId)) + } == UserManager.SWITCHABILITY_STATUS_OK + } + + @SuppressLint("UseCompatLoadingForDrawables") + private suspend fun getUserImage( + isGuest: Boolean, + userId: Int, + ): Drawable { + if (isGuest) { + return checkNotNull(applicationContext.getDrawable(R.drawable.ic_account_circle)) + } + + // TODO(b/246631653): cache the bitmaps to avoid the background work to fetch them. + // TODO(b/246631653): downscale the bitmaps to R.dimen.max_avatar_size if requested. + val userIcon = withContext(backgroundDispatcher) { manager.getUserIcon(userId) } + if (userIcon != null) { + return BitmapDrawable(userIcon) + } + + return UserIcons.getDefaultUserIcon( + applicationContext.resources, + userId, + /* light= */ false + ) + } + + companion object { + private const val TAG = "UserInteractor" + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt new file mode 100644 index 0000000000000..68d710560ebfc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt @@ -0,0 +1,554 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import android.content.Intent +import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.internal.R.drawable.ic_account_circle +import com.android.systemui.R +import com.android.systemui.common.shared.model.Text +import com.android.systemui.user.data.model.UserSwitcherSettingsModel +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(JUnit4::class) +class UserInteractorRefactoredTest : UserInteractorTest() { + + override fun isRefactored(): Boolean { + return true + } + + @Before + override fun setUp() { + super.setUp() + + overrideResource(R.drawable.ic_account_circle, GUEST_ICON) + overrideResource( + com.android.internal.R.string.config_supervisedUserCreationPackage, + SUPERVISED_USER_CREATION_APP_PACKAGE, + ) + whenever(manager.getUserIcon(anyInt())).thenReturn(ICON) + whenever(manager.canAddMoreUsers(any())).thenReturn(true) + } + + @Test + fun `users - switcher enabled`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: List? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + assertUsers(models = value, count = 3, includeGuest = true) + + job.cancel() + } + + @Test + fun `users - switches to second user`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: List? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + userRepository.setSelectedUserInfo(userInfos[1]) + + assertUsers(models = value, count = 2, selectedIndex = 1) + job.cancel() + } + + @Test + fun `users - switcher not enabled`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) + + var value: List? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + assertUsers(models = value, count = 1) + + job.cancel() + } + + @Test + fun selectedUser() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: UserModel? = null + val job = underTest.selectedUser.onEach { value = it }.launchIn(this) + assertUser(value, id = 0, isSelected = true) + + userRepository.setSelectedUserInfo(userInfos[1]) + assertUser(value, id = 1, isSelected = true) + + job.cancel() + } + + @Test + fun `actions - device unlocked`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + + job.cancel() + } + + @Test + fun `actions - device unlocked user not primary - empty list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(emptyList()) + + job.cancel() + } + + @Test + fun `actions - device unlocked user is guest - empty list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + assertThat(userInfos[1].isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(emptyList()) + + job.cancel() + } + + @Test + fun `actions - device locked add from lockscreen set - full list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings( + UserSwitcherSettingsModel( + isUserSwitcherEnabled = true, + isAddUsersFromLockscreen = true, + ) + ) + keyguardRepository.setKeyguardShowing(false) + var value: List? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + + job.cancel() + } + + @Test + fun `actions - device locked - only guest action is shown`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(true) + var value: List? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(listOf(UserActionModel.ENTER_GUEST_MODE)) + + job.cancel() + } + + @Test + fun `executeAction - add user - dialog shown`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.executeAction(UserActionModel.ADD_USER) + assertThat(dialogRequest) + .isEqualTo( + ShowDialogRequestModel.ShowAddUserDialog( + userHandle = userInfos[0].userHandle, + isKeyguardShowing = false, + showEphemeralMessage = false, + ) + ) + + underTest.onDialogShown() + assertThat(dialogRequest).isNull() + + job.cancel() + } + + @Test + fun `executeAction - add supervised user - starts activity`() = + runBlocking(IMMEDIATE) { + underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) + + val intentCaptor = kotlinArgumentCaptor() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(false)) + assertThat(intentCaptor.value.action) + .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER) + assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE) + } + + @Test + fun `executeAction - navigate to manage users`() = + runBlocking(IMMEDIATE) { + underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + + val intentCaptor = kotlinArgumentCaptor() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(false)) + assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS) + } + + @Test + fun `executeAction - guest mode`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) + whenever(manager.createGuest(any())).thenReturn(guestUserInfo) + val dialogRequests = mutableListOf() + val showDialogsJob = + underTest.dialogShowRequests + .onEach { + dialogRequests.add(it) + if (it != null) { + underTest.onDialogShown() + } + } + .launchIn(this) + val dismissDialogsJob = + underTest.dialogDismissRequests + .onEach { + if (it != null) { + underTest.onDialogDismissed() + } + } + .launchIn(this) + + underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) + + assertThat(dialogRequests) + .contains( + ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true), + ) + verify(activityManager).switchUser(guestUserInfo.id) + + showDialogsJob.cancel() + dismissDialogsJob.cancel() + } + + @Test + fun `selectUser - already selected guest re-selected - exit guest dialog`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + val guestUserInfo = userInfos[1] + assertThat(guestUserInfo.isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(guestUserInfo) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = guestUserInfo.id) + + assertThat(dialogRequest) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + job.cancel() + } + + @Test + fun `selectUser - currently guest non-guest selected - exit guest dialog`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + val guestUserInfo = userInfos[1] + assertThat(guestUserInfo.isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(guestUserInfo) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = userInfos[0].id) + + assertThat(dialogRequest) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + job.cancel() + } + + @Test + fun `selectUser - not currently guest - switches users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = userInfos[1].id) + + assertThat(dialogRequest).isNull() + verify(activityManager).switchUser(userInfos[1].id) + job.cancel() + } + + @Test + fun `Telephony call state changes - refreshes users`() = + runBlocking(IMMEDIATE) { + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + telephonyRepository.setCallState(1) + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `User switched broadcast`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val callback1: UserInteractor.UserCallback = mock() + val callback2: UserInteractor.UserCallback = mock() + underTest.addCallback(callback1) + underTest.addCallback(callback2) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + userRepository.setSelectedUserInfo(userInfos[1]) + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_SWITCHED) + .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id), + ) + } + + verify(callback1).onUserStateChanged() + verify(callback2).onUserStateChanged() + assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id) + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `User info changed broadcast`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_INFO_CHANGED), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `System user unlocked broadcast - refresh users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_UNLOCKED) + .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `Non-system user unlocked broadcast - do not refresh users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount) + } + + private fun assertUsers( + models: List?, + count: Int, + selectedIndex: Int = 0, + includeGuest: Boolean = false, + ) { + checkNotNull(models) + assertThat(models.size).isEqualTo(count) + models.forEachIndexed { index, model -> + assertUser( + model = model, + id = index, + isSelected = index == selectedIndex, + isGuest = includeGuest && index == count - 1 + ) + } + } + + private fun assertUser( + model: UserModel?, + id: Int, + isSelected: Boolean = false, + isGuest: Boolean = false, + ) { + checkNotNull(model) + assertThat(model.id).isEqualTo(id) + assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id")) + assertThat(model.isSelected).isEqualTo(isSelected) + assertThat(model.isSelectable).isTrue() + assertThat(model.isGuest).isEqualTo(isGuest) + } + + private fun createUserInfos( + count: Int, + includeGuest: Boolean, + ): List { + return (0 until count).map { index -> + val isGuest = includeGuest && index == count - 1 + createUserInfo( + id = index, + name = + if (isGuest) { + "guest" + } else { + "user_$index" + }, + isPrimary = !isGuest && index == 0, + isGuest = isGuest, + ) + } + } + + private fun createUserInfo( + id: Int, + name: String, + isPrimary: Boolean = false, + isGuest: Boolean = false, + ): UserInfo { + return UserInfo( + id, + name, + /* iconPath= */ "", + /* flags= */ if (isPrimary) { + UserInfo.FLAG_PRIMARY + } else { + 0 + }, + if (isGuest) { + UserManager.USER_TYPE_FULL_GUEST + } else { + UserManager.USER_TYPE_FULL_SYSTEM + }, + ) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private val ICON: Bitmap = mock() + private val GUEST_ICON: Drawable = mock() + private val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index e914e2e0a1dad..cdfe9208e0b82 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -17,51 +17,60 @@ package com.android.systemui.user.domain.interactor -import androidx.test.filters.SmallTest +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager +import android.os.UserManager +import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.data.repository.FakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.nullable -import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 +import kotlinx.coroutines.test.TestCoroutineScope import org.mockito.Mock -import org.mockito.Mockito.anyBoolean -import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -@SmallTest -@RunWith(JUnit4::class) -class UserInteractorTest : SysuiTestCase() { +abstract class UserInteractorTest : SysuiTestCase() { - @Mock private lateinit var controller: UserSwitcherController - @Mock private lateinit var activityStarter: ActivityStarter + @Mock protected lateinit var controller: UserSwitcherController + @Mock protected lateinit var activityStarter: ActivityStarter + @Mock protected lateinit var manager: UserManager + @Mock protected lateinit var activityManager: ActivityManager + @Mock protected lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock protected lateinit var devicePolicyManager: DevicePolicyManager + @Mock protected lateinit var uiEventLogger: UiEventLogger - private lateinit var underTest: UserInteractor + protected lateinit var underTest: UserInteractor - private lateinit var userRepository: FakeUserRepository - private lateinit var keyguardRepository: FakeKeyguardRepository + protected lateinit var userRepository: FakeUserRepository + protected lateinit var keyguardRepository: FakeKeyguardRepository + protected lateinit var telephonyRepository: FakeTelephonyRepository - @Before - fun setUp() { + abstract fun isRefactored(): Boolean + + open fun setUp() { MockitoAnnotations.initMocks(this) userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() + telephonyRepository = FakeTelephonyRepository() + val applicationScope = TestCoroutineScope() + val refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = applicationScope, + mainDispatcher = IMMEDIATE, + repository = userRepository, + ) underTest = UserInteractor( + applicationContext = context, repository = userRepository, controller = controller, activityStarter = activityStarter, @@ -69,142 +78,34 @@ class UserInteractorTest : SysuiTestCase() { KeyguardInteractor( repository = keyguardRepository, ), - ) - } - - @Test - fun `actions - not actionable when locked and locked - no actions`() = - runBlocking(IMMEDIATE) { - userRepository.setActions(UserActionModel.values().toList()) - userRepository.setActionableWhenLocked(false) - keyguardRepository.setKeyguardShowing(true) - - var actions: List? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions).isEmpty() - job.cancel() - } - - @Test - fun `actions - not actionable when locked and not locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) - userRepository.setActionableWhenLocked(false) - keyguardRepository.setKeyguardShowing(false) - - var actions: List? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + featureFlags = + FakeFeatureFlags().apply { + set(Flags.REFACTORED_USER_SWITCHER_CONTROLLER, isRefactored()) + }, + manager = manager, + applicationScope = applicationScope, + telephonyInteractor = + TelephonyInteractor( + repository = telephonyRepository, + ), + broadcastDispatcher = fakeBroadcastDispatcher, + backgroundDispatcher = IMMEDIATE, + activityManager = activityManager, + refreshUsersScheduler = refreshUsersScheduler, + guestUserInteractor = + GuestUserInteractor( + applicationContext = context, + applicationScope = applicationScope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = userRepository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = refreshUsersScheduler, + uiEventLogger = uiEventLogger, ) - ) - job.cancel() - } - - @Test - fun `actions - actionable when locked and not locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) ) - userRepository.setActionableWhenLocked(true) - keyguardRepository.setKeyguardShowing(false) - - var actions: List? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun `actions - actionable when locked and locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) - userRepository.setActionableWhenLocked(true) - keyguardRepository.setKeyguardShowing(true) - - var actions: List? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun selectUser() { - val userId = 3 - - underTest.selectUser(userId) - - verify(controller).onUserSelected(eq(userId), nullable()) - } - - @Test - fun `executeAction - guest`() { - underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) - - verify(controller).createAndSwitchToGuestUser(nullable()) - } - - @Test - fun `executeAction - add user`() { - underTest.executeAction(UserActionModel.ADD_USER) - - verify(controller).showAddUserDialog(nullable()) - } - - @Test - fun `executeAction - add supervised user`() { - underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) - - verify(controller).startSupervisedUserActivity() - } - - @Test - fun `executeAction - manage users`() { - underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - - verify(activityStarter).startActivity(any(), anyBoolean()) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt new file mode 100644 index 0000000000000..c3a9705bf6baa --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.nullable +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(JUnit4::class) +open class UserInteractorUnrefactoredTest : UserInteractorTest() { + + override fun isRefactored(): Boolean { + return false + } + + @Before + override fun setUp() { + super.setUp() + } + + @Test + fun `actions - not actionable when locked and locked - no actions`() = + runBlocking(IMMEDIATE) { + userRepository.setActions(UserActionModel.values().toList()) + userRepository.setActionableWhenLocked(false) + keyguardRepository.setKeyguardShowing(true) + + var actions: List? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions).isEmpty() + job.cancel() + } + + @Test + fun `actions - not actionable when locked and not locked`() = + runBlocking(IMMEDIATE) { + userRepository.setActions( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + userRepository.setActionableWhenLocked(false) + keyguardRepository.setKeyguardShowing(false) + + var actions: List? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun `actions - actionable when locked and not locked`() = + runBlocking(IMMEDIATE) { + userRepository.setActions( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + userRepository.setActionableWhenLocked(true) + keyguardRepository.setKeyguardShowing(false) + + var actions: List? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun `actions - actionable when locked and locked`() = + runBlocking(IMMEDIATE) { + userRepository.setActions( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + userRepository.setActionableWhenLocked(true) + keyguardRepository.setKeyguardShowing(true) + + var actions: List? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun selectUser() { + val userId = 3 + + underTest.selectUser(userId) + + verify(controller).onUserSelected(eq(userId), nullable()) + } + + @Test + fun `executeAction - guest`() { + underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) + + verify(controller).createAndSwitchToGuestUser(nullable()) + } + + @Test + fun `executeAction - add user`() { + underTest.executeAction(UserActionModel.ADD_USER) + + verify(controller).showAddUserDialog(nullable()) + } + + @Test + fun `executeAction - add supervised user`() { + underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) + + verify(controller).startSupervisedUserActivity() + } + + @Test + fun `executeAction - manage users`() { + underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + + verify(activityStarter).startActivity(any(), anyBoolean()) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index ef4500df3600f..3a84064be61db 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -17,17 +17,28 @@ package com.android.systemui.user.ui.viewmodel +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager import android.graphics.drawable.Drawable +import android.os.UserManager import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Text +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.power.data.repository.FakePowerRepository import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.data.repository.FakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.domain.interactor.GuestUserInteractor +import com.android.systemui.user.domain.interactor.RefreshUsersScheduler import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel @@ -38,6 +49,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.yield import org.junit.Before import org.junit.Test @@ -52,6 +64,11 @@ class UserSwitcherViewModelTest : SysuiTestCase() { @Mock private lateinit var controller: UserSwitcherController @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var activityManager: ActivityManager + @Mock private lateinit var manager: UserManager + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var uiEventLogger: UiEventLogger private lateinit var underTest: UserSwitcherViewModel @@ -66,17 +83,51 @@ class UserSwitcherViewModelTest : SysuiTestCase() { userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() powerRepository = FakePowerRepository() + val featureFlags = FakeFeatureFlags() + featureFlags.set(Flags.REFACTORED_USER_SWITCHER_CONTROLLER, false) + val scope = TestCoroutineScope() + val refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = scope, + mainDispatcher = IMMEDIATE, + repository = userRepository, + ) underTest = UserSwitcherViewModel.Factory( userInteractor = UserInteractor( + applicationContext = context, repository = userRepository, controller = controller, activityStarter = activityStarter, keyguardInteractor = KeyguardInteractor( repository = keyguardRepository, - ) + ), + featureFlags = featureFlags, + manager = manager, + applicationScope = scope, + telephonyInteractor = + TelephonyInteractor( + repository = FakeTelephonyRepository(), + ), + broadcastDispatcher = fakeBroadcastDispatcher, + backgroundDispatcher = IMMEDIATE, + activityManager = activityManager, + refreshUsersScheduler = refreshUsersScheduler, + guestUserInteractor = + GuestUserInteractor( + applicationContext = context, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = userRepository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = refreshUsersScheduler, + uiEventLogger = uiEventLogger, + ), ), powerInteractor = PowerInteractor( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt index 53dcc8d269c97..bb646f09b7747 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt @@ -37,10 +37,18 @@ class FakeBroadcastDispatcher( dumpManager: DumpManager, logger: BroadcastDispatcherLogger, userTracker: UserTracker -) : BroadcastDispatcher( - context, looper, executor, dumpManager, logger, userTracker, PendingRemovalStore(logger)) { +) : + BroadcastDispatcher( + context, + looper, + executor, + dumpManager, + logger, + userTracker, + PendingRemovalStore(logger) + ) { - private val registeredReceivers = ArraySet() + val registeredReceivers = ArraySet() override fun registerReceiverWithHandler( receiver: BroadcastReceiver, @@ -78,4 +86,4 @@ class FakeBroadcastDispatcher( } registeredReceivers.clear() } -} \ No newline at end of file +}