From f3dcc77dbee4b793fa83ff8b60f7730313d3323f Mon Sep 17 00:00:00 2001 From: Andreas Miko Date: Tue, 10 Oct 2023 13:32:07 +0200 Subject: Refactor UserInteractor to UserSwitcherInteractor This change aims at splitting up access to selected user data into a smaller interactor such that there are fewer dependencies as there are many classes that depend on just getting the current user info Test: ran all sysui tests Bug: b/303808405 Change-Id: I197fdf33a04419eb819c4b46fcf470080d01aa26 --- .../KeyguardSecurityContainerController.java | 10 +- .../domain/interactor/KeyguardDismissInteractor.kt | 18 +- .../keyguard/ui/binder/KeyguardDismissBinder.kt | 16 +- .../domain/interactor/FooterActionsInteractor.kt | 6 +- .../android/systemui/settings/UserTrackerImpl.kt | 1 + .../shade/domain/interactor/ShadeInteractor.kt | 10 +- .../statusbar/policy/UserSwitcherController.kt | 49 +- .../domain/interactor/SelectedUserInteractor.kt | 17 + .../user/domain/interactor/UserInteractor.kt | 831 -------------- .../domain/interactor/UserSwitcherInteractor.kt | 820 +++++++++++++ .../ui/dialog/UserSwitcherDialogCoordinator.kt | 8 +- .../ui/viewmodel/StatusBarUserChipViewModel.kt | 7 +- .../user/ui/viewmodel/UserSwitcherViewModel.kt | 21 +- .../KeyguardSecurityContainerControllerTest.kt | 9 +- .../KeyguardDismissActionInteractorTest.kt | 2 - .../interactor/KeyguardDismissInteractorTest.kt | 2 - .../NotificationShadeWindowControllerImplTest.java | 4 +- .../shade/QuickSettingsControllerBaseTest.java | 9 +- .../interactor/SelectedUserInteractorTest.kt | 44 + .../user/domain/interactor/UserInteractorTest.kt | 1206 -------------------- .../interactor/UserSwitcherInteractorTest.kt | 1206 ++++++++++++++++++++ .../ui/viewmodel/StatusBarUserChipViewModelTest.kt | 5 +- .../user/ui/viewmodel/UserSwitcherViewModelTest.kt | 6 +- .../com/android/systemui/wmshell/BubblesTest.java | 4 +- .../interactor/KeyguardDismissInteractorFactory.kt | 53 +- .../systemui/qs/footer/FooterActionsTestUtils.kt | 6 +- 26 files changed, 2190 insertions(+), 2180 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt create mode 100644 packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt delete mode 100644 packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 51dafac7b421..15bb2f85307a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -91,7 +91,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; -import com.android.systemui.user.domain.interactor.UserInteractor; +import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.android.systemui.util.ViewController; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.GlobalSettings; @@ -420,7 +420,7 @@ public class KeyguardSecurityContainerController extends ViewController mDeviceEntryInteractor; private final Provider mJavaAdapter; private final DeviceProvisionedController mDeviceProvisionedController; @@ -453,7 +453,7 @@ public class KeyguardSecurityContainerController extends ViewController javaAdapter, - UserInteractor userInteractor, + SelectedUserInteractor selectedUserInteractor, DeviceProvisionedController deviceProvisionedController, FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate, KeyguardTransitionInteractor keyguardTransitionInteractor, @@ -487,7 +487,7 @@ public class KeyguardSecurityContainerController extends ViewController { if (isDeviceEntered) { - final int selectedUserId = mUserInteractor.getSelectedUserId(); + final int selectedUserId = mSelectedUserInteractor.getSelectedUserId(); showNextSecurityScreenOrFinish( /* authenticated= */ true, selectedUserId, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt index cab69285cc94..17e8bb320912 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt @@ -24,15 +24,15 @@ import com.android.systemui.keyguard.data.repository.TrustRepository import com.android.systemui.keyguard.shared.model.DismissAction import com.android.systemui.keyguard.shared.model.KeyguardDone import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.kotlin.Utils.Companion.toQuad import com.android.systemui.util.kotlin.sample -import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import javax.inject.Inject /** Encapsulates business logic for requesting the keyguard to dismiss/finish/done. */ @SysUISingleton @@ -40,11 +40,11 @@ class KeyguardDismissInteractor @Inject constructor( trustRepository: TrustRepository, - val keyguardRepository: KeyguardRepository, - val primaryBouncerInteractor: PrimaryBouncerInteractor, - val alternateBouncerInteractor: AlternateBouncerInteractor, - val powerInteractor: PowerInteractor, - val userInteractor: UserInteractor, + private val keyguardRepository: KeyguardRepository, + primaryBouncerInteractor: PrimaryBouncerInteractor, + alternateBouncerInteractor: AlternateBouncerInteractor, + powerInteractor: PowerInteractor, + private val selectedUserInteractor: SelectedUserInteractor, ) { /* * Updates when a biometric has authenticated the device and is requesting to dismiss @@ -82,7 +82,7 @@ constructor( */ private val primaryAuthenticated: Flow = primaryBouncerInteractor.keyguardAuthenticatedPrimaryAuth - .filter { authedUserId -> authedUserId == userInteractor.getSelectedUserId() } + .filter { authedUserId -> authedUserId == selectedUserInteractor.getSelectedUserId() } .map {} // map to Unit /* @@ -92,7 +92,7 @@ constructor( */ private val userRequestedBouncerWhenAlreadyAuthenticated: Flow = primaryBouncerInteractor.userRequestedBouncerWhenAlreadyAuthenticated - .filter { authedUserId -> authedUserId == userInteractor.getSelectedUserId() } + .filter { authedUserId -> authedUserId == selectedUserInteractor.getSelectedUserId() } .map {} // map to Unit /** Updates when keyguardDone should be requested. */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardDismissBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardDismissBinder.kt index f14552ba0685..5075a0766c2c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardDismissBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardDismissBinder.kt @@ -25,20 +25,18 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardDismissInteractor import com.android.systemui.keyguard.shared.model.KeyguardDone import com.android.systemui.log.core.LogLevel -import com.android.systemui.user.domain.interactor.UserInteractor -import javax.inject.Inject +import com.android.systemui.user.domain.interactor.SelectedUserInteractor import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch +import javax.inject.Inject /** Handles keyguard dismissal requests. */ -@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class KeyguardDismissBinder @Inject constructor( private val interactor: KeyguardDismissInteractor, - private val userInteractor: UserInteractor, + private val selectedUserInteractor: SelectedUserInteractor, private val viewMediatorCallback: ViewMediatorCallback, @Application private val scope: CoroutineScope, private val keyguardLogger: KeyguardLogger, @@ -55,11 +53,15 @@ constructor( when (keyguardDoneTiming) { KeyguardDone.LATER -> { log("keyguardDonePending") - viewMediatorCallback.keyguardDonePending(userInteractor.getSelectedUserId()) + viewMediatorCallback.keyguardDonePending( + selectedUserInteractor.getSelectedUserId() + ) } else -> { log("keyguardDone") - viewMediatorCallback.keyguardDone(userInteractor.getSelectedUserId()) + viewMediatorCallback.keyguardDone( + selectedUserInteractor.getSelectedUserId() + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt index c91ed133a11e..8e307408ba86 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt @@ -42,7 +42,7 @@ import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig import com.android.systemui.security.data.repository.SecurityRepository import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.user.data.repository.UserSwitcherRepository -import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.domain.interactor.UserSwitcherInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -102,7 +102,7 @@ constructor( private val deviceProvisionedController: DeviceProvisionedController, private val qsSecurityFooterUtils: QSSecurityFooterUtils, private val fgsManagerController: FgsManagerController, - private val userInteractor: UserInteractor, + private val userSwitcherInteractor: UserSwitcherInteractor, securityRepository: SecurityRepository, foregroundServicesRepository: ForegroundServicesRepository, userSwitcherRepository: UserSwitcherRepository, @@ -178,6 +178,6 @@ constructor( } override fun showUserSwitcher(expandable: Expandable) { - userInteractor.showUserSwitcher(expandable) + userSwitcherInteractor.showUserSwitcher(expandable) } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index 393a698bcdb7..99127ea928bf 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -90,6 +90,7 @@ open class UserTrackerImpl internal constructor( private val isBackgroundUserSwitchEnabled: Boolean get() = featureFlagsProvider.get().isEnabled(Flags.USER_TRACKER_BACKGROUND_CALLBACKS) + @Deprecated("Use UserInteractor.getSelectedUserId()") override var userId: Int by SynchronizedDelegate(context.userId) protected set diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index e487a6fb9617..205a80a04089 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -34,10 +34,8 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.Share import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository -import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.domain.interactor.UserSwitcherInteractor import com.android.systemui.util.kotlin.pairwise -import javax.inject.Inject -import javax.inject.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.currentCoroutineContext @@ -53,6 +51,8 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.isActive +import javax.inject.Inject +import javax.inject.Provider /** Business logic for shade interactions. */ @OptIn(ExperimentalCoroutinesApi::class) @@ -71,7 +71,7 @@ constructor( keyguardTransitionInteractor: KeyguardTransitionInteractor, powerInteractor: PowerInteractor, userSetupRepository: UserSetupRepository, - userInteractor: UserInteractor, + userSwitcherInteractor: UserSwitcherInteractor, sharedNotificationContainerInteractor: SharedNotificationContainerInteractor, repository: ShadeRepository, ) { @@ -227,7 +227,7 @@ constructor( isDeviceProvisioned && // Disallow QS during setup if it's a simple user switcher. (The user intends to // use the lock screen user switcher, QS is not needed.) - (isUserSetup || !userInteractor.isSimpleUserSwitcher) && + (isUserSetup || !userSwitcherInteractor.isSimpleUserSwitcher) && isShadeEnabled && disableFlags.isQuickSettingsEnabled() && !isDozing diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt index f88339a4d077..7829d6e7760a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt @@ -27,7 +27,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower import com.android.systemui.user.data.source.UserRecord import com.android.systemui.user.domain.interactor.GuestUserInteractor -import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.domain.interactor.UserSwitcherInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import dagger.Lazy import java.io.PrintWriter @@ -41,7 +41,7 @@ class UserSwitcherController @Inject constructor( @Application private val applicationContext: Context, - private val userInteractorLazy: Lazy, + private val userSwitcherInteractorLazy: Lazy, private val guestUserInteractorLazy: Lazy, private val keyguardInteractorLazy: Lazy, private val activityStarter: ActivityStarter, @@ -53,26 +53,29 @@ constructor( fun onUserSwitched() } - private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() } + private val mUserSwitcherInteractor: UserSwitcherInteractor by lazy { + userSwitcherInteractorLazy.get() + } private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() } private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() } - private val callbackCompatMap = mutableMapOf() + private val callbackCompatMap = + mutableMapOf() /** The current list of [UserRecord]. */ val users: ArrayList - get() = userInteractor.userRecords.value + get() = mUserSwitcherInteractor.userRecords.value /** Whether the user switcher experience should use the simple experience. */ val isSimpleUserSwitcher: Boolean - get() = userInteractor.isSimpleUserSwitcher + get() = mUserSwitcherInteractor.isSimpleUserSwitcher val isUserSwitcherEnabled: Boolean - get() = userInteractor.isUserSwitcherEnabled + get() = mUserSwitcherInteractor.isUserSwitcherEnabled /** The [UserRecord] of the current user or `null` when none. */ val currentUserRecord: UserRecord? - get() = userInteractor.selectedUserRecord.value + get() = mUserSwitcherInteractor.selectedUserRecord.value /** The name of the current user of the device or `null`, when none is selected. */ val currentUserName: String? @@ -81,8 +84,8 @@ constructor( LegacyUserUiHelper.getUserRecordName( context = applicationContext, record = it, - isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated, - isGuestUserResetting = userInteractor.isGuestUserResetting, + isGuestUserAutoCreated = mUserSwitcherInteractor.isGuestUserAutoCreated, + isGuestUserResetting = mUserSwitcherInteractor.isGuestUserResetting, ) } @@ -98,21 +101,21 @@ constructor( * @param dialogShower An optional [DialogShower] in case we need to show dialogs. */ fun onUserSelected(userId: Int, dialogShower: DialogShower?) { - userInteractor.selectUser(userId, dialogShower) + mUserSwitcherInteractor.selectUser(userId, dialogShower) } /** Whether the guest user is configured to always be present on the device. */ val isGuestUserAutoCreated: Boolean - get() = userInteractor.isGuestUserAutoCreated + get() = mUserSwitcherInteractor.isGuestUserAutoCreated /** Whether the guest user is currently being reset. */ val isGuestUserResetting: Boolean - get() = userInteractor.isGuestUserResetting + get() = mUserSwitcherInteractor.isGuestUserResetting /** Registers an adapter to notify when the users change. */ fun addAdapter(adapter: WeakReference) { - userInteractor.addCallback( - object : UserInteractor.UserCallback { + mUserSwitcherInteractor.addCallback( + object : UserSwitcherInteractor.UserCallback { override fun isEvictable(): Boolean { return adapter.get() == null } @@ -129,7 +132,7 @@ constructor( record: UserRecord, dialogShower: DialogShower?, ) { - userInteractor.onRecordSelected(record, dialogShower) + mUserSwitcherInteractor.onRecordSelected(record, dialogShower) } /** @@ -152,7 +155,7 @@ constructor( * `UserHandle.USER_NULL`, then switch immediately to the newly created guest user. */ fun removeGuestUser(guestUserId: Int, targetUserId: Int) { - userInteractor.removeGuestUser( + mUserSwitcherInteractor.removeGuestUser( guestUserId = guestUserId, targetUserId = targetUserId, ) @@ -168,7 +171,7 @@ constructor( * only if its ephemeral, else keep guest */ fun exitGuestUser(guestUserId: Int, targetUserId: Int, forceRemoveGuestOnExit: Boolean) { - userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) + mUserSwitcherInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) } /** @@ -194,31 +197,31 @@ constructor( * The pictures are only loaded if they have not been loaded yet. */ fun refreshUsers() { - userInteractor.refreshUsers() + mUserSwitcherInteractor.refreshUsers() } /** Adds a subscriber to when user switches. */ fun addUserSwitchCallback(callback: UserSwitchCallback) { val interactorCallback = - object : UserInteractor.UserCallback { + object : UserSwitcherInteractor.UserCallback { override fun onUserStateChanged() { callback.onUserSwitched() } } callbackCompatMap[callback] = interactorCallback - userInteractor.addCallback(interactorCallback) + mUserSwitcherInteractor.addCallback(interactorCallback) } /** Removes a previously-added subscriber. */ fun removeUserSwitchCallback(callback: UserSwitchCallback) { val interactorCallback = callbackCompatMap.remove(callback) if (interactorCallback != null) { - userInteractor.removeCallback(interactorCallback) + mUserSwitcherInteractor.removeCallback(interactorCallback) } } fun dump(pw: PrintWriter, args: Array) { - userInteractor.dump(pw) + mUserSwitcherInteractor.dump(pw) } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt new file mode 100644 index 000000000000..fedd58bb53a6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt @@ -0,0 +1,17 @@ +package com.android.systemui.user.domain.interactor + +import android.annotation.UserIdInt +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject + +/** Encapsulates business logic to interact the selected user */ +@SysUISingleton +class SelectedUserInteractor @Inject constructor(private val repository: UserRepository) { + + /** Returns the ID of the currently-selected user. */ + @UserIdInt + fun getSelectedUserId(): Int { + return repository.getSelectedUserInfo().id + } +} 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 deleted file mode 100644 index dbc3bf3a75a2..000000000000 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ /dev/null @@ -1,831 +0,0 @@ -/* - * 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.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.graphics.drawable.Icon -import android.os.Process -import android.os.RemoteException -import android.os.UserHandle -import android.os.UserManager -import android.provider.Settings -import android.util.Log -import com.android.internal.logging.UiEventLogger -import com.android.internal.util.UserIcons -import com.android.keyguard.KeyguardUpdateMonitor -import com.android.keyguard.KeyguardUpdateMonitorCallback -import com.android.systemui.SystemUISecondaryUserService -import com.android.systemui.animation.Expandable -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.qs.user.UserSwitchDialogController -import com.android.systemui.res.R -import com.android.systemui.telephony.domain.interactor.TelephonyInteractor -import com.android.systemui.user.CreateUserActivity -import com.android.systemui.user.data.model.UserSwitcherSettingsModel -import com.android.systemui.user.data.repository.UserRepository -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.domain.model.ShowDialogRequestModel -import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel -import com.android.systemui.user.utils.MultiUserActionsEvent -import com.android.systemui.user.utils.MultiUserActionsEventHelper -import com.android.systemui.util.kotlin.pairwise -import com.android.systemui.utils.UserRestrictionChecker -import java.io.PrintWriter -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.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext - -/** Encapsulates business logic to interact with user data and systems. */ -@SysUISingleton -class UserInteractor -@Inject -constructor( - @Application private val applicationContext: Context, - private val repository: UserRepository, - private val activityStarter: ActivityStarter, - private val keyguardInteractor: KeyguardInteractor, - private val featureFlags: FeatureFlags, - private val manager: UserManager, - private val headlessSystemUserMode: HeadlessSystemUserMode, - @Application private val applicationScope: CoroutineScope, - telephonyInteractor: TelephonyInteractor, - broadcastDispatcher: BroadcastDispatcher, - keyguardUpdateMonitor: KeyguardUpdateMonitor, - @Background private val backgroundDispatcher: CoroutineDispatcher, - private val activityManager: ActivityManager, - private val refreshUsersScheduler: RefreshUsersScheduler, - private val guestUserInteractor: GuestUserInteractor, - private val uiEventLogger: UiEventLogger, - private val userRestrictionChecker: UserRestrictionChecker, -) { - /** - * Defines interface for classes that can be notified when the state of users on the device is - * changed. - */ - interface UserCallback { - /** Returns `true` if this callback can be cleaned-up. */ - fun isEvictable(): Boolean = false - - /** Notifies that the state of users on the device has changed. */ - fun onUserStateChanged() - } - - private val supervisedUserPackageName: String? - get() = - applicationContext.getString( - com.android.internal.R.string.config_supervisedUserCreationPackage - ) - - private val callbackMutex = Mutex() - private val callbacks = mutableSetOf() - private val userInfos: Flow> = - repository.userInfos.map { userInfos -> userInfos.filter { it.isFull } } - - /** List of current on-device users to select from. */ - val users: Flow> - get() = - combine( - userInfos, - repository.selectedUserInfo, - repository.userSwitcherSettings, - ) { userInfos, selectedUserInfo, settings -> - toUserModels( - userInfos = userInfos, - selectedUserId = selectedUserInfo.id, - isUserSwitcherEnabled = settings.isUserSwitcherEnabled, - ) - } - - /** The currently-selected user. */ - val selectedUser: Flow - get() = - repository.selectedUserInfo.map { selectedUserInfo -> - val selectedUserId = selectedUserInfo.id - toUserModel( - userInfo = selectedUserInfo, - selectedUserId = selectedUserId, - canSwitchUsers = canSwitchUsers(selectedUserId) - ) - } - - /** List of user-switcher related actions that are available. */ - val actions: Flow> - get() = - combine( - repository.selectedUserInfo, - userInfos, - repository.userSwitcherSettings, - keyguardInteractor.isKeyguardShowing, - ) { _, userInfos, settings, isDeviceLocked -> - buildList { - if (!isDeviceLocked || settings.isAddUsersFromLockscreen) { - // The device is locked and our setting to allow actions that add users - // from the lock-screen is not enabled. We can finish building the list - // here. - val isFullScreen = featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER) - - val actionList: List = - if (isFullScreen) { - listOf( - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.ENTER_GUEST_MODE, - ) - } else { - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - } - actionList.map { - when (it) { - UserActionModel.ENTER_GUEST_MODE -> { - val hasGuestUser = userInfos.any { it.isGuest } - if (!hasGuestUser && canCreateGuestUser(settings)) { - add(UserActionModel.ENTER_GUEST_MODE) - } - } - UserActionModel.ADD_USER -> { - val canCreateUsers = - UserActionsUtil.canCreateUser( - manager, - repository, - settings.isUserSwitcherEnabled, - settings.isAddUsersFromLockscreen, - ) - - if (canCreateUsers) { - add(UserActionModel.ADD_USER) - } - } - UserActionModel.ADD_SUPERVISED_USER -> { - if ( - UserActionsUtil.canCreateSupervisedUser( - manager, - repository, - settings.isUserSwitcherEnabled, - settings.isAddUsersFromLockscreen, - supervisedUserPackageName, - ) - ) { - add(UserActionModel.ADD_SUPERVISED_USER) - } - } - else -> Unit - } - } - } - if ( - UserActionsUtil.canManageUsers( - repository, - settings.isUserSwitcherEnabled, - settings.isAddUsersFromLockscreen, - ) - ) { - add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - } - } - } - - val userRecords: StateFlow> = - combine( - userInfos, - repository.selectedUserInfo, - actions, - repository.userSwitcherSettings, - ) { userInfos, selectedUserInfo, actionModels, settings -> - ArrayList( - userInfos.map { - toRecord( - userInfo = it, - selectedUserId = selectedUserInfo.id, - ) - } + - actionModels.map { - toRecord( - action = it, - selectedUserId = selectedUserInfo.id, - isRestricted = - it != UserActionModel.ENTER_GUEST_MODE && - it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT && - !settings.isAddUsersFromLockscreen, - ) - } - ) - } - .onEach { notifyCallbacks() } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = ArrayList(), - ) - - val selectedUserRecord: StateFlow = - repository.selectedUserInfo - .map { selectedUserInfo -> - toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id) - } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = null, - ) - - /** Whether the device is configured to always have a guest user available. */ - val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated - - /** Whether the guest user is currently being reset. */ - val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting - - /** Whether to enable the user chip in the status bar */ - val isStatusBarUserChipEnabled: Boolean = repository.isStatusBarUserChipEnabled - - private val _dialogShowRequests = MutableStateFlow(null) - val dialogShowRequests: Flow = _dialogShowRequests.asStateFlow() - - private val _dialogDismissRequests = MutableStateFlow(null) - val dialogDismissRequests: Flow = _dialogDismissRequests.asStateFlow() - - val isSimpleUserSwitcher: Boolean - get() = repository.isSimpleUserSwitcher() - - val isUserSwitcherEnabled: Boolean - get() = repository.isUserSwitcherEnabled() - - val keyguardUpdateMonitorCallback = - object : KeyguardUpdateMonitorCallback() { - override fun onKeyguardGoingAway() { - dismissDialog() - } - } - - init { - 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) - addAction(Intent.ACTION_LOCALE_CHANGED) - }, - user = UserHandle.SYSTEM, - map = { intent, _ -> intent }, - ), - repository.selectedUserInfo.pairwise(null), - ) { intent, selectedUserChange -> - Pair(intent, selectedUserChange.previousValue) - } - .onEach { (intent, previousSelectedUser) -> - onBroadcastReceived(intent, previousSelectedUser) - } - .launchIn(applicationScope) - restartSecondaryService(repository.getSelectedUserInfo().id) - keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) - } - - fun addCallback(callback: UserCallback) { - applicationScope.launch { callbackMutex.withLock { callbacks.add(callback) } } - } - - fun removeCallback(callback: UserCallback) { - applicationScope.launch { callbackMutex.withLock { callbacks.remove(callback) } } - } - - fun refreshUsers() { - refreshUsersScheduler.refreshIfNotPaused() - } - - fun onDialogShown() { - _dialogShowRequests.value = null - } - - fun onDialogDismissed() { - _dialogDismissRequests.value = null - } - - fun dump(pw: PrintWriter) { - pw.println("UserInteractor state:") - pw.println(" lastSelectedNonGuestUserId=${repository.lastSelectedNonGuestUserId}") - - val users = userRecords.value.filter { it.info != null } - pw.println(" userCount=${userRecords.value.count { LegacyUserDataHelper.isUser(it) }}") - for (i in users.indices) { - pw.println(" ${users[i]}") - } - - val actions = userRecords.value.filter { it.info == null } - pw.println(" actionCount=${userRecords.value.count { !LegacyUserDataHelper.isUser(it) }}") - for (i in actions.indices) { - pw.println(" ${actions[i]}") - } - - pw.println("isSimpleUserSwitcher=$isSimpleUserSwitcher") - pw.println("isUserSwitcherEnabled=$isUserSwitcherEnabled") - pw.println("isGuestUserAutoCreated=$isGuestUserAutoCreated") - } - - fun onDeviceBootCompleted() { - guestUserInteractor.onDeviceBootCompleted() - } - - /** Switches to the user or executes the action represented by the given record. */ - fun onRecordSelected( - record: UserRecord, - dialogShower: UserSwitchDialogController.DialogShower? = null, - ) { - if (LegacyUserDataHelper.isUser(record)) { - // It's safe to use checkNotNull around record.info because isUser only returns true - // if record.info is not null. - uiEventLogger.log( - MultiUserActionsEventHelper.userSwitchMetric(checkNotNull(record.info)) - ) - selectUser(checkNotNull(record.info).id, dialogShower) - } else { - executeAction(LegacyUserDataHelper.toUserActionModel(record), dialogShower) - } - } - - /** Switches to the user with the given user ID. */ - fun selectUser( - newlySelectedUserId: Int, - dialogShower: UserSwitchDialogController.DialogShower? = null, - ) { - 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, - dialogShower = dialogShower, - ) - ) - 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, - dialogShower = dialogShower, - ) - ) - return - } - - dialogShower?.dismiss() - - switchUser(newlySelectedUserId) - } - - /** Executes the given action. */ - fun executeAction( - action: UserActionModel, - dialogShower: UserSwitchDialogController.DialogShower? = null, - ) { - when (action) { - UserActionModel.ENTER_GUEST_MODE -> { - uiEventLogger.log(MultiUserActionsEvent.CREATE_GUEST_FROM_USER_SWITCHER) - guestUserInteractor.createAndSwitchTo( - this::showDialog, - this::dismissDialog, - ) { userId -> - selectUser(userId, dialogShower) - } - } - UserActionModel.ADD_USER -> { - uiEventLogger.log(MultiUserActionsEvent.CREATE_USER_FROM_USER_SWITCHER) - val currentUser = repository.getSelectedUserInfo() - dismissDialog() - activityStarter.startActivity( - CreateUserActivity.createIntentForStart( - applicationContext, - keyguardInteractor.isKeyguardShowing() - ), - /* dismissShade= */ true, - /* animationController */ null, - /* showOverLockscreenWhenLocked */ true, - /* userHandle */ currentUser.getUserHandle(), - ) - } - UserActionModel.ADD_SUPERVISED_USER -> { - uiEventLogger.log(MultiUserActionsEvent.CREATE_RESTRICTED_USER_FROM_USER_SWITCHER) - dismissDialog() - activityStarter.startActivity( - Intent() - .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) - .setPackage(supervisedUserPackageName) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), - /* dismissShade= */ true, - ) - } - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> - activityStarter.startActivity( - Intent(Settings.ACTION_USER_SETTINGS), - /* dismissShade= */ true, - ) - } - } - - 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, - ) - } - - fun removeGuestUser( - @UserIdInt guestUserId: Int, - @UserIdInt targetUserId: Int, - ) { - applicationScope.launch { - guestUserInteractor.remove( - guestUserId = guestUserId, - targetUserId = targetUserId, - ::showDialog, - ::dismissDialog, - ::selectUser, - ) - } - } - - fun showUserSwitcher(expandable: Expandable) { - if (featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { - showDialog(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable)) - } else { - showDialog(ShowDialogRequestModel.ShowUserSwitcherDialog(expandable)) - } - } - - /** Returns the ID of the currently-selected user. */ - @UserIdInt - fun getSelectedUserId(): Int { - return repository.getSelectedUserInfo().id - } - - private fun showDialog(request: ShowDialogRequestModel) { - _dialogShowRequests.value = request - } - - private fun dismissDialog() { - _dialogDismissRequests.value = Unit - } - - private fun notifyCallbacks() { - applicationScope.launch { - callbackMutex.withLock { - val iterator = callbacks.iterator() - while (iterator.hasNext()) { - val callback = iterator.next() - if (!callback.isEvictable()) { - callback.onUserStateChanged() - } else { - iterator.remove() - } - } - } - } - } - - private suspend fun toRecord( - userInfo: UserInfo, - selectedUserId: Int, - ): UserRecord { - return LegacyUserDataHelper.createRecord( - context = applicationContext, - manager = manager, - userInfo = userInfo, - picture = null, - isCurrent = userInfo.id == selectedUserId, - canSwitchUsers = canSwitchUsers(selectedUserId), - ) - } - - private suspend fun toRecord( - action: UserActionModel, - selectedUserId: Int, - isRestricted: Boolean, - ): UserRecord { - return LegacyUserDataHelper.createRecord( - context = applicationContext, - selectedUserId = selectedUserId, - actionType = action, - isRestricted = isRestricted, - isSwitchToEnabled = - canSwitchUsers( - selectedUserId = selectedUserId, - isAction = true, - ) && - // If the user is auto-created is must not be currently resetting. - !(isGuestUserAutoCreated && isGuestUserResetting), - userRestrictionChecker = userRestrictionChecker, - ) - } - - private fun switchUser(userId: Int) { - // TODO(b/246631653): track jank and latency 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_LOCALE_CHANGED -> true - Intent.ACTION_USER_SWITCHED -> { - dismissDialog() - val selectedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) - if (previousUserInfo?.id != selectedUserId) { - notifyCallbacks() - 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) { - // Do not start service for user that is marked for deletion. - if (!manager.aliveUsers.map { it.id }.contains(userId)) { - return - } - - 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 != Process.myUserHandle().identifier) { - 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 -> - filterAndMapToUserModel( - userInfo = userInfo, - selectedUserId = selectedUserId, - canSwitchUsers = canSwitchUsers, - isUserSwitcherEnabled = isUserSwitcherEnabled, - ) - } - } - - /** - * Maps UserInfo to UserModel based on some parameters and return null under certain conditions - * to be filtered out. - */ - private suspend fun filterAndMapToUserModel( - userInfo: UserInfo, - selectedUserId: Int, - canSwitchUsers: Boolean, - isUserSwitcherEnabled: Boolean, - ): UserModel? { - 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 - // We meet the conditions to return the UserModel. - userInfo.isGuest || userInfo.supportsSwitchToByUser() -> - toUserModel(userInfo, selectedUserId, canSwitchUsers) - else -> null - } - } - - /** Maps UserInfo to UserModel based on some parameters. */ - private suspend fun toUserModel( - userInfo: UserInfo, - selectedUserId: Int, - canSwitchUsers: Boolean - ): UserModel { - val userId = userInfo.id - val isSelected = userId == selectedUserId - return if (userInfo.isGuest) { - UserModel( - id = userId, - name = Text.Loaded(userInfo.name), - image = - getUserImage( - isGuest = true, - userId = userId, - ), - isSelected = isSelected, - isSelectable = canSwitchUsers, - isGuest = true, - ) - } else { - UserModel( - id = userId, - name = Text.Loaded(userInfo.name), - image = - getUserImage( - isGuest = false, - userId = userId, - ), - isSelected = isSelected, - isSelectable = canSwitchUsers || isSelected, - isGuest = false, - ) - } - } - - private suspend fun canSwitchUsers( - selectedUserId: Int, - isAction: Boolean = false, - ): Boolean { - val isHeadlessSystemUserMode = - withContext(backgroundDispatcher) { headlessSystemUserMode.isHeadlessSystemUserMode() } - // Whether menu item should be active. True if item is a user or if any user has - // signed in since reboot or in all cases for non-headless system user mode. - val isItemEnabled = !isAction || !isHeadlessSystemUserMode || isAnyUserUnlocked() - return isItemEnabled && - withContext(backgroundDispatcher) { - manager.getUserSwitchability(UserHandle.of(selectedUserId)) - } == UserManager.SWITCHABILITY_STATUS_OK - } - - private suspend fun isAnyUserUnlocked(): Boolean { - return manager - .getUsers( - /* excludePartial= */ true, - /* excludeDying= */ true, - /* excludePreCreated= */ true - ) - .any { user -> - user.id != UserHandle.USER_SYSTEM && - withContext(backgroundDispatcher) { manager.isUserUnlocked(user.userHandle) } - } - } - - @SuppressLint("UseCompatLoadingForDrawables") - private suspend fun getUserImage( - isGuest: Boolean, - userId: Int, - ): Drawable { - if (isGuest) { - return checkNotNull( - applicationContext.getDrawable(com.android.settingslib.R.drawable.ic_account_circle) - ) - } - - // TODO(b/246631653): cache the bitmaps to avoid the background work to fetch them. - val userIcon = - withContext(backgroundDispatcher) { - manager.getUserIcon(userId)?.let { bitmap -> - val iconSize = - applicationContext.resources.getDimensionPixelSize( - R.dimen.bouncer_user_switcher_icon_size - ) - Icon.scaleDownIfNecessary(bitmap, iconSize, iconSize) - } - } - - if (userIcon != null) { - return BitmapDrawable(userIcon) - } - - return UserIcons.getDefaultUserIcon( - applicationContext.resources, - userId, - /* light= */ false - ) - } - - private fun canCreateGuestUser(settings: UserSwitcherSettingsModel): Boolean { - return guestUserInteractor.isGuestUserAutoCreated || - UserActionsUtil.canCreateGuest( - manager, - repository, - settings.isUserSwitcherEnabled, - settings.isAddUsersFromLockscreen, - ) - } - - companion object { - private const val TAG = "UserInteractor" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt new file mode 100644 index 000000000000..e0d205fc4b6a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt @@ -0,0 +1,820 @@ +/* + * Copyright (C) 2023 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.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.graphics.drawable.Icon +import android.os.Process +import android.os.RemoteException +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import android.util.Log +import com.android.internal.logging.UiEventLogger +import com.android.internal.util.UserIcons +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.systemui.SystemUISecondaryUserService +import com.android.systemui.animation.Expandable +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.qs.user.UserSwitchDialogController +import com.android.systemui.res.R +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import com.android.systemui.user.CreateUserActivity +import com.android.systemui.user.data.model.UserSwitcherSettingsModel +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.user.utils.MultiUserActionsEvent +import com.android.systemui.user.utils.MultiUserActionsEventHelper +import com.android.systemui.util.kotlin.pairwise +import com.android.systemui.utils.UserRestrictionChecker +import java.io.PrintWriter +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.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext + +/** Encapsulates business logic to for the user switcher. */ +@SysUISingleton +class UserSwitcherInteractor +@Inject +constructor( + @Application private val applicationContext: Context, + private val repository: UserRepository, + private val activityStarter: ActivityStarter, + private val keyguardInteractor: KeyguardInteractor, + private val featureFlags: FeatureFlags, + private val manager: UserManager, + private val headlessSystemUserMode: HeadlessSystemUserMode, + @Application private val applicationScope: CoroutineScope, + telephonyInteractor: TelephonyInteractor, + broadcastDispatcher: BroadcastDispatcher, + keyguardUpdateMonitor: KeyguardUpdateMonitor, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val activityManager: ActivityManager, + private val refreshUsersScheduler: RefreshUsersScheduler, + private val guestUserInteractor: GuestUserInteractor, + private val uiEventLogger: UiEventLogger, + private val userRestrictionChecker: UserRestrictionChecker, +) { + /** + * Defines interface for classes that can be notified when the state of users on the device is + * changed. + */ + interface UserCallback { + /** Returns `true` if this callback can be cleaned-up. */ + fun isEvictable(): Boolean = false + + /** Notifies that the state of users on the device has changed. */ + fun onUserStateChanged() + } + + private val supervisedUserPackageName: String? + get() = + applicationContext.getString( + com.android.internal.R.string.config_supervisedUserCreationPackage + ) + + private val callbackMutex = Mutex() + private val callbacks = mutableSetOf() + private val userInfos: Flow> = + repository.userInfos.map { userInfos -> userInfos.filter { it.isFull } } + + /** List of current on-device users to select from. */ + val users: Flow> + get() = + combine( + userInfos, + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { userInfos, selectedUserInfo, settings -> + toUserModels( + userInfos = userInfos, + selectedUserId = selectedUserInfo.id, + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, + ) + } + + /** The currently-selected user. */ + val selectedUser: Flow + get() = + repository.selectedUserInfo.map { selectedUserInfo -> + val selectedUserId = selectedUserInfo.id + toUserModel( + userInfo = selectedUserInfo, + selectedUserId = selectedUserId, + canSwitchUsers = canSwitchUsers(selectedUserId) + ) + } + + /** List of user-switcher related actions that are available. */ + val actions: Flow> + get() = + combine( + repository.selectedUserInfo, + userInfos, + repository.userSwitcherSettings, + keyguardInteractor.isKeyguardShowing, + ) { _, userInfos, settings, isDeviceLocked -> + buildList { + if (!isDeviceLocked || settings.isAddUsersFromLockscreen) { + // The device is locked and our setting to allow actions that add users + // from the lock-screen is not enabled. We can finish building the list + // here. + val isFullScreen = featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER) + + val actionList: List = + if (isFullScreen) { + listOf( + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.ENTER_GUEST_MODE, + ) + } else { + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + } + actionList.map { + when (it) { + UserActionModel.ENTER_GUEST_MODE -> { + val hasGuestUser = userInfos.any { it.isGuest } + if (!hasGuestUser && canCreateGuestUser(settings)) { + add(UserActionModel.ENTER_GUEST_MODE) + } + } + UserActionModel.ADD_USER -> { + val canCreateUsers = + UserActionsUtil.canCreateUser( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + ) + + if (canCreateUsers) { + add(UserActionModel.ADD_USER) + } + } + UserActionModel.ADD_SUPERVISED_USER -> { + if ( + UserActionsUtil.canCreateSupervisedUser( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + supervisedUserPackageName, + ) + ) { + add(UserActionModel.ADD_SUPERVISED_USER) + } + } + else -> Unit + } + } + } + if ( + UserActionsUtil.canManageUsers( + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + ) + ) { + add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + } + } + } + + val userRecords: StateFlow> = + combine( + userInfos, + repository.selectedUserInfo, + actions, + repository.userSwitcherSettings, + ) { userInfos, selectedUserInfo, actionModels, settings -> + ArrayList( + userInfos.map { + toRecord( + userInfo = it, + selectedUserId = selectedUserInfo.id, + ) + } + + actionModels.map { + toRecord( + action = it, + selectedUserId = selectedUserInfo.id, + isRestricted = + it != UserActionModel.ENTER_GUEST_MODE && + it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT && + !settings.isAddUsersFromLockscreen, + ) + } + ) + } + .onEach { notifyCallbacks() } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = ArrayList(), + ) + + val selectedUserRecord: StateFlow = + repository.selectedUserInfo + .map { selectedUserInfo -> + toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + + /** Whether the device is configured to always have a guest user available. */ + val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated + + /** Whether the guest user is currently being reset. */ + val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting + + /** Whether to enable the user chip in the status bar */ + val isStatusBarUserChipEnabled: Boolean = repository.isStatusBarUserChipEnabled + + private val _dialogShowRequests = MutableStateFlow(null) + val dialogShowRequests: Flow = _dialogShowRequests.asStateFlow() + + private val _dialogDismissRequests = MutableStateFlow(null) + val dialogDismissRequests: Flow = _dialogDismissRequests.asStateFlow() + + val isSimpleUserSwitcher: Boolean + get() = repository.isSimpleUserSwitcher() + + val isUserSwitcherEnabled: Boolean + get() = repository.isUserSwitcherEnabled() + + val keyguardUpdateMonitorCallback = + object : KeyguardUpdateMonitorCallback() { + override fun onKeyguardGoingAway() { + dismissDialog() + } + } + + init { + 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) + addAction(Intent.ACTION_LOCALE_CHANGED) + }, + user = UserHandle.SYSTEM, + map = { intent, _ -> intent }, + ), + repository.selectedUserInfo.pairwise(null), + ) { intent, selectedUserChange -> + Pair(intent, selectedUserChange.previousValue) + } + .onEach { (intent, previousSelectedUser) -> + onBroadcastReceived(intent, previousSelectedUser) + } + .launchIn(applicationScope) + restartSecondaryService(repository.getSelectedUserInfo().id) + keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) + } + + fun addCallback(callback: UserCallback) { + applicationScope.launch { callbackMutex.withLock { callbacks.add(callback) } } + } + + fun removeCallback(callback: UserCallback) { + applicationScope.launch { callbackMutex.withLock { callbacks.remove(callback) } } + } + + fun refreshUsers() { + refreshUsersScheduler.refreshIfNotPaused() + } + + fun onDialogShown() { + _dialogShowRequests.value = null + } + + fun onDialogDismissed() { + _dialogDismissRequests.value = null + } + + fun dump(pw: PrintWriter) { + pw.println("UserInteractor state:") + pw.println(" lastSelectedNonGuestUserId=${repository.lastSelectedNonGuestUserId}") + + val users = userRecords.value.filter { it.info != null } + pw.println(" userCount=${userRecords.value.count { LegacyUserDataHelper.isUser(it) }}") + for (i in users.indices) { + pw.println(" ${users[i]}") + } + + val actions = userRecords.value.filter { it.info == null } + pw.println(" actionCount=${userRecords.value.count { !LegacyUserDataHelper.isUser(it) }}") + for (i in actions.indices) { + pw.println(" ${actions[i]}") + } + + pw.println("isSimpleUserSwitcher=$isSimpleUserSwitcher") + pw.println("isUserSwitcherEnabled=$isUserSwitcherEnabled") + pw.println("isGuestUserAutoCreated=$isGuestUserAutoCreated") + } + + /** Switches to the user or executes the action represented by the given record. */ + fun onRecordSelected( + record: UserRecord, + dialogShower: UserSwitchDialogController.DialogShower? = null, + ) { + if (LegacyUserDataHelper.isUser(record)) { + // It's safe to use checkNotNull around record.info because isUser only returns true + // if record.info is not null. + uiEventLogger.log( + MultiUserActionsEventHelper.userSwitchMetric(checkNotNull(record.info)) + ) + selectUser(checkNotNull(record.info).id, dialogShower) + } else { + executeAction(LegacyUserDataHelper.toUserActionModel(record), dialogShower) + } + } + + /** Switches to the user with the given user ID. */ + fun selectUser( + newlySelectedUserId: Int, + dialogShower: UserSwitchDialogController.DialogShower? = null, + ) { + 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, + dialogShower = dialogShower, + ) + ) + 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, + dialogShower = dialogShower, + ) + ) + return + } + + dialogShower?.dismiss() + + switchUser(newlySelectedUserId) + } + + /** Executes the given action. */ + fun executeAction( + action: UserActionModel, + dialogShower: UserSwitchDialogController.DialogShower? = null, + ) { + when (action) { + UserActionModel.ENTER_GUEST_MODE -> { + uiEventLogger.log(MultiUserActionsEvent.CREATE_GUEST_FROM_USER_SWITCHER) + guestUserInteractor.createAndSwitchTo( + this::showDialog, + this::dismissDialog, + ) { userId -> + selectUser(userId, dialogShower) + } + } + UserActionModel.ADD_USER -> { + uiEventLogger.log(MultiUserActionsEvent.CREATE_USER_FROM_USER_SWITCHER) + val currentUser = repository.getSelectedUserInfo() + dismissDialog() + activityStarter.startActivity( + CreateUserActivity.createIntentForStart( + applicationContext, + keyguardInteractor.isKeyguardShowing() + ), + /* dismissShade= */ true, + /* animationController */ null, + /* showOverLockscreenWhenLocked */ true, + /* userHandle */ currentUser.getUserHandle(), + ) + } + UserActionModel.ADD_SUPERVISED_USER -> { + uiEventLogger.log(MultiUserActionsEvent.CREATE_RESTRICTED_USER_FROM_USER_SWITCHER) + dismissDialog() + activityStarter.startActivity( + Intent() + .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) + .setPackage(supervisedUserPackageName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + /* dismissShade= */ true, + ) + } + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> + activityStarter.startActivity( + Intent(Settings.ACTION_USER_SETTINGS), + /* dismissShade= */ true, + ) + } + } + + 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, + ) + } + + fun removeGuestUser( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + ) { + applicationScope.launch { + guestUserInteractor.remove( + guestUserId = guestUserId, + targetUserId = targetUserId, + ::showDialog, + ::dismissDialog, + ::selectUser, + ) + } + } + + fun showUserSwitcher(expandable: Expandable) { + if (featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { + showDialog(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable)) + } else { + showDialog(ShowDialogRequestModel.ShowUserSwitcherDialog(expandable)) + } + } + + private fun showDialog(request: ShowDialogRequestModel) { + _dialogShowRequests.value = request + } + + private fun dismissDialog() { + _dialogDismissRequests.value = Unit + } + + private fun notifyCallbacks() { + applicationScope.launch { + callbackMutex.withLock { + val iterator = callbacks.iterator() + while (iterator.hasNext()) { + val callback = iterator.next() + if (!callback.isEvictable()) { + callback.onUserStateChanged() + } else { + iterator.remove() + } + } + } + } + } + + private suspend fun toRecord( + userInfo: UserInfo, + selectedUserId: Int, + ): UserRecord { + return LegacyUserDataHelper.createRecord( + context = applicationContext, + manager = manager, + userInfo = userInfo, + picture = null, + isCurrent = userInfo.id == selectedUserId, + canSwitchUsers = canSwitchUsers(selectedUserId), + ) + } + + private suspend fun toRecord( + action: UserActionModel, + selectedUserId: Int, + isRestricted: Boolean, + ): UserRecord { + return LegacyUserDataHelper.createRecord( + context = applicationContext, + selectedUserId = selectedUserId, + actionType = action, + isRestricted = isRestricted, + isSwitchToEnabled = + canSwitchUsers( + selectedUserId = selectedUserId, + isAction = true, + ) && + // If the user is auto-created is must not be currently resetting. + !(isGuestUserAutoCreated && isGuestUserResetting), + userRestrictionChecker = userRestrictionChecker, + ) + } + + private fun switchUser(userId: Int) { + // TODO(b/246631653): track jank and latency 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_LOCALE_CHANGED -> true + Intent.ACTION_USER_SWITCHED -> { + dismissDialog() + val selectedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) + if (previousUserInfo?.id != selectedUserId) { + notifyCallbacks() + 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) { + // Do not start service for user that is marked for deletion. + if (!manager.aliveUsers.map { it.id }.contains(userId)) { + return + } + + 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 != Process.myUserHandle().identifier) { + 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 -> + filterAndMapToUserModel( + userInfo = userInfo, + selectedUserId = selectedUserId, + canSwitchUsers = canSwitchUsers, + isUserSwitcherEnabled = isUserSwitcherEnabled, + ) + } + } + + /** + * Maps UserInfo to UserModel based on some parameters and return null under certain conditions + * to be filtered out. + */ + private suspend fun filterAndMapToUserModel( + userInfo: UserInfo, + selectedUserId: Int, + canSwitchUsers: Boolean, + isUserSwitcherEnabled: Boolean, + ): UserModel? { + 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 + // We meet the conditions to return the UserModel. + userInfo.isGuest || userInfo.supportsSwitchToByUser() -> + toUserModel(userInfo, selectedUserId, canSwitchUsers) + else -> null + } + } + + /** Maps UserInfo to UserModel based on some parameters. */ + private suspend fun toUserModel( + userInfo: UserInfo, + selectedUserId: Int, + canSwitchUsers: Boolean + ): UserModel { + val userId = userInfo.id + val isSelected = userId == selectedUserId + return if (userInfo.isGuest) { + UserModel( + id = userId, + name = Text.Loaded(userInfo.name), + image = + getUserImage( + isGuest = true, + userId = userId, + ), + isSelected = isSelected, + isSelectable = canSwitchUsers, + isGuest = true, + ) + } else { + UserModel( + id = userId, + name = Text.Loaded(userInfo.name), + image = + getUserImage( + isGuest = false, + userId = userId, + ), + isSelected = isSelected, + isSelectable = canSwitchUsers || isSelected, + isGuest = false, + ) + } + } + + private suspend fun canSwitchUsers( + selectedUserId: Int, + isAction: Boolean = false, + ): Boolean { + val isHeadlessSystemUserMode = + withContext(backgroundDispatcher) { headlessSystemUserMode.isHeadlessSystemUserMode() } + // Whether menu item should be active. True if item is a user or if any user has + // signed in since reboot or in all cases for non-headless system user mode. + val isItemEnabled = !isAction || !isHeadlessSystemUserMode || isAnyUserUnlocked() + return isItemEnabled && + withContext(backgroundDispatcher) { + manager.getUserSwitchability(UserHandle.of(selectedUserId)) + } == UserManager.SWITCHABILITY_STATUS_OK + } + + private suspend fun isAnyUserUnlocked(): Boolean { + return manager + .getUsers( + /* excludePartial= */ true, + /* excludeDying= */ true, + /* excludePreCreated= */ true + ) + .any { user -> + user.id != UserHandle.USER_SYSTEM && + withContext(backgroundDispatcher) { manager.isUserUnlocked(user.userHandle) } + } + } + + @SuppressLint("UseCompatLoadingForDrawables") + private suspend fun getUserImage( + isGuest: Boolean, + userId: Int, + ): Drawable { + if (isGuest) { + return checkNotNull( + applicationContext.getDrawable(com.android.settingslib.R.drawable.ic_account_circle) + ) + } + + // TODO(b/246631653): cache the bitmaps to avoid the background work to fetch them. + val userIcon = + withContext(backgroundDispatcher) { + manager.getUserIcon(userId)?.let { bitmap -> + val iconSize = + applicationContext.resources.getDimensionPixelSize( + R.dimen.bouncer_user_switcher_icon_size + ) + Icon.scaleDownIfNecessary(bitmap, iconSize, iconSize) + } + } + + if (userIcon != null) { + return BitmapDrawable(userIcon) + } + + return UserIcons.getDefaultUserIcon( + applicationContext.resources, + userId, + /* light= */ false + ) + } + + private fun canCreateGuestUser(settings: UserSwitcherSettingsModel): Boolean { + return guestUserInteractor.isGuestUserAutoCreated || + UserActionsUtil.canCreateGuest( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + ) + } + + companion object { + private const val TAG = "UserSwitcherInteractor" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt index 0930cb8a3d7a..cced98376197 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -33,15 +33,15 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.tiles.UserDetailView import com.android.systemui.user.UserSwitchFullscreenDialog -import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.domain.interactor.UserSwitcherInteractor import com.android.systemui.user.domain.model.ShowDialogRequestModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import dagger.Lazy -import javax.inject.Inject -import javax.inject.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Provider /** Coordinates dialogs for user switcher logic. */ @SysUISingleton @@ -53,7 +53,7 @@ constructor( private val falsingManager: Lazy, private val broadcastSender: Lazy, private val dialogLaunchAnimator: Lazy, - private val interactor: Lazy, + private val interactor: Lazy, private val userDetailAdapterProvider: Provider, private val eventLogger: Lazy, private val activityStarter: Lazy, diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt index 78edad7c3af2..2c425b199b4e 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt @@ -17,12 +17,10 @@ package com.android.systemui.user.ui.viewmodel -import android.content.Context import android.graphics.drawable.Drawable import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Text -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.domain.interactor.UserSwitcherInteractor import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -33,8 +31,7 @@ import kotlinx.coroutines.flow.mapLatest class StatusBarUserChipViewModel @Inject constructor( - @Application private val context: Context, - interactor: UserInteractor, + interactor: UserSwitcherInteractor, ) { /** Whether the status bar chip ui should be available */ val chipEnabled: Boolean = interactor.isStatusBarUserChipEnabled diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt index 20f0fa8cf46b..1c31e8aef1ec 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt @@ -20,35 +20,34 @@ package com.android.systemui.user.ui.viewmodel import com.android.systemui.common.shared.model.Text import com.android.systemui.common.ui.drawable.CircularDrawable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.res.R import com.android.systemui.user.domain.interactor.GuestUserInteractor -import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.domain.interactor.UserSwitcherInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel -import javax.inject.Inject -import kotlin.math.ceil import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import javax.inject.Inject +import kotlin.math.ceil /** Models UI state for the user switcher feature. */ @SysUISingleton class UserSwitcherViewModel @Inject constructor( - private val userInteractor: UserInteractor, + private val userSwitcherInteractor: UserSwitcherInteractor, private val guestUserInteractor: GuestUserInteractor, ) { /** The currently selected user. */ val selectedUser: Flow = - userInteractor.selectedUser.map { user -> toViewModel(user) } + userSwitcherInteractor.selectedUser.map { user -> toViewModel(user) } /** On-device users. */ val users: Flow> = - userInteractor.users.map { models -> models.map { user -> toViewModel(user) } } + userSwitcherInteractor.users.map { models -> models.map { user -> toViewModel(user) } } /** The maximum number of columns that the user selection grid should use. */ val maximumUserColumns: Flow = users.map { getMaxUserSwitcherItemColumns(it.size) } @@ -61,7 +60,9 @@ constructor( val isMenuVisible: Flow = _isMenuVisible /** The user action menu. */ val menu: Flow> = - userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } } + userSwitcherInteractor.actions.map { actions -> + actions.map { action -> toViewModel(action) } + } /** Whether the button to open the user action menu is visible. */ val isOpenMenuButtonVisible: Flow = menu.map { it.isNotEmpty() } @@ -175,7 +176,7 @@ constructor( isTablet = true, ), onClicked = { - userInteractor.executeAction(action = model) + userSwitcherInteractor.executeAction(action = model) // We don't finish because we want to show a dialog over the full-screen UI and // that dialog can be dismissed in case the user changes their mind and decides not // to add a user. @@ -195,7 +196,7 @@ constructor( null } else { { - userInteractor.selectUser(model.id) + userSwitcherInteractor.selectUser(model.id) userSwitched.value = true } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index 20d4eb907944..581739030aad 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -63,7 +63,7 @@ import com.android.systemui.statusbar.policy.DevicePostureController import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.UserSwitcherController -import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.kotlin.JavaAdapter import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argThat @@ -74,7 +74,6 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.GlobalSettings import com.google.common.truth.Truth import dagger.Lazy -import java.util.Optional import junit.framework.Assert import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -97,6 +96,7 @@ import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import java.util.Optional @SmallTest @RunWith(AndroidJUnit4::class) @@ -136,7 +136,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var viewMediatorCallback: ViewMediatorCallback @Mock private lateinit var audioManager: AudioManager - @Mock private lateinit var userInteractor: UserInteractor + @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor @Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController @Mock private lateinit var postureController: DevicePostureController @@ -218,7 +218,6 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { featureFlags ) - whenever(userInteractor.getSelectedUserId()).thenReturn(TARGET_USER_ID) sceneTestUtils = SceneTestUtils(this) sceneInteractor = sceneTestUtils.sceneInteractor() keyguardTransitionInteractor = @@ -260,7 +259,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { mock(), mock(), { JavaAdapter(sceneTestUtils.testScope.backgroundScope) }, - userInteractor, + mSelectedUserInteractor, deviceProvisionedController, faceAuthAccessibilityDelegate, keyguardTransitionInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt index d6e19cbcb826..e87adf5e424b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt @@ -64,8 +64,6 @@ class KeyguardDismissActionInteractorTest : SysuiTestCase() { KeyguardDismissInteractorFactory.create( context = context, testScope = testScope, - broadcastDispatcher = fakeBroadcastDispatcher, - dispatcher = dispatcher, ) keyguardRepository = dismissInteractorWithDependencies.keyguardRepository transitionRepository = FakeKeyguardTransitionRepository() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorTest.kt index a5cfbbfda196..ecb46bdd06c4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorTest.kt @@ -58,8 +58,6 @@ class KeyguardDismissInteractorTest : SysuiTestCase() { KeyguardDismissInteractorFactory.create( context = context, testScope = testScope, - broadcastDispatcher = fakeBroadcastDispatcher, - dispatcher = dispatcher, ) underTest = underTestDependencies.interactor underTestDependencies.userRepository.setUserInfos(listOf(userInfo)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index eb006100d535..05da80032059 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -90,7 +90,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository; -import com.android.systemui.user.domain.interactor.UserInteractor; +import com.android.systemui.user.domain.interactor.UserSwitcherInteractor; import com.google.common.util.concurrent.MoreExecutors; @@ -230,7 +230,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { keyguardTransitionInteractor, powerInteractor, new FakeUserSetupRepository(), - mock(UserInteractor.class), + mock(UserSwitcherInteractor.class), new SharedNotificationContainerInteractor( configurationRepository, mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java index 65174bab7f63..dd81807faf75 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java @@ -95,15 +95,16 @@ import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository; -import com.android.systemui.user.domain.interactor.UserInteractor; +import com.android.systemui.user.domain.interactor.UserSwitcherInteractor; import com.android.systemui.util.kotlin.JavaAdapter; +import dagger.Lazy; + import org.junit.After; import org.junit.Before; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import dagger.Lazy; import kotlinx.coroutines.test.TestScope; public class QuickSettingsControllerBaseTest extends SysuiTestCase { @@ -162,7 +163,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { @Mock protected DumpManager mDumpManager; @Mock protected UiEventLogger mUiEventLogger; @Mock protected CastController mCastController; - @Mock protected UserInteractor mUserInteractor; + @Mock protected UserSwitcherInteractor mUserSwitcherInteractor; protected FakeDisableFlagsRepository mDisableFlagsRepository = new FakeDisableFlagsRepository(); protected FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository(); @@ -266,7 +267,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { keyguardTransitionInteractor, powerInteractor, new FakeUserSetupRepository(), - mUserInteractor, + mUserSwitcherInteractor, new SharedNotificationContainerInteractor( configurationRepository, mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt new file mode 100644 index 000000000000..8b1c69734528 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt @@ -0,0 +1,44 @@ +package com.android.systemui.user.domain.interactor + +import android.content.pm.UserInfo +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.user.data.repository.FakeUserRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class SelectedUserInteractorTest : SysuiTestCase() { + + private lateinit var underTest: SelectedUserInteractor + + private val userRepository = FakeUserRepository() + + @Before + fun setUp() { + userRepository.setUserInfos(USER_INFOS) + underTest = SelectedUserInteractor(userRepository) + } + + @Test + fun getSelectedUserIdReturnsId() { + runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[0]) } + + val actualId = underTest.getSelectedUserId() + + assertThat(actualId).isEqualTo(USER_INFOS[0].id) + } + + companion object { + private val USER_INFOS = + listOf( + UserInfo(100, "First user", 0), + UserInfo(101, "Second user", 0), + ) + } +} 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 deleted file mode 100644 index c56266dde752..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ /dev/null @@ -1,1206 +0,0 @@ -/* - * 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.app.ActivityManager -import android.app.admin.DevicePolicyManager -import android.content.Context -import android.content.Intent -import android.content.pm.UserInfo -import android.graphics.Bitmap -import android.graphics.drawable.Drawable -import android.os.Process -import android.os.UserHandle -import android.os.UserManager -import android.provider.Settings -import androidx.test.filters.SmallTest -import com.android.internal.logging.UiEventLogger -import com.android.keyguard.KeyguardUpdateMonitor -import com.android.keyguard.KeyguardUpdateMonitorCallback -import com.android.systemui.GuestResetOrExitSessionReceiver -import com.android.systemui.GuestResumeSessionReceiver -import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.Expandable -import com.android.systemui.common.shared.model.Text -import com.android.systemui.coroutines.collectLastValue -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.KeyguardInteractorFactory -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.qs.user.UserSwitchDialogController -import com.android.systemui.res.R -import com.android.systemui.statusbar.policy.DeviceProvisionedController -import com.android.systemui.telephony.data.repository.FakeTelephonyRepository -import com.android.systemui.telephony.domain.interactor.TelephonyInteractor -import com.android.systemui.user.data.model.UserSwitcherSettingsModel -import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.user.data.source.UserRecord -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.user.utils.MultiUserActionsEvent -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 junit.framework.Assert.assertNotNull -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.ArgumentMatchers.anyInt -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.never -import org.mockito.Mockito.spy -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(JUnit4::class) -class UserInteractorTest : SysuiTestCase() { - - @Mock private lateinit var activityStarter: ActivityStarter - @Mock private lateinit var manager: UserManager - @Mock private lateinit var headlessSystemUserMode: HeadlessSystemUserMode - @Mock private lateinit var activityManager: ActivityManager - @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController - @Mock private lateinit var devicePolicyManager: DevicePolicyManager - @Mock private lateinit var uiEventLogger: UiEventLogger - @Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower - @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver - @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver - @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - - private lateinit var underTest: UserInteractor - - private lateinit var spyContext: Context - private lateinit var testScope: TestScope - private lateinit var userRepository: FakeUserRepository - private lateinit var keyguardReply: KeyguardInteractorFactory.WithDependencies - private lateinit var keyguardRepository: FakeKeyguardRepository - private lateinit var telephonyRepository: FakeTelephonyRepository - private lateinit var testDispatcher: TestDispatcher - private lateinit var featureFlags: FakeFeatureFlags - private lateinit var refreshUsersScheduler: RefreshUsersScheduler - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(manager.getUserIcon(anyInt())).thenReturn(ICON) - whenever(manager.canAddMoreUsers(any())).thenReturn(true) - - overrideResource(com.android.settingslib.R.drawable.ic_account_circle, GUEST_ICON) - overrideResource(R.dimen.max_avatar_size, 10) - overrideResource( - com.android.internal.R.string.config_supervisedUserCreationPackage, - SUPERVISED_USER_CREATION_APP_PACKAGE, - ) - - featureFlags = - FakeFeatureFlags().apply { - set(Flags.FULL_SCREEN_USER_SWITCHER, false) - set(Flags.FACE_AUTH_REFACTOR, true) - } - spyContext = spy(context) - keyguardReply = KeyguardInteractorFactory.create(featureFlags = featureFlags) - keyguardRepository = keyguardReply.repository - userRepository = FakeUserRepository() - telephonyRepository = FakeTelephonyRepository() - testDispatcher = StandardTestDispatcher() - testScope = TestScope(testDispatcher) - refreshUsersScheduler = - RefreshUsersScheduler( - applicationScope = testScope.backgroundScope, - mainDispatcher = testDispatcher, - repository = userRepository, - ) - } - - @Test - fun createUserInteractor_processUser_noSecondaryService() { - createUserInteractor() - verify(spyContext, never()).startServiceAsUser(any(), any()) - } - - @Test - fun createUserInteractor_nonProcessUser_startsSecondaryService() { - val userId = Process.myUserHandle().identifier + 1 - whenever(manager.aliveUsers).thenReturn(listOf(createUserInfo(userId, "abc"))) - - createUserInteractor(false /* startAsProcessUser */) - verify(spyContext).startServiceAsUser(any(), any()) - } - - @Test - fun testKeyguardUpdateMonitor_onKeyguardGoingAway() { - createUserInteractor() - testScope.runTest { - val argumentCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) - verify(keyguardUpdateMonitor).registerCallback(argumentCaptor.capture()) - - argumentCaptor.value.onKeyguardGoingAway() - - val lastValue = collectLastValue(underTest.dialogDismissRequests) - assertNotNull(lastValue) - } - } - - @Test - fun onRecordSelected_user() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 3, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - underTest.onRecordSelected(UserRecord(info = userInfos[1]), dialogShower) - - verify(uiEventLogger, times(1)) - .log(MultiUserActionsEvent.SWITCH_TO_USER_FROM_USER_SWITCHER) - verify(dialogShower).dismiss() - verify(activityManager).switchUser(userInfos[1].id) - Unit - } - } - - @Test - fun onRecordSelected_switchToGuestUser() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - underTest.onRecordSelected(UserRecord(info = userInfos.last())) - - verify(uiEventLogger, times(1)) - .log(MultiUserActionsEvent.SWITCH_TO_GUEST_FROM_USER_SWITCHER) - verify(activityManager).switchUser(userInfos.last().id) - Unit - } - } - - @Test - fun onRecordSelected_switchToRestrictedUser() { - createUserInteractor() - testScope.runTest { - var userInfos = createUserInfos(count = 2, includeGuest = false).toMutableList() - userInfos.add( - UserInfo( - 60, - "Restricted user", - /* iconPath= */ "", - /* flags= */ UserInfo.FLAG_FULL, - UserManager.USER_TYPE_FULL_RESTRICTED, - ) - ) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - underTest.onRecordSelected(UserRecord(info = userInfos.last())) - - verify(uiEventLogger, times(1)) - .log(MultiUserActionsEvent.SWITCH_TO_RESTRICTED_USER_FROM_USER_SWITCHER) - verify(activityManager).switchUser(userInfos.last().id) - Unit - } - } - - @Test - fun onRecordSelected_enterGuestMode() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 3, 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) - - underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower) - runCurrent() - - verify(uiEventLogger, times(1)) - .log(MultiUserActionsEvent.CREATE_GUEST_FROM_USER_SWITCHER) - verify(dialogShower).dismiss() - verify(manager).createGuest(any()) - Unit - } - } - - @Test - fun onRecordSelected_action() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - underTest.onRecordSelected(UserRecord(isAddSupervisedUser = true), dialogShower) - - verify(uiEventLogger, times(1)) - .log(MultiUserActionsEvent.CREATE_RESTRICTED_USER_FROM_USER_SWITCHER) - verify(dialogShower, never()).dismiss() - verify(activityStarter).startActivity(any(), anyBoolean()) - } - } - - @Test - fun users_switcherEnabled() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - val value = collectLastValue(underTest.users) - - assertUsers(models = value(), count = 3, includeGuest = true) - } - } - - @Test - fun users_switchesToSecondUser() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - val value = collectLastValue(underTest.users) - userRepository.setSelectedUserInfo(userInfos[1]) - - assertUsers(models = value(), count = 2, selectedIndex = 1) - } - } - - @Test - fun users_switcherNotEnabled() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) - - val value = collectLastValue(underTest.users) - assertUsers(models = value(), count = 1) - } - } - - @Test - fun selectedUser() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - val value = collectLastValue(underTest.selectedUser) - assertUser(value(), id = 0, isSelected = true) - - userRepository.setSelectedUserInfo(userInfos[1]) - assertUser(value(), id = 1, isSelected = true) - } - } - - @Test - fun actions_deviceUnlocked() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - keyguardRepository.setKeyguardShowing(false) - val value = collectLastValue(underTest.actions) - - runCurrent() - - assertThat(value()) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - } - } - - @Test - fun actions_deviceUnlocked_fullScreen() { - createUserInteractor() - testScope.runTest { - featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) - val userInfos = createUserInfos(count = 2, includeGuest = false) - - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - keyguardRepository.setKeyguardShowing(false) - val value = collectLastValue(underTest.actions) - - assertThat(value()) - .isEqualTo( - listOf( - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - } - } - - @Test - fun actions_deviceUnlockedUserNotPrimary_emptyList() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[1]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - keyguardRepository.setKeyguardShowing(false) - val value = collectLastValue(underTest.actions) - - assertThat(value()).isEqualTo(emptyList()) - } - } - - @Test - fun actions_deviceUnlockedUserIsGuest_emptyList() { - createUserInteractor() - testScope.runTest { - 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) - val value = collectLastValue(underTest.actions) - - assertThat(value()).isEqualTo(emptyList()) - } - } - - @Test - fun actions_deviceLockedAddFromLockscreenSet_fullList() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings( - UserSwitcherSettingsModel( - isUserSwitcherEnabled = true, - isAddUsersFromLockscreen = true, - ) - ) - keyguardRepository.setKeyguardShowing(false) - val value = collectLastValue(underTest.actions) - - assertThat(value()) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - } - } - - @Test - fun actions_deviceLockedAddFromLockscreenSet_fullList_fullScreen() { - createUserInteractor() - testScope.runTest { - featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings( - UserSwitcherSettingsModel( - isUserSwitcherEnabled = true, - isAddUsersFromLockscreen = true, - ) - ) - keyguardRepository.setKeyguardShowing(false) - val value = collectLastValue(underTest.actions) - - assertThat(value()) - .isEqualTo( - listOf( - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - } - } - - @Test - fun actions_deviceLocked_onlymanageUserIsShown() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - keyguardRepository.setKeyguardShowing(true) - val value = collectLastValue(underTest.actions) - - assertThat(value()).isEqualTo(listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)) - } - } - - @Test - fun executeAction_addUser_dismissesDialogAndStartsActivity() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - keyguardRepository.setKeyguardShowing(false) - - underTest.executeAction(UserActionModel.ADD_USER) - verify(uiEventLogger, times(1)) - .log(MultiUserActionsEvent.CREATE_USER_FROM_USER_SWITCHER) - underTest.onDialogShown() - } - } - - @Test - fun executeAction_addSupervisedUser_dismissesDialogAndStartsActivity() { - createUserInteractor() - testScope.runTest { - underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) - - verify(uiEventLogger, times(1)) - .log(MultiUserActionsEvent.CREATE_RESTRICTED_USER_FROM_USER_SWITCHER) - val intentCaptor = kotlinArgumentCaptor() - verify(activityStarter).startActivity(intentCaptor.capture(), eq(true)) - assertThat(intentCaptor.value.action) - .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER) - assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE) - } - } - - @Test - fun executeAction_navigateToManageUsers() { - createUserInteractor() - testScope.runTest { - underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - - val intentCaptor = kotlinArgumentCaptor() - verify(activityStarter).startActivity(intentCaptor.capture(), eq(true)) - assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS) - } - } - - @Test - fun executeAction_guestMode() { - createUserInteractor() - testScope.runTest { - 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() - backgroundScope.launch { - underTest.dialogShowRequests.collect { - dialogRequests.add(it) - if (it != null) { - underTest.onDialogShown() - } - } - } - backgroundScope.launch { - underTest.dialogDismissRequests.collect { - if (it != null) { - underTest.onDialogDismissed() - } - } - } - - underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) - runCurrent() - - verify(uiEventLogger, times(1)) - .log(MultiUserActionsEvent.CREATE_GUEST_FROM_USER_SWITCHER) - assertThat(dialogRequests) - .contains( - ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true), - ) - verify(activityManager).switchUser(guestUserInfo.id) - } - } - - @Test - fun selectUser_alreadySelectedGuestReSelected_exitGuestDialog() { - createUserInteractor() - testScope.runTest { - 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)) - val dialogRequest = collectLastValue(underTest.dialogShowRequests) - - underTest.selectUser( - newlySelectedUserId = guestUserInfo.id, - dialogShower = dialogShower, - ) - - assertThat(dialogRequest()) - .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) - verify(dialogShower, never()).dismiss() - } - } - - @Test - fun selectUser_currentlyGuestNonGuestSelected_exitGuestDialog() { - createUserInteractor() - testScope.runTest { - 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)) - val dialogRequest = collectLastValue(underTest.dialogShowRequests) - - underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower) - - assertThat(dialogRequest()) - .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) - verify(dialogShower, never()).dismiss() - } - } - - @Test - fun selectUser_notCurrentlyGuest_switchesUsers() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - val dialogRequest = collectLastValue(underTest.dialogShowRequests) - - underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower) - - assertThat(dialogRequest()).isNull() - verify(activityManager).switchUser(userInfos[1].id) - verify(dialogShower).dismiss() - } - } - - @Test - fun telephonyCallStateChanges_refreshesUsers() { - createUserInteractor() - testScope.runTest { - runCurrent() - - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - telephonyRepository.setCallState(1) - runCurrent() - - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) - } - } - - @Test - fun userSwitchedBroadcast() { - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - whenever(manager.aliveUsers).thenReturn(userInfos) - createUserInteractor() - 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) - runCurrent() - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - userRepository.setSelectedUserInfo(userInfos[1]) - runCurrent() - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - spyContext, - Intent(Intent.ACTION_USER_SWITCHED) - .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id), - ) - runCurrent() - - verify(callback1, atLeastOnce()).onUserStateChanged() - verify(callback2, atLeastOnce()).onUserStateChanged() - assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id) - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) - verify(spyContext).startServiceAsUser(any(), eq(UserHandle.of(userInfos[1].id))) - } - } - - @Test - fun userInfoChangedBroadcast() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - runCurrent() - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - spyContext, - Intent(Intent.ACTION_USER_INFO_CHANGED), - ) - - runCurrent() - - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) - } - } - - @Test - fun systemUserUnlockedBroadcast_refreshUsers() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - runCurrent() - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - spyContext, - Intent(Intent.ACTION_USER_UNLOCKED) - .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM), - ) - runCurrent() - - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) - } - } - - @Test - fun localeChanged_refreshUsers() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - runCurrent() - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - spyContext, - Intent(Intent.ACTION_LOCALE_CHANGED) - ) - runCurrent() - - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) - } - } - - @Test - fun nonSystemUserUnlockedBroadcast_doNotRefreshUsers() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - spyContext, - Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337), - ) - - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount) - } - } - - @Test - fun userRecords() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 3, includeGuest = false) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - keyguardRepository.setKeyguardShowing(false) - - runCurrent() - - assertRecords( - records = underTest.userRecords.value, - userIds = listOf(0, 1, 2), - selectedUserIndex = 0, - includeGuest = false, - expectedActions = - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ), - ) - } - } - - @Test - fun userRecordsFullScreen() { - createUserInteractor() - testScope.runTest { - featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) - val userInfos = createUserInfos(count = 3, includeGuest = false) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - keyguardRepository.setKeyguardShowing(false) - - runCurrent() - - assertRecords( - records = underTest.userRecords.value, - userIds = listOf(0, 1, 2), - selectedUserIndex = 0, - includeGuest = false, - expectedActions = - listOf( - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ), - ) - } - } - - @Test - fun selectedUserRecord() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - keyguardRepository.setKeyguardShowing(false) - - assertRecordForUser( - record = underTest.selectedUserRecord.value, - id = 0, - hasPicture = true, - isCurrent = true, - isSwitchToEnabled = true, - ) - } - } - - @Test - fun users_secondaryUser_guestUserCanBeSwitchedTo() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[1]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - val res = collectLastValue(underTest.users) - assertThat(res()?.size == 3).isTrue() - assertThat(res()?.find { it.isGuest }).isNotNull() - } - } - - @Test - fun users_secondaryUser_noGuestAction() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[1]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - val res = collectLastValue(underTest.actions) - assertThat(res()?.find { it == UserActionModel.ENTER_GUEST_MODE }).isNull() - } - } - - @Test - fun users_secondaryUser_noGuestUserRecord() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[1]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - assertThat(underTest.userRecords.value.find { it.isGuest }).isNull() - } - } - - @Test - fun showUserSwitcher_fullScreenDisabled_showsDialogSwitcher() { - createUserInteractor() - testScope.runTest { - val expandable = mock() - underTest.showUserSwitcher(expandable) - - val dialogRequest = collectLastValue(underTest.dialogShowRequests) - - // Dialog is shown. - assertThat(dialogRequest()) - .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherDialog(expandable)) - - underTest.onDialogShown() - assertThat(dialogRequest()).isNull() - } - } - - @Test - fun showUserSwitcher_fullScreenEnabled_launchesFullScreenDialog() { - createUserInteractor() - testScope.runTest { - featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) - - val expandable = mock() - underTest.showUserSwitcher(expandable) - - val dialogRequest = collectLastValue(underTest.dialogShowRequests) - - // Dialog is shown. - assertThat(dialogRequest()) - .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable)) - - underTest.onDialogShown() - assertThat(dialogRequest()).isNull() - } - } - - @Test - fun users_secondaryUser_managedProfileIsNotIncluded() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList() - userInfos.add( - UserInfo( - 50, - "Work Profile", - /* iconPath= */ "", - /* flags= */ UserInfo.FLAG_MANAGED_PROFILE - ) - ) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[1]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - val res = collectLastValue(underTest.users) - assertThat(res()?.size == 3).isTrue() - } - } - - @Test - fun currentUserIsNotPrimaryAndUserSwitcherIsDisabled() { - createUserInteractor() - testScope.runTest { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[1]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) - val selectedUser = collectLastValue(underTest.selectedUser) - assertThat(selectedUser()).isNotNull() - } - } - - @Test - fun userRecords_isActionAndNoUsersUnlocked_actionIsDisabled() { - createUserInteractor() - testScope.runTest { - keyguardRepository.setKeyguardShowing(true) - whenever(manager.getUserSwitchability(any())) - .thenReturn(UserManager.SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED) - val userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList() - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[1]) - userRepository.setSettings( - UserSwitcherSettingsModel( - isUserSwitcherEnabled = true, - isAddUsersFromLockscreen = true - ) - ) - - runCurrent() - underTest.userRecords.value - .filter { it.info == null } - .forEach { action -> assertThat(action.isSwitchToEnabled).isFalse() } - } - } - - @Test - fun userRecords_isActionAndNoUsersUnlocked_actionIsDisabled_HeadlessMode() { - createUserInteractor() - testScope.runTest { - keyguardRepository.setKeyguardShowing(true) - whenever(headlessSystemUserMode.isHeadlessSystemUserMode()).thenReturn(true) - whenever(manager.isUserUnlocked(anyInt())).thenReturn(false) - val userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList() - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[1]) - userRepository.setSettings( - UserSwitcherSettingsModel( - isUserSwitcherEnabled = true, - isAddUsersFromLockscreen = true - ) - ) - - runCurrent() - underTest.userRecords.value - .filter { it.info == null } - .forEach { action -> assertThat(action.isSwitchToEnabled).isFalse() } - } - } - - @Test - fun initWithNoAliveUsers() { - whenever(manager.aliveUsers).thenReturn(listOf()) - createUserInteractor() - verify(spyContext, never()).startServiceAsUser(any(), any()) - } - - 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 assertRecords( - records: List, - userIds: List, - selectedUserIndex: Int = 0, - includeGuest: Boolean = false, - expectedActions: List = emptyList(), - ) { - assertThat(records.size >= userIds.size).isTrue() - userIds.indices.forEach { userIndex -> - val record = records[userIndex] - assertThat(record.info).isNotNull() - val isGuest = includeGuest && userIndex == userIds.size - 1 - assertRecordForUser( - record = record, - id = userIds[userIndex], - hasPicture = !isGuest, - isCurrent = userIndex == selectedUserIndex, - isGuest = isGuest, - isSwitchToEnabled = true, - ) - } - - assertThat(records.size - userIds.size).isEqualTo(expectedActions.size) - (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex -> - val record = records[actionIndex] - assertThat(record.info).isNull() - assertRecordForAction( - record = record, - type = expectedActions[actionIndex - userIds.size], - ) - } - } - - private fun assertRecordForUser( - record: UserRecord?, - id: Int? = null, - hasPicture: Boolean = false, - isCurrent: Boolean = false, - isGuest: Boolean = false, - isSwitchToEnabled: Boolean = false, - ) { - checkNotNull(record) - assertThat(record.info?.id).isEqualTo(id) - assertThat(record.picture != null).isEqualTo(hasPicture) - assertThat(record.isCurrent).isEqualTo(isCurrent) - assertThat(record.isGuest).isEqualTo(isGuest) - assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled) - } - - 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) - } - - private fun createUserInteractor(startAsProcessUser: Boolean = true) { - val processUserId = Process.myUserHandle().identifier - val startUserId = if (startAsProcessUser) processUserId else (processUserId + 1) - runBlocking { - val userInfo = - createUserInfo(id = startUserId, name = "user_$startUserId", isPrimary = true) - userRepository.setUserInfos(listOf(userInfo)) - userRepository.setSelectedUserInfo(userInfo) - } - underTest = - UserInteractor( - applicationContext = spyContext, - repository = userRepository, - activityStarter = activityStarter, - keyguardInteractor = keyguardReply.keyguardInteractor, - manager = manager, - headlessSystemUserMode = headlessSystemUserMode, - applicationScope = testScope.backgroundScope, - telephonyInteractor = - TelephonyInteractor( - repository = telephonyRepository, - ), - broadcastDispatcher = fakeBroadcastDispatcher, - keyguardUpdateMonitor = keyguardUpdateMonitor, - backgroundDispatcher = testDispatcher, - activityManager = activityManager, - refreshUsersScheduler = refreshUsersScheduler, - guestUserInteractor = - GuestUserInteractor( - applicationContext = spyContext, - applicationScope = testScope.backgroundScope, - mainDispatcher = testDispatcher, - backgroundDispatcher = testDispatcher, - manager = manager, - repository = userRepository, - deviceProvisionedController = deviceProvisionedController, - devicePolicyManager = devicePolicyManager, - refreshUsersScheduler = refreshUsersScheduler, - uiEventLogger = uiEventLogger, - resumeSessionReceiver = resumeSessionReceiver, - resetOrExitSessionReceiver = resetOrExitSessionReceiver, - ), - uiEventLogger = uiEventLogger, - featureFlags = featureFlags, - userRestrictionChecker = mock(), - ) - } - - 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 or UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL - } else { - UserInfo.FLAG_FULL - }, - if (isGuest) { - UserManager.USER_TYPE_FULL_GUEST - } else { - UserManager.USER_TYPE_FULL_SYSTEM - }, - ) - } - - companion object { - private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) - private val GUEST_ICON: Drawable = mock() - private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation" - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt new file mode 100644 index 000000000000..1968d75a7964 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt @@ -0,0 +1,1206 @@ +/* + * 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.app.ActivityManager +import android.app.admin.DevicePolicyManager +import android.content.Context +import android.content.Intent +import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.os.Process +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.systemui.GuestResetOrExitSessionReceiver +import com.android.systemui.GuestResumeSessionReceiver +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.Text +import com.android.systemui.coroutines.collectLastValue +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.KeyguardInteractorFactory +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.qs.user.UserSwitchDialogController +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.telephony.data.repository.FakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import com.android.systemui.user.data.model.UserSwitcherSettingsModel +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.source.UserRecord +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.user.utils.MultiUserActionsEvent +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 junit.framework.Assert.assertNotNull +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class UserSwitcherInteractorTest : SysuiTestCase() { + + @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var manager: UserManager + @Mock private lateinit var headlessSystemUserMode: HeadlessSystemUserMode + @Mock private lateinit var activityManager: ActivityManager + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var uiEventLogger: UiEventLogger + @Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower + @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver + @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + + private lateinit var underTest: UserSwitcherInteractor + + private lateinit var spyContext: Context + private lateinit var testScope: TestScope + private lateinit var userRepository: FakeUserRepository + private lateinit var keyguardReply: KeyguardInteractorFactory.WithDependencies + private lateinit var keyguardRepository: FakeKeyguardRepository + private lateinit var telephonyRepository: FakeTelephonyRepository + private lateinit var testDispatcher: TestDispatcher + private lateinit var featureFlags: FakeFeatureFlags + private lateinit var refreshUsersScheduler: RefreshUsersScheduler + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(manager.getUserIcon(anyInt())).thenReturn(ICON) + whenever(manager.canAddMoreUsers(any())).thenReturn(true) + + overrideResource(com.android.settingslib.R.drawable.ic_account_circle, GUEST_ICON) + overrideResource(R.dimen.max_avatar_size, 10) + overrideResource( + com.android.internal.R.string.config_supervisedUserCreationPackage, + SUPERVISED_USER_CREATION_APP_PACKAGE, + ) + + featureFlags = + FakeFeatureFlags().apply { + set(Flags.FULL_SCREEN_USER_SWITCHER, false) + set(Flags.FACE_AUTH_REFACTOR, true) + } + spyContext = spy(context) + keyguardReply = KeyguardInteractorFactory.create(featureFlags = featureFlags) + keyguardRepository = keyguardReply.repository + userRepository = FakeUserRepository() + telephonyRepository = FakeTelephonyRepository() + testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = testScope.backgroundScope, + mainDispatcher = testDispatcher, + repository = userRepository, + ) + } + + @Test + fun createUserInteractor_processUser_noSecondaryService() { + createUserInteractor() + verify(spyContext, never()).startServiceAsUser(any(), any()) + } + + @Test + fun createUserInteractor_nonProcessUser_startsSecondaryService() { + val userId = Process.myUserHandle().identifier + 1 + whenever(manager.aliveUsers).thenReturn(listOf(createUserInfo(userId, "abc"))) + + createUserInteractor(false /* startAsProcessUser */) + verify(spyContext).startServiceAsUser(any(), any()) + } + + @Test + fun testKeyguardUpdateMonitor_onKeyguardGoingAway() { + createUserInteractor() + testScope.runTest { + val argumentCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) + verify(keyguardUpdateMonitor).registerCallback(argumentCaptor.capture()) + + argumentCaptor.value.onKeyguardGoingAway() + + val lastValue = collectLastValue(underTest.dialogDismissRequests) + assertNotNull(lastValue) + } + } + + @Test + fun onRecordSelected_user() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + underTest.onRecordSelected(UserRecord(info = userInfos[1]), dialogShower) + + verify(uiEventLogger, times(1)) + .log(MultiUserActionsEvent.SWITCH_TO_USER_FROM_USER_SWITCHER) + verify(dialogShower).dismiss() + verify(activityManager).switchUser(userInfos[1].id) + Unit + } + } + + @Test + fun onRecordSelected_switchToGuestUser() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + underTest.onRecordSelected(UserRecord(info = userInfos.last())) + + verify(uiEventLogger, times(1)) + .log(MultiUserActionsEvent.SWITCH_TO_GUEST_FROM_USER_SWITCHER) + verify(activityManager).switchUser(userInfos.last().id) + Unit + } + } + + @Test + fun onRecordSelected_switchToRestrictedUser() { + createUserInteractor() + testScope.runTest { + var userInfos = createUserInfos(count = 2, includeGuest = false).toMutableList() + userInfos.add( + UserInfo( + 60, + "Restricted user", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_FULL, + UserManager.USER_TYPE_FULL_RESTRICTED, + ) + ) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + underTest.onRecordSelected(UserRecord(info = userInfos.last())) + + verify(uiEventLogger, times(1)) + .log(MultiUserActionsEvent.SWITCH_TO_RESTRICTED_USER_FROM_USER_SWITCHER) + verify(activityManager).switchUser(userInfos.last().id) + Unit + } + } + + @Test + fun onRecordSelected_enterGuestMode() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 3, 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) + + underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower) + runCurrent() + + verify(uiEventLogger, times(1)) + .log(MultiUserActionsEvent.CREATE_GUEST_FROM_USER_SWITCHER) + verify(dialogShower).dismiss() + verify(manager).createGuest(any()) + Unit + } + } + + @Test + fun onRecordSelected_action() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + underTest.onRecordSelected(UserRecord(isAddSupervisedUser = true), dialogShower) + + verify(uiEventLogger, times(1)) + .log(MultiUserActionsEvent.CREATE_RESTRICTED_USER_FROM_USER_SWITCHER) + verify(dialogShower, never()).dismiss() + verify(activityStarter).startActivity(any(), anyBoolean()) + } + } + + @Test + fun users_switcherEnabled() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + val value = collectLastValue(underTest.users) + + assertUsers(models = value(), count = 3, includeGuest = true) + } + } + + @Test + fun users_switchesToSecondUser() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + val value = collectLastValue(underTest.users) + userRepository.setSelectedUserInfo(userInfos[1]) + + assertUsers(models = value(), count = 2, selectedIndex = 1) + } + } + + @Test + fun users_switcherNotEnabled() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) + + val value = collectLastValue(underTest.users) + assertUsers(models = value(), count = 1) + } + } + + @Test + fun selectedUser() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + val value = collectLastValue(underTest.selectedUser) + assertUser(value(), id = 0, isSelected = true) + + userRepository.setSelectedUserInfo(userInfos[1]) + assertUser(value(), id = 1, isSelected = true) + } + } + + @Test + fun actions_deviceUnlocked() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + val value = collectLastValue(underTest.actions) + + runCurrent() + + assertThat(value()) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + } + } + + @Test + fun actions_deviceUnlocked_fullScreen() { + createUserInteractor() + testScope.runTest { + featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + val userInfos = createUserInfos(count = 2, includeGuest = false) + + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + val value = collectLastValue(underTest.actions) + + assertThat(value()) + .isEqualTo( + listOf( + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + } + } + + @Test + fun actions_deviceUnlockedUserNotPrimary_emptyList() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + val value = collectLastValue(underTest.actions) + + assertThat(value()).isEqualTo(emptyList()) + } + } + + @Test + fun actions_deviceUnlockedUserIsGuest_emptyList() { + createUserInteractor() + testScope.runTest { + 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) + val value = collectLastValue(underTest.actions) + + assertThat(value()).isEqualTo(emptyList()) + } + } + + @Test + fun actions_deviceLockedAddFromLockscreenSet_fullList() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings( + UserSwitcherSettingsModel( + isUserSwitcherEnabled = true, + isAddUsersFromLockscreen = true, + ) + ) + keyguardRepository.setKeyguardShowing(false) + val value = collectLastValue(underTest.actions) + + assertThat(value()) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + } + } + + @Test + fun actions_deviceLockedAddFromLockscreenSet_fullList_fullScreen() { + createUserInteractor() + testScope.runTest { + featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings( + UserSwitcherSettingsModel( + isUserSwitcherEnabled = true, + isAddUsersFromLockscreen = true, + ) + ) + keyguardRepository.setKeyguardShowing(false) + val value = collectLastValue(underTest.actions) + + assertThat(value()) + .isEqualTo( + listOf( + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + } + } + + @Test + fun actions_deviceLocked_onlymanageUserIsShown() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(true) + val value = collectLastValue(underTest.actions) + + assertThat(value()).isEqualTo(listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)) + } + } + + @Test + fun executeAction_addUser_dismissesDialogAndStartsActivity() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + underTest.executeAction(UserActionModel.ADD_USER) + verify(uiEventLogger, times(1)) + .log(MultiUserActionsEvent.CREATE_USER_FROM_USER_SWITCHER) + underTest.onDialogShown() + } + } + + @Test + fun executeAction_addSupervisedUser_dismissesDialogAndStartsActivity() { + createUserInteractor() + testScope.runTest { + underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) + + verify(uiEventLogger, times(1)) + .log(MultiUserActionsEvent.CREATE_RESTRICTED_USER_FROM_USER_SWITCHER) + val intentCaptor = kotlinArgumentCaptor() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(true)) + assertThat(intentCaptor.value.action) + .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER) + assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE) + } + } + + @Test + fun executeAction_navigateToManageUsers() { + createUserInteractor() + testScope.runTest { + underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + + val intentCaptor = kotlinArgumentCaptor() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(true)) + assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS) + } + } + + @Test + fun executeAction_guestMode() { + createUserInteractor() + testScope.runTest { + 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() + backgroundScope.launch { + underTest.dialogShowRequests.collect { + dialogRequests.add(it) + if (it != null) { + underTest.onDialogShown() + } + } + } + backgroundScope.launch { + underTest.dialogDismissRequests.collect { + if (it != null) { + underTest.onDialogDismissed() + } + } + } + + underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) + runCurrent() + + verify(uiEventLogger, times(1)) + .log(MultiUserActionsEvent.CREATE_GUEST_FROM_USER_SWITCHER) + assertThat(dialogRequests) + .contains( + ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true), + ) + verify(activityManager).switchUser(guestUserInfo.id) + } + } + + @Test + fun selectUser_alreadySelectedGuestReSelected_exitGuestDialog() { + createUserInteractor() + testScope.runTest { + 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)) + val dialogRequest = collectLastValue(underTest.dialogShowRequests) + + underTest.selectUser( + newlySelectedUserId = guestUserInfo.id, + dialogShower = dialogShower, + ) + + assertThat(dialogRequest()) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + verify(dialogShower, never()).dismiss() + } + } + + @Test + fun selectUser_currentlyGuestNonGuestSelected_exitGuestDialog() { + createUserInteractor() + testScope.runTest { + 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)) + val dialogRequest = collectLastValue(underTest.dialogShowRequests) + + underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower) + + assertThat(dialogRequest()) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + verify(dialogShower, never()).dismiss() + } + } + + @Test + fun selectUser_notCurrentlyGuest_switchesUsers() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val dialogRequest = collectLastValue(underTest.dialogShowRequests) + + underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower) + + assertThat(dialogRequest()).isNull() + verify(activityManager).switchUser(userInfos[1].id) + verify(dialogShower).dismiss() + } + } + + @Test + fun telephonyCallStateChanges_refreshesUsers() { + createUserInteractor() + testScope.runTest { + runCurrent() + + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + telephonyRepository.setCallState(1) + runCurrent() + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + } + + @Test + fun userSwitchedBroadcast() { + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + whenever(manager.aliveUsers).thenReturn(userInfos) + createUserInteractor() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val callback1: UserSwitcherInteractor.UserCallback = mock() + val callback2: UserSwitcherInteractor.UserCallback = mock() + underTest.addCallback(callback1) + underTest.addCallback(callback2) + runCurrent() + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + userRepository.setSelectedUserInfo(userInfos[1]) + runCurrent() + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + spyContext, + Intent(Intent.ACTION_USER_SWITCHED) + .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id), + ) + runCurrent() + + verify(callback1, atLeastOnce()).onUserStateChanged() + verify(callback2, atLeastOnce()).onUserStateChanged() + assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id) + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + verify(spyContext).startServiceAsUser(any(), eq(UserHandle.of(userInfos[1].id))) + } + } + + @Test + fun userInfoChangedBroadcast() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + runCurrent() + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + spyContext, + Intent(Intent.ACTION_USER_INFO_CHANGED), + ) + + runCurrent() + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + } + + @Test + fun systemUserUnlockedBroadcast_refreshUsers() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + runCurrent() + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + spyContext, + Intent(Intent.ACTION_USER_UNLOCKED) + .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM), + ) + runCurrent() + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + } + + @Test + fun localeChanged_refreshUsers() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + runCurrent() + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + spyContext, + Intent(Intent.ACTION_LOCALE_CHANGED) + ) + runCurrent() + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + } + + @Test + fun nonSystemUserUnlockedBroadcast_doNotRefreshUsers() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + spyContext, + Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337), + ) + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount) + } + } + + @Test + fun userRecords() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + runCurrent() + + assertRecords( + records = underTest.userRecords.value, + userIds = listOf(0, 1, 2), + selectedUserIndex = 0, + includeGuest = false, + expectedActions = + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ), + ) + } + } + + @Test + fun userRecordsFullScreen() { + createUserInteractor() + testScope.runTest { + featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + runCurrent() + + assertRecords( + records = underTest.userRecords.value, + userIds = listOf(0, 1, 2), + selectedUserIndex = 0, + includeGuest = false, + expectedActions = + listOf( + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ), + ) + } + } + + @Test + fun selectedUserRecord() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + assertRecordForUser( + record = underTest.selectedUserRecord.value, + id = 0, + hasPicture = true, + isCurrent = true, + isSwitchToEnabled = true, + ) + } + } + + @Test + fun users_secondaryUser_guestUserCanBeSwitchedTo() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + val res = collectLastValue(underTest.users) + assertThat(res()?.size == 3).isTrue() + assertThat(res()?.find { it.isGuest }).isNotNull() + } + } + + @Test + fun users_secondaryUser_noGuestAction() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + val res = collectLastValue(underTest.actions) + assertThat(res()?.find { it == UserActionModel.ENTER_GUEST_MODE }).isNull() + } + } + + @Test + fun users_secondaryUser_noGuestUserRecord() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + assertThat(underTest.userRecords.value.find { it.isGuest }).isNull() + } + } + + @Test + fun showUserSwitcher_fullScreenDisabled_showsDialogSwitcher() { + createUserInteractor() + testScope.runTest { + val expandable = mock() + underTest.showUserSwitcher(expandable) + + val dialogRequest = collectLastValue(underTest.dialogShowRequests) + + // Dialog is shown. + assertThat(dialogRequest()) + .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherDialog(expandable)) + + underTest.onDialogShown() + assertThat(dialogRequest()).isNull() + } + } + + @Test + fun showUserSwitcher_fullScreenEnabled_launchesFullScreenDialog() { + createUserInteractor() + testScope.runTest { + featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + + val expandable = mock() + underTest.showUserSwitcher(expandable) + + val dialogRequest = collectLastValue(underTest.dialogShowRequests) + + // Dialog is shown. + assertThat(dialogRequest()) + .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable)) + + underTest.onDialogShown() + assertThat(dialogRequest()).isNull() + } + } + + @Test + fun users_secondaryUser_managedProfileIsNotIncluded() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList() + userInfos.add( + UserInfo( + 50, + "Work Profile", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_MANAGED_PROFILE + ) + ) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + val res = collectLastValue(underTest.users) + assertThat(res()?.size == 3).isTrue() + } + } + + @Test + fun currentUserIsNotPrimaryAndUserSwitcherIsDisabled() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) + val selectedUser = collectLastValue(underTest.selectedUser) + assertThat(selectedUser()).isNotNull() + } + } + + @Test + fun userRecords_isActionAndNoUsersUnlocked_actionIsDisabled() { + createUserInteractor() + testScope.runTest { + keyguardRepository.setKeyguardShowing(true) + whenever(manager.getUserSwitchability(any())) + .thenReturn(UserManager.SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED) + val userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings( + UserSwitcherSettingsModel( + isUserSwitcherEnabled = true, + isAddUsersFromLockscreen = true + ) + ) + + runCurrent() + underTest.userRecords.value + .filter { it.info == null } + .forEach { action -> assertThat(action.isSwitchToEnabled).isFalse() } + } + } + + @Test + fun userRecords_isActionAndNoUsersUnlocked_actionIsDisabled_HeadlessMode() { + createUserInteractor() + testScope.runTest { + keyguardRepository.setKeyguardShowing(true) + whenever(headlessSystemUserMode.isHeadlessSystemUserMode()).thenReturn(true) + whenever(manager.isUserUnlocked(anyInt())).thenReturn(false) + val userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings( + UserSwitcherSettingsModel( + isUserSwitcherEnabled = true, + isAddUsersFromLockscreen = true + ) + ) + + runCurrent() + underTest.userRecords.value + .filter { it.info == null } + .forEach { action -> assertThat(action.isSwitchToEnabled).isFalse() } + } + } + + @Test + fun initWithNoAliveUsers() { + whenever(manager.aliveUsers).thenReturn(listOf()) + createUserInteractor() + verify(spyContext, never()).startServiceAsUser(any(), any()) + } + + 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 assertRecords( + records: List, + userIds: List, + selectedUserIndex: Int = 0, + includeGuest: Boolean = false, + expectedActions: List = emptyList(), + ) { + assertThat(records.size >= userIds.size).isTrue() + userIds.indices.forEach { userIndex -> + val record = records[userIndex] + assertThat(record.info).isNotNull() + val isGuest = includeGuest && userIndex == userIds.size - 1 + assertRecordForUser( + record = record, + id = userIds[userIndex], + hasPicture = !isGuest, + isCurrent = userIndex == selectedUserIndex, + isGuest = isGuest, + isSwitchToEnabled = true, + ) + } + + assertThat(records.size - userIds.size).isEqualTo(expectedActions.size) + (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex -> + val record = records[actionIndex] + assertThat(record.info).isNull() + assertRecordForAction( + record = record, + type = expectedActions[actionIndex - userIds.size], + ) + } + } + + private fun assertRecordForUser( + record: UserRecord?, + id: Int? = null, + hasPicture: Boolean = false, + isCurrent: Boolean = false, + isGuest: Boolean = false, + isSwitchToEnabled: Boolean = false, + ) { + checkNotNull(record) + assertThat(record.info?.id).isEqualTo(id) + assertThat(record.picture != null).isEqualTo(hasPicture) + assertThat(record.isCurrent).isEqualTo(isCurrent) + assertThat(record.isGuest).isEqualTo(isGuest) + assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled) + } + + 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) + } + + private fun createUserInteractor(startAsProcessUser: Boolean = true) { + val processUserId = Process.myUserHandle().identifier + val startUserId = if (startAsProcessUser) processUserId else (processUserId + 1) + runBlocking { + val userInfo = + createUserInfo(id = startUserId, name = "user_$startUserId", isPrimary = true) + userRepository.setUserInfos(listOf(userInfo)) + userRepository.setSelectedUserInfo(userInfo) + } + underTest = + UserSwitcherInteractor( + applicationContext = spyContext, + repository = userRepository, + activityStarter = activityStarter, + keyguardInteractor = keyguardReply.keyguardInteractor, + manager = manager, + headlessSystemUserMode = headlessSystemUserMode, + applicationScope = testScope.backgroundScope, + telephonyInteractor = + TelephonyInteractor( + repository = telephonyRepository, + ), + broadcastDispatcher = fakeBroadcastDispatcher, + keyguardUpdateMonitor = keyguardUpdateMonitor, + backgroundDispatcher = testDispatcher, + activityManager = activityManager, + refreshUsersScheduler = refreshUsersScheduler, + guestUserInteractor = + GuestUserInteractor( + applicationContext = spyContext, + applicationScope = testScope.backgroundScope, + mainDispatcher = testDispatcher, + backgroundDispatcher = testDispatcher, + manager = manager, + repository = userRepository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = refreshUsersScheduler, + uiEventLogger = uiEventLogger, + resumeSessionReceiver = resumeSessionReceiver, + resetOrExitSessionReceiver = resetOrExitSessionReceiver, + ), + uiEventLogger = uiEventLogger, + featureFlags = featureFlags, + userRestrictionChecker = mock(), + ) + } + + 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 or UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL + } else { + UserInfo.FLAG_FULL + }, + if (isGuest) { + UserManager.USER_TYPE_FULL_GUEST + } else { + UserManager.USER_TYPE_FULL_SYSTEM + }, + ) + } + + companion object { + private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + private val GUEST_ICON: Drawable = mock() + private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt index a8db368d4150..7041eab9d247 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt @@ -42,7 +42,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.UserInteractor +import com.android.systemui.user.domain.interactor.UserSwitcherInteractor import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -243,9 +243,8 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { userRepository.setSelectedUserInfo(USER_0) } return StatusBarUserChipViewModel( - context = context, interactor = - UserInteractor( + UserSwitcherInteractor( applicationContext = context, repository = userRepository, activityStarter = activityStarter, 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 c236b12d723f..686f492fde50 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 @@ -42,7 +42,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.UserInteractor +import com.android.systemui.user.domain.interactor.UserSwitcherInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.util.mockito.any @@ -157,8 +157,8 @@ class UserSwitcherViewModelTest : SysuiTestCase() { underTest = UserSwitcherViewModel( - userInteractor = - UserInteractor( + userSwitcherInteractor = + UserSwitcherInteractor( applicationContext = context, repository = userRepository, activityStarter = activityStarter, diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index c8327029026d..c8d1334b2be8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -156,7 +156,7 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository; -import com.android.systemui.user.domain.interactor.UserInteractor; +import com.android.systemui.user.domain.interactor.UserSwitcherInteractor; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.bubbles.Bubble; @@ -450,7 +450,7 @@ public class BubblesTest extends SysuiTestCase { keyguardTransitionInteractor, powerInteractor, new FakeUserSetupRepository(), - mock(UserInteractor.class), + mock(UserSwitcherInteractor.class), new SharedNotificationContainerInteractor( configurationRepository, mContext, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt index 8e96b522e997..7cac01925b12 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt @@ -16,11 +16,8 @@ package com.android.systemui.keyguard.domain.interactor -import android.app.ActivityManager import android.content.Context import android.os.Handler -import android.os.UserManager -import com.android.internal.logging.UiEventLogger import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository @@ -28,7 +25,6 @@ import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.ui.BouncerView -import com.android.systemui.broadcast.FakeBroadcastDispatcher import com.android.systemui.classifier.FalsingCollector import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags @@ -36,21 +32,13 @@ import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeTrustRepository -import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.data.repository.FakePowerRepository import com.android.systemui.power.domain.interactor.PowerInteractorFactory import com.android.systemui.statusbar.policy.KeyguardStateController -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.HeadlessSystemUserMode -import com.android.systemui.user.domain.interactor.RefreshUsersScheduler -import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.time.FakeSystemClock -import com.android.systemui.utils.UserRestrictionChecker -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.test.TestScope import org.mockito.Mockito.mock @@ -64,8 +52,6 @@ object KeyguardDismissInteractorFactory { fun create( context: Context, testScope: TestScope, - broadcastDispatcher: FakeBroadcastDispatcher, - dispatcher: CoroutineDispatcher, trustRepository: FakeTrustRepository = FakeTrustRepository(), keyguardRepository: FakeKeyguardRepository = FakeKeyguardRepository(), bouncerRepository: FakeKeyguardBouncerRepository = FakeKeyguardBouncerRepository(), @@ -103,37 +89,12 @@ object KeyguardDismissInteractorFactory { keyguardUpdateMonitor, ) val powerInteractorWithDeps = - PowerInteractorFactory.create( - repository = powerRepository, - ) - val userInteractor = - UserInteractor( - applicationContext = context, + PowerInteractorFactory.create( + repository = powerRepository, + ) + val selectedUserInteractor = + SelectedUserInteractor( repository = userRepository, - mock(ActivityStarter::class.java), - keyguardInteractor = - KeyguardInteractorFactory.create( - repository = keyguardRepository, - bouncerRepository = bouncerRepository, - featureFlags = featureFlags, - ) - .keyguardInteractor, - featureFlags = featureFlags, - manager = mock(UserManager::class.java), - headlessSystemUserMode = mock(HeadlessSystemUserMode::class.java), - applicationScope = testScope.backgroundScope, - telephonyInteractor = - TelephonyInteractor( - repository = FakeTelephonyRepository(), - ), - broadcastDispatcher = broadcastDispatcher, - keyguardUpdateMonitor = keyguardUpdateMonitor, - backgroundDispatcher = dispatcher, - activityManager = mock(ActivityManager::class.java), - refreshUsersScheduler = mock(RefreshUsersScheduler::class.java), - guestUserInteractor = mock(GuestUserInteractor::class.java), - uiEventLogger = mock(UiEventLogger::class.java), - userRestrictionChecker = mock(UserRestrictionChecker::class.java), ) return WithDependencies( trustRepository = trustRepository, @@ -149,7 +110,7 @@ object KeyguardDismissInteractorFactory { primaryBouncerInteractor, alternateBouncerInteractor, powerInteractorWithDeps.powerInteractor, - userInteractor, + selectedUserInteractor, ), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt index 911eafae5c27..7a99bd0d54cf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt @@ -51,7 +51,7 @@ import com.android.systemui.statusbar.policy.UserInfoController import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.user.data.repository.UserSwitcherRepository import com.android.systemui.user.data.repository.UserSwitcherRepositoryImpl -import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.domain.interactor.UserSwitcherInteractor import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.FakeGlobalSettings import com.android.systemui.util.settings.GlobalSettings @@ -102,7 +102,7 @@ class FooterActionsTestUtils( deviceProvisionedController: DeviceProvisionedController = mock(), qsSecurityFooterUtils: QSSecurityFooterUtils = mock(), fgsManagerController: FgsManagerController = mock(), - userInteractor: UserInteractor = mock(), + userSwitcherInteractor: UserSwitcherInteractor = mock(), securityRepository: SecurityRepository = securityRepository(), foregroundServicesRepository: ForegroundServicesRepository = foregroundServicesRepository(), userSwitcherRepository: UserSwitcherRepository = userSwitcherRepository(), @@ -116,7 +116,7 @@ class FooterActionsTestUtils( deviceProvisionedController, qsSecurityFooterUtils, fgsManagerController, - userInteractor, + userSwitcherInteractor, securityRepository, foregroundServicesRepository, userSwitcherRepository, -- cgit v1.2.3-59-g8ed1b