diff options
| author | 2022-10-04 22:19:28 +0000 | |
|---|---|---|
| committer | 2022-10-04 22:19:28 +0000 | |
| commit | 6e543ea69dc51a21c0e6d96c2f87d0c3785dbced (patch) | |
| tree | d8bd076af1fb5ac857441f9e15ac13cb7128034b | |
| parent | a60e9c21a3bd0d116f80a805f626d3f6021722a8 (diff) | |
| parent | 3cd2429e55b995d140d696788465cdf4d8de440d (diff) | |
Merge changes from topics "usc-less-domain", "usc-less-split-flag", "usc-less-ui" into tm-qpr-dev am: 3cd2429e55
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20064881
Change-Id: I0dc00354e9a0cffe33a10f96ac26b191eb18f111
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
46 files changed, 4363 insertions, 687 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 443d2774f0e0..06dbab980793 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -81,6 +81,7 @@ import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule; import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule; import com.android.systemui.statusbar.window.StatusBarWindowModule; +import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule; import com.android.systemui.tuner.dagger.TunerModule; import com.android.systemui.unfold.SysUIUnfoldModule; import com.android.systemui.user.UserModule; @@ -145,6 +146,7 @@ import dagger.Provides; StatusBarWindowModule.class, SysUIConcurrencyModule.class, SysUIUnfoldModule.class, + TelephonyRepositoryModule.class, TunerModule.class, UserModule.class, UtilModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index 96a43aa4b2e8..43742a8a3696 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -104,9 +104,26 @@ public class Flags { public static final ReleasedFlag MODERN_USER_SWITCHER_ACTIVITY = new ReleasedFlag(209, true); - /** Whether the new implementation of UserSwitcherController should be used. */ - public static final UnreleasedFlag REFACTORED_USER_SWITCHER_CONTROLLER = - new UnreleasedFlag(210, false); + /** + * Whether the user interactor and repository should use `UserSwitcherController`. + * + * <p>If this is {@code false}, the interactor and repo skip the controller and directly access + * the framework APIs. + */ + public static final UnreleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER = + new UnreleasedFlag(210, true); + + /** + * Whether `UserSwitcherController` should use the user interactor. + * + * <p>When this is {@code true}, the controller does not directly access framework APIs. + * Instead, it goes through the interactor. + * + * <p>Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is + * {@code true} as it would created a cycle between controller -> interactor -> controller. + */ + public static final UnreleasedFlag USER_CONTROLLER_USES_INTERACTOR = + new UnreleasedFlag(211, false); /***************************************/ // 300 - power menu diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 840a4b20a3f0..4c4b588888d1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -85,6 +85,15 @@ interface KeyguardRepository { */ val dozeAmount: Flow<Float> + /** + * Returns `true` if the keyguard is showing; `false` otherwise. + * + * Note: this is also `true` when the lock-screen is occluded with an `Activity` "above" it in + * the z-order (which is not really above the system UI window, but rather - the lock-screen + * becomes invisible to reveal the "occluding activity"). + */ + fun isKeyguardShowing(): Boolean + /** Sets whether the bottom area UI should animate the transition out of doze state. */ fun setAnimateDozingTransitions(animate: Boolean) @@ -103,7 +112,7 @@ class KeyguardRepositoryImpl @Inject constructor( statusBarStateController: StatusBarStateController, - keyguardStateController: KeyguardStateController, + private val keyguardStateController: KeyguardStateController, dozeHost: DozeHost, ) : KeyguardRepository { private val _animateBottomAreaDozingTransitions = MutableStateFlow(false) @@ -168,6 +177,10 @@ constructor( awaitClose { statusBarStateController.removeCallback(callback) } } + override fun isKeyguardShowing(): Boolean { + return keyguardStateController.isShowing + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.value = animate } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index dccc94178ed5..192919e32cf6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.Flow class KeyguardInteractor @Inject constructor( - repository: KeyguardRepository, + private val repository: KeyguardRepository, ) { /** * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at @@ -40,4 +40,8 @@ constructor( val isDozing: Flow<Boolean> = repository.isDozing /** Whether the keyguard is showing ot not. */ val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing + + fun isKeyguardShowing(): Boolean { + return repository.isKeyguardShowing() + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java index 97476b2d1cde..d2d5063c7ae0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java @@ -134,7 +134,7 @@ public class UserDetailView extends PseudoGridView { v.bind(name, drawable, item.info.id); } v.setActivated(item.isCurrent); - v.setDisabledByAdmin(mController.isDisabledByAdmin(item)); + v.setDisabledByAdmin(item.isDisabledByAdmin()); v.setEnabled(item.isSwitchToEnabled); UserSwitcherController.setSelectableAlpha(v); @@ -173,16 +173,16 @@ public class UserDetailView extends PseudoGridView { Trace.beginSection("UserDetailView.Adapter#onClick"); UserRecord userRecord = (UserRecord) view.getTag(); - if (mController.isDisabledByAdmin(userRecord)) { + if (userRecord.isDisabledByAdmin()) { final Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent( - mContext, mController.getEnforcedAdmin(userRecord)); + mContext, userRecord.enforcedAdmin); mController.startActivity(intent); } else if (userRecord.isSwitchToEnabled) { MetricsLogger.action(mContext, MetricsEvent.QS_SWITCH_USER); mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH); if (!userRecord.isAddUser && !userRecord.isRestricted - && !mController.isDisabledByAdmin(userRecord)) { + && !userRecord.isDisabledByAdmin()) { if (mCurrentUserView != null) { mCurrentUserView.setActivated(false); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java index 0995a00533a8..712953e14d60 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java @@ -505,7 +505,7 @@ public class KeyguardUserSwitcherController extends ViewController<KeyguardUserS v.bind(name, drawable, item.info.id); } v.setActivated(item.isCurrent); - v.setDisabledByAdmin(getController().isDisabledByAdmin(item)); + v.setDisabledByAdmin(item.isDisabledByAdmin()); v.setEnabled(item.isSwitchToEnabled); UserSwitcherController.setSelectableAlpha(v); 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 843c2329092c..146b222c94ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.policy import android.annotation.UserIdInt import android.content.Intent import android.view.View -import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin import com.android.systemui.Dumpable import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower import com.android.systemui.user.data.source.UserRecord @@ -130,12 +129,6 @@ interface UserSwitcherController : Dumpable { /** Whether keyguard is showing. */ val isKeyguardShowing: Boolean - /** Returns the [EnforcedAdmin] for the given record, or `null` if there isn't one. */ - fun getEnforcedAdmin(record: UserRecord): EnforcedAdmin? - - /** Returns `true` if the given record is disabled by the admin; `false` otherwise. */ - fun isDisabledByAdmin(record: UserRecord): Boolean - /** Starts an activity with the given [Intent]. */ fun startActivity(intent: Intent) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt index 12834f68c3b7..4932a6544d05 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.policy import android.content.Intent import android.view.View -import com.android.settingslib.RestrictedLockUtils import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.qs.user.UserSwitchDialogController @@ -38,8 +37,9 @@ constructor( @Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>, ) : UserSwitcherController { - private val isNewImpl: Boolean - get() = flags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER) + private val useInteractor: Boolean = + flags.isEnabled(Flags.USER_CONTROLLER_USES_INTERACTOR) && + !flags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) private val _oldImpl: UserSwitcherControllerOldImpl get() = oldImpl.get() @@ -49,7 +49,7 @@ constructor( override val users: ArrayList<UserRecord> get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.users @@ -57,14 +57,14 @@ constructor( override val isSimpleUserSwitcher: Boolean get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.isSimpleUserSwitcher } override fun init(view: View) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.init(view) @@ -73,7 +73,7 @@ constructor( override val currentUserRecord: UserRecord? get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.currentUserRecord @@ -81,7 +81,7 @@ constructor( override val currentUserName: String? get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.currentUserName @@ -91,7 +91,7 @@ constructor( userId: Int, dialogShower: UserSwitchDialogController.DialogShower? ) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.onUserSelected(userId, dialogShower) @@ -100,7 +100,7 @@ constructor( override val isAddUsersFromLockScreenEnabled: Flow<Boolean> get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.isAddUsersFromLockScreenEnabled @@ -108,7 +108,7 @@ constructor( override val isGuestUserAutoCreated: Boolean get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.isGuestUserAutoCreated @@ -116,7 +116,7 @@ constructor( override val isGuestUserResetting: Boolean get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.isGuestUserResetting @@ -125,7 +125,7 @@ constructor( override fun createAndSwitchToGuestUser( dialogShower: UserSwitchDialogController.DialogShower?, ) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.createAndSwitchToGuestUser(dialogShower) @@ -133,7 +133,7 @@ constructor( } override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.showAddUserDialog(dialogShower) @@ -141,7 +141,7 @@ constructor( } override fun startSupervisedUserActivity() { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.startSupervisedUserActivity() @@ -149,7 +149,7 @@ constructor( } override fun onDensityOrFontScaleChanged() { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.onDensityOrFontScaleChanged() @@ -157,7 +157,7 @@ constructor( } override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.addAdapter(adapter) @@ -168,7 +168,7 @@ constructor( record: UserRecord, dialogShower: UserSwitchDialogController.DialogShower?, ) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.onUserListItemClicked(record, dialogShower) @@ -176,7 +176,7 @@ constructor( } override fun removeGuestUser(guestUserId: Int, targetUserId: Int) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.removeGuestUser(guestUserId, targetUserId) @@ -188,7 +188,7 @@ constructor( targetUserId: Int, forceRemoveGuestOnExit: Boolean ) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) @@ -196,7 +196,7 @@ constructor( } override fun schedulePostBootGuestCreation() { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.schedulePostBootGuestCreation() @@ -205,30 +205,14 @@ constructor( override val isKeyguardShowing: Boolean get() = - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.isKeyguardShowing } - override fun getEnforcedAdmin(record: UserRecord): RestrictedLockUtils.EnforcedAdmin? { - return if (isNewImpl) { - notYetImplemented() - } else { - _oldImpl.getEnforcedAdmin(record) - } - } - - override fun isDisabledByAdmin(record: UserRecord): Boolean { - return if (isNewImpl) { - notYetImplemented() - } else { - _oldImpl.isDisabledByAdmin(record) - } - } - override fun startActivity(intent: Intent) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.startActivity(intent) @@ -236,7 +220,7 @@ constructor( } override fun refreshUsers(forcePictureLoadForId: Int) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.refreshUsers(forcePictureLoadForId) @@ -244,7 +228,7 @@ constructor( } override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.addUserSwitchCallback(callback) @@ -252,7 +236,7 @@ constructor( } override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.removeUserSwitchCallback(callback) @@ -260,7 +244,7 @@ constructor( } override fun dump(pw: PrintWriter, args: Array<out String>) { - if (isNewImpl) { + if (useInteractor) { notYetImplemented() } else { _oldImpl.dump(pw, args) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java index d365aa6f952d..46d2f3ac9ce4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java @@ -17,17 +17,13 @@ package com.android.systemui.statusbar.policy; import static android.os.UserManager.SWITCHABILITY_STATUS_OK; -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; - import android.annotation.UserIdInt; -import android.app.ActivityManager; import android.app.AlertDialog; import android.app.Dialog; import android.app.IActivityManager; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.UserInfo; @@ -40,7 +36,6 @@ import android.os.UserManager; import android.provider.Settings; import android.telephony.TelephonyCallback; import android.text.TextUtils; -import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -49,17 +44,14 @@ import android.view.WindowManagerGlobal; import android.widget.Toast; import androidx.annotation.Nullable; -import androidx.collection.SimpleArrayMap; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.internal.util.LatencyTracker; -import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.users.UserCreatingDialog; import com.android.systemui.GuestResetOrExitSessionReceiver; import com.android.systemui.GuestResumeSessionReceiver; -import com.android.systemui.R; import com.android.systemui.SystemUISecondaryUserService; import com.android.systemui.animation.DialogCuj; import com.android.systemui.animation.DialogLaunchAnimator; @@ -75,10 +67,12 @@ import com.android.systemui.plugins.FalsingManager; import com.android.systemui.qs.QSUserSwitcherEvent; import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower; import com.android.systemui.settings.UserTracker; -import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.telephony.TelephonyListenerManager; -import com.android.systemui.user.CreateUserActivity; import com.android.systemui.user.data.source.UserRecord; +import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper; +import com.android.systemui.user.shared.model.UserActionModel; +import com.android.systemui.user.ui.dialog.AddUserDialog; +import com.android.systemui.user.ui.dialog.ExitGuestDialog; import com.android.systemui.util.settings.GlobalSettings; import com.android.systemui.util.settings.SecureSettings; @@ -139,9 +133,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { private final InteractionJankMonitor mInteractionJankMonitor; private final LatencyTracker mLatencyTracker; private final DialogLaunchAnimator mDialogLaunchAnimator; - private final SimpleArrayMap<UserRecord, EnforcedAdmin> mEnforcedAdminByUserRecord = - new SimpleArrayMap<>(); - private final ArraySet<UserRecord> mDisabledByAdmin = new ArraySet<>(); private ArrayList<UserRecord> mUsers = new ArrayList<>(); @VisibleForTesting @@ -334,7 +325,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { for (UserInfo info : infos) { boolean isCurrent = currentId == info.id; - boolean switchToEnabled = canSwitchUsers || isCurrent; if (!mUserSwitcherEnabled && !info.isPrimary()) { continue; } @@ -343,25 +333,22 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { if (info.isGuest()) { // Tapping guest icon triggers remove and a user switch therefore // the icon shouldn't be enabled even if the user is current - guestRecord = new UserRecord(info, null /* picture */, - true /* isGuest */, isCurrent, false /* isAddUser */, - false /* isRestricted */, canSwitchUsers, - false /* isAddSupervisedUser */); + guestRecord = LegacyUserDataHelper.createRecord( + mContext, + mUserManager, + null /* picture */, + info, + isCurrent, + canSwitchUsers); } else if (info.supportsSwitchToByUser()) { - Bitmap picture = bitmaps.get(info.id); - if (picture == null) { - picture = mUserManager.getUserIcon(info.id); - - if (picture != null) { - int avatarSize = mContext.getResources() - .getDimensionPixelSize(R.dimen.max_avatar_size); - picture = Bitmap.createScaledBitmap( - picture, avatarSize, avatarSize, true); - } - } - records.add(new UserRecord(info, picture, false /* isGuest */, - isCurrent, false /* isAddUser */, false /* isRestricted */, - switchToEnabled, false /* isAddSupervisedUser */)); + records.add( + LegacyUserDataHelper.createRecord( + mContext, + mUserManager, + bitmaps.get(info.id), + info, + isCurrent, + canSwitchUsers)); } } } @@ -372,18 +359,20 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { // we will just use it as an indicator for "Resetting guest...". // Otherwise, default to canSwitchUsers. boolean isSwitchToGuestEnabled = !mGuestIsResetting.get() && canSwitchUsers; - guestRecord = new UserRecord(null /* info */, null /* picture */, - true /* isGuest */, false /* isCurrent */, - false /* isAddUser */, false /* isRestricted */, - isSwitchToGuestEnabled, false /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(guestRecord); + guestRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ENTER_GUEST_MODE, + false /* isRestricted */, + isSwitchToGuestEnabled); records.add(guestRecord); } else if (canCreateGuest(guestRecord != null)) { - guestRecord = new UserRecord(null /* info */, null /* picture */, - true /* isGuest */, false /* isCurrent */, - false /* isAddUser */, createIsRestricted(), canSwitchUsers, - false /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(guestRecord); + guestRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ENTER_GUEST_MODE, + false /* isRestricted */, + canSwitchUsers); records.add(guestRecord); } } else { @@ -391,20 +380,23 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { } if (canCreateUser()) { - UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */, - false /* isGuest */, false /* isCurrent */, true /* isAddUser */, - createIsRestricted(), canSwitchUsers, - false /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(addUserRecord); - records.add(addUserRecord); + final UserRecord userRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ADD_USER, + createIsRestricted(), + canSwitchUsers); + records.add(userRecord); } if (canCreateSupervisedUser()) { - UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */, - false /* isGuest */, false /* isCurrent */, false /* isAddUser */, - createIsRestricted(), canSwitchUsers, true /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(addUserRecord); - records.add(addUserRecord); + final UserRecord userRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ADD_SUPERVISED_USER, + createIsRestricted(), + canSwitchUsers); + records.add(userRecord); } mUiExecutor.execute(() -> { @@ -591,12 +583,23 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { showExitGuestDialog(id, isGuestEphemeral, newId, dialogShower); } - private void showExitGuestDialog(int id, boolean isGuestEphemeral, - int targetId, DialogShower dialogShower) { + private void showExitGuestDialog( + int id, + boolean isGuestEphemeral, + int targetId, + DialogShower dialogShower) { if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) { mExitGuestDialog.cancel(); } - mExitGuestDialog = new ExitGuestDialog(mContext, id, isGuestEphemeral, targetId); + mExitGuestDialog = new ExitGuestDialog( + mContext, + id, + isGuestEphemeral, + targetId, + mKeyguardStateController.isShowing(), + mFalsingManager, + mDialogLaunchAnimator, + this::exitGuestUser); if (dialogShower != null) { dialogShower.showDialog(mExitGuestDialog, new DialogCuj( InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, @@ -622,7 +625,15 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { if (mAddUserDialog != null && mAddUserDialog.isShowing()) { mAddUserDialog.cancel(); } - mAddUserDialog = new AddUserDialog(mContext); + final UserInfo currentUser = mUserTracker.getUserInfo(); + mAddUserDialog = new AddUserDialog( + mContext, + currentUser.getUserHandle(), + mKeyguardStateController.isShowing(), + /* showEphemeralMessage= */currentUser.isGuest() && currentUser.isEphemeral(), + mFalsingManager, + mBroadcastSender, + mDialogLaunchAnimator); if (dialogShower != null) { dialogShower.showDialog(mAddUserDialog, new DialogCuj( @@ -964,30 +975,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { return mKeyguardStateController.isShowing(); } - @Override - @Nullable - public EnforcedAdmin getEnforcedAdmin(UserRecord record) { - return mEnforcedAdminByUserRecord.get(record); - } - - @Override - public boolean isDisabledByAdmin(UserRecord record) { - return mDisabledByAdmin.contains(record); - } - - private void checkIfAddUserDisallowedByAdminOnly(UserRecord record) { - EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, - UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId()); - if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, - UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId())) { - mDisabledByAdmin.add(record); - mEnforcedAdminByUserRecord.put(record, admin); - } else { - mDisabledByAdmin.remove(record); - mEnforcedAdminByUserRecord.put(record, null); - } - } - private boolean shouldUseSimpleUserSwitcher() { int defaultSimpleUserSwitcher = mContext.getResources().getBoolean( com.android.internal.R.bool.config_expandLockScreenUserSwitcher) ? 1 : 0; @@ -1052,133 +1039,4 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { } } }; - - - private final class ExitGuestDialog extends SystemUIDialog implements - DialogInterface.OnClickListener { - - private final int mGuestId; - private final int mTargetId; - private final boolean mIsGuestEphemeral; - - ExitGuestDialog(Context context, int guestId, boolean isGuestEphemeral, - int targetId) { - super(context); - if (isGuestEphemeral) { - setTitle(context.getString( - com.android.settingslib.R.string.guest_exit_dialog_title)); - setMessage(context.getString( - com.android.settingslib.R.string.guest_exit_dialog_message)); - setButton(DialogInterface.BUTTON_NEUTRAL, - context.getString(android.R.string.cancel), this); - setButton(DialogInterface.BUTTON_POSITIVE, - context.getString( - com.android.settingslib.R.string.guest_exit_dialog_button), this); - } else { - setTitle(context.getString( - com.android.settingslib - .R.string.guest_exit_dialog_title_non_ephemeral)); - setMessage(context.getString( - com.android.settingslib - .R.string.guest_exit_dialog_message_non_ephemeral)); - setButton(DialogInterface.BUTTON_NEUTRAL, - context.getString(android.R.string.cancel), this); - setButton(DialogInterface.BUTTON_NEGATIVE, - context.getString( - com.android.settingslib.R.string.guest_exit_clear_data_button), - this); - setButton(DialogInterface.BUTTON_POSITIVE, - context.getString( - com.android.settingslib.R.string.guest_exit_save_data_button), - this); - } - SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing()); - setCanceledOnTouchOutside(false); - mGuestId = guestId; - mTargetId = targetId; - mIsGuestEphemeral = isGuestEphemeral; - } - - @Override - public void onClick(DialogInterface dialog, int which) { - int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY - : FalsingManager.HIGH_PENALTY; - if (mFalsingManager.isFalseTap(penalty)) { - return; - } - if (mIsGuestEphemeral) { - if (which == DialogInterface.BUTTON_POSITIVE) { - mDialogLaunchAnimator.dismissStack(this); - // Ephemeral guest: exit guest, guest is removed by the system - // on exit, since its marked ephemeral - exitGuestUser(mGuestId, mTargetId, false); - } else if (which == DialogInterface.BUTTON_NEGATIVE) { - // Cancel clicked, do nothing - cancel(); - } - } else { - if (which == DialogInterface.BUTTON_POSITIVE) { - mDialogLaunchAnimator.dismissStack(this); - // Non-ephemeral guest: exit guest, guest is not removed by the system - // on exit, since its marked non-ephemeral - exitGuestUser(mGuestId, mTargetId, false); - } else if (which == DialogInterface.BUTTON_NEGATIVE) { - mDialogLaunchAnimator.dismissStack(this); - // Non-ephemeral guest: remove guest and then exit - exitGuestUser(mGuestId, mTargetId, true); - } else if (which == DialogInterface.BUTTON_NEUTRAL) { - // Cancel clicked, do nothing - cancel(); - } - } - } - } - - @VisibleForTesting - final class AddUserDialog extends SystemUIDialog implements - DialogInterface.OnClickListener { - - AddUserDialog(Context context) { - super(context); - - setTitle(com.android.settingslib.R.string.user_add_user_title); - String message = context.getString( - com.android.settingslib.R.string.user_add_user_message_short); - UserInfo currentUser = mUserTracker.getUserInfo(); - if (currentUser != null && currentUser.isGuest() && currentUser.isEphemeral()) { - message += context.getString(R.string.user_add_user_message_guest_remove); - } - setMessage(message); - setButton(DialogInterface.BUTTON_NEUTRAL, - context.getString(android.R.string.cancel), this); - setButton(DialogInterface.BUTTON_POSITIVE, - context.getString(android.R.string.ok), this); - SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing()); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY - : FalsingManager.MODERATE_PENALTY; - if (mFalsingManager.isFalseTap(penalty)) { - return; - } - if (which == BUTTON_NEUTRAL) { - cancel(); - } else { - mDialogLaunchAnimator.dismissStack(this); - if (ActivityManager.isUserAMonkey()) { - return; - } - // Use broadcast instead of ShadeController, as this dialog may have started in - // another process and normal dagger bindings are not available - mBroadcastSender.sendBroadcastAsUser( - new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), UserHandle.CURRENT); - getContext().startActivityAsUser( - CreateUserActivity.createIntentForStart(getContext()), - mUserTracker.getUserHandle()); - } - } - } - } diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt new file mode 100644 index 000000000000..9c38dc0f8852 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt @@ -0,0 +1,56 @@ +/* + * 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.telephony.data.repository + +import android.telephony.Annotation +import android.telephony.TelephonyCallback +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.telephony.TelephonyListenerManager +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** Defines interface for classes that encapsulate _some_ telephony-related state. */ +interface TelephonyRepository { + /** The state of the current call. */ + @Annotation.CallState val callState: Flow<Int> +} + +/** + * NOTE: This repository tracks only telephony-related state regarding the default mobile + * subscription. `TelephonyListenerManager` does not create new instances of `TelephonyManager` on a + * per-subscription basis and thus will always be tracking telephony information regarding + * `SubscriptionManager.getDefaultSubscriptionId`. See `TelephonyManager` and `SubscriptionManager` + * for more documentation. + */ +@SysUISingleton +class TelephonyRepositoryImpl +@Inject +constructor( + private val manager: TelephonyListenerManager, +) : TelephonyRepository { + @Annotation.CallState + override val callState: Flow<Int> = conflatedCallbackFlow { + val listener = TelephonyCallback.CallStateListener { state -> trySend(state) } + + manager.addCallStateListener(listener) + + awaitClose { manager.removeCallStateListener(listener) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt new file mode 100644 index 000000000000..630fbf2d1a07 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt @@ -0,0 +1,26 @@ +/* + * 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.telephony.data.repository + +import dagger.Binds +import dagger.Module + +@Module +interface TelephonyRepositoryModule { + @Binds fun repository(impl: TelephonyRepositoryImpl): TelephonyRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt new file mode 100644 index 000000000000..86ca33df24dd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt @@ -0,0 +1,34 @@ +/* + * 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.telephony.domain.interactor + +import android.telephony.Annotation +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.telephony.data.repository.TelephonyRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** Hosts business logic related to telephony. */ +@SysUISingleton +class TelephonyInteractor +@Inject +constructor( + repository: TelephonyRepository, +) { + @Annotation.CallState val callState: Flow<Int> = repository.callState +} diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java index 5b522dcc4885..0c72b78a3c46 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java +++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java @@ -20,6 +20,7 @@ import android.app.Activity; import com.android.settingslib.users.EditUserInfoController; import com.android.systemui.user.data.repository.UserRepositoryModule; +import com.android.systemui.user.ui.dialog.UserDialogModule; import dagger.Binds; import dagger.Module; @@ -32,6 +33,7 @@ import dagger.multibindings.IntoMap; */ @Module( includes = { + UserDialogModule.class, UserRepositoryModule.class, } ) diff --git a/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt new file mode 100644 index 000000000000..4fd55c0e21c8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt @@ -0,0 +1,25 @@ +/* + * 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.data.model + +/** Encapsulates the state of settings related to user switching. */ +data class UserSwitcherSettingsModel( + val isSimpleUserSwitcher: Boolean = false, + val isAddUsersFromLockscreen: Boolean = false, + val isUserSwitcherEnabled: Boolean = false, +) diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index 035638800f9c..3014f39c17f8 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -18,9 +18,13 @@ package com.android.systemui.user.data.repository import android.content.Context +import android.content.pm.UserInfo import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable +import android.os.UserHandle import android.os.UserManager +import android.provider.Settings +import androidx.annotation.VisibleForTesting import androidx.appcompat.content.res.AppCompatResources import com.android.internal.util.UserIcons import com.android.systemui.R @@ -29,15 +33,36 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall 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.dagger.qualifiers.Main +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.source.UserRecord 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 com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Acts as source of truth for user related data. @@ -55,6 +80,18 @@ interface UserRepository { /** List of available user-related actions. */ val actions: Flow<List<UserActionModel>> + /** User switcher related settings. */ + val userSwitcherSettings: Flow<UserSwitcherSettingsModel> + + /** List of all users on the device. */ + val userInfos: Flow<List<UserInfo>> + + /** [UserInfo] of the currently-selected user. */ + val selectedUserInfo: Flow<UserInfo> + + /** User ID of the last non-guest selected user. */ + val lastSelectedNonGuestUserId: Int + /** Whether actions are available even when locked. */ val isActionableWhenLocked: Flow<Boolean> @@ -62,7 +99,23 @@ interface UserRepository { val isGuestUserAutoCreated: Boolean /** Whether the guest user is currently being reset. */ - val isGuestUserResetting: Boolean + var isGuestUserResetting: Boolean + + /** Whether we've scheduled the creation of a guest user. */ + val isGuestUserCreationScheduled: AtomicBoolean + + /** The user of the secondary service. */ + var secondaryUserId: Int + + /** Whether refresh users should be paused. */ + var isRefreshUsersPaused: Boolean + + /** Asynchronously refresh the list of users. This will cause [userInfos] to be updated. */ + fun refreshUsers() + + fun getSelectedUserInfo(): UserInfo + + fun isSimpleUserSwitcher(): Boolean } @SysUISingleton @@ -71,9 +124,31 @@ class UserRepositoryImpl constructor( @Application private val appContext: Context, private val manager: UserManager, - controller: UserSwitcherController, + private val controller: UserSwitcherController, + @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val globalSettings: GlobalSettings, + private val tracker: UserTracker, + private val featureFlags: FeatureFlags, ) : UserRepository { + private val isNewImpl: Boolean + get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) + + private val _userSwitcherSettings = MutableStateFlow<UserSwitcherSettingsModel?>(null) + override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> = + _userSwitcherSettings.asStateFlow().filterNotNull() + + private val _userInfos = MutableStateFlow<List<UserInfo>?>(null) + override val userInfos: Flow<List<UserInfo>> = _userInfos.filterNotNull() + + private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null) + override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull() + + override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM + private set + private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow { fun send() { trySendWithFailureLogging( @@ -99,11 +174,148 @@ constructor( override val actions: Flow<List<UserActionModel>> = userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } } - override val isActionableWhenLocked: Flow<Boolean> = controller.isAddUsersFromLockScreenEnabled + override val isActionableWhenLocked: Flow<Boolean> = + if (isNewImpl) { + emptyFlow() + } else { + controller.isAddUsersFromLockScreenEnabled + } + + override val isGuestUserAutoCreated: Boolean = + if (isNewImpl) { + appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated) + } else { + controller.isGuestUserAutoCreated + } + + private var _isGuestUserResetting: Boolean = false + override var isGuestUserResetting: Boolean = + if (isNewImpl) { + _isGuestUserResetting + } else { + controller.isGuestUserResetting + } + set(value) = + if (isNewImpl) { + _isGuestUserResetting = value + } else { + error("Not supported in the old implementation!") + } + + override val isGuestUserCreationScheduled = AtomicBoolean() + + override var secondaryUserId: Int = UserHandle.USER_NULL - override val isGuestUserAutoCreated: Boolean = controller.isGuestUserAutoCreated + override var isRefreshUsersPaused: Boolean = false - override val isGuestUserResetting: Boolean = controller.isGuestUserResetting + init { + if (isNewImpl) { + observeSelectedUser() + observeUserSettings() + } + } + + override fun refreshUsers() { + applicationScope.launch { + val result = withContext(backgroundDispatcher) { manager.aliveUsers } + + if (result != null) { + _userInfos.value = result + } + } + } + + override fun getSelectedUserInfo(): UserInfo { + return checkNotNull(_selectedUserInfo.value) + } + + override fun isSimpleUserSwitcher(): Boolean { + return checkNotNull(_userSwitcherSettings.value?.isSimpleUserSwitcher) + } + + private fun observeSelectedUser() { + conflatedCallbackFlow { + fun send() { + trySendWithFailureLogging(tracker.userInfo, TAG) + } + + val callback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + send() + } + } + + tracker.addCallback(callback, mainDispatcher.asExecutor()) + send() + + awaitClose { tracker.removeCallback(callback) } + } + .onEach { + if (!it.isGuest) { + lastSelectedNonGuestUserId = it.id + } + + _selectedUserInfo.value = it + } + .launchIn(applicationScope) + } + + private fun observeUserSettings() { + globalSettings + .observerFlow( + names = + arrayOf( + SETTING_SIMPLE_USER_SWITCHER, + Settings.Global.ADD_USERS_WHEN_LOCKED, + Settings.Global.USER_SWITCHER_ENABLED, + ), + userId = UserHandle.USER_SYSTEM, + ) + .onStart { emit(Unit) } // Forces an initial update. + .map { getSettings() } + .onEach { _userSwitcherSettings.value = it } + .launchIn(applicationScope) + } + + private suspend fun getSettings(): UserSwitcherSettingsModel { + return withContext(backgroundDispatcher) { + val isSimpleUserSwitcher = + globalSettings.getIntForUser( + SETTING_SIMPLE_USER_SWITCHER, + if ( + appContext.resources.getBoolean( + com.android.internal.R.bool.config_expandLockScreenUserSwitcher + ) + ) { + 1 + } else { + 0 + }, + UserHandle.USER_SYSTEM, + ) != 0 + + val isAddUsersFromLockscreen = + globalSettings.getIntForUser( + Settings.Global.ADD_USERS_WHEN_LOCKED, + 0, + UserHandle.USER_SYSTEM, + ) != 0 + + val isUserSwitcherEnabled = + globalSettings.getIntForUser( + Settings.Global.USER_SWITCHER_ENABLED, + 0, + UserHandle.USER_SYSTEM, + ) != 0 + + UserSwitcherSettingsModel( + isSimpleUserSwitcher = isSimpleUserSwitcher, + isAddUsersFromLockscreen = isAddUsersFromLockscreen, + isUserSwitcherEnabled = isUserSwitcherEnabled, + ) + } + } private fun UserRecord.isUser(): Boolean { return when { @@ -125,6 +337,7 @@ constructor( image = getUserImage(this), isSelected = isCurrent, isSelectable = isSwitchToEnabled || isGuest, + isGuest = isGuest, ) } @@ -162,5 +375,6 @@ constructor( companion object { private const val TAG = "UserRepository" + @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher" } } diff --git a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt index cf6da9a60d78..9370286d7ee7 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt @@ -19,6 +19,7 @@ package com.android.systemui.user.data.source import android.content.pm.UserInfo import android.graphics.Bitmap import android.os.UserHandle +import com.android.settingslib.RestrictedLockUtils /** Encapsulates raw data for a user or an option item related to managing users on the device. */ data class UserRecord( @@ -41,6 +42,11 @@ data class UserRecord( @JvmField val isSwitchToEnabled: Boolean = false, /** Whether this record represents an option to add another supervised user to the device. */ @JvmField val isAddSupervisedUser: Boolean = false, + /** + * An enforcing admin, if the user action represented by this record is disabled by the admin. + * If not disabled, this is `null`. + */ + @JvmField val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin? = null, ) { /** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */ fun copyWithIsCurrent(isCurrent: Boolean): UserRecord { @@ -59,6 +65,14 @@ data class UserRecord( } } + /** + * Returns `true` if the user action represented by this record has been disabled by an admin; + * `false` otherwise. + */ + fun isDisabledByAdmin(): Boolean { + return enforcedAdmin != null + } + companion object { @JvmStatic fun createForGuest(): UserRecord { diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt new file mode 100644 index 000000000000..27748128a557 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt @@ -0,0 +1,322 @@ +/* + * 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.UserIdInt +import android.app.admin.DevicePolicyManager +import android.content.Context +import android.content.pm.UserInfo +import android.os.RemoteException +import android.os.UserHandle +import android.os.UserManager +import android.util.Log +import android.view.WindowManagerGlobal +import android.widget.Toast +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.QSUserSwitcherEvent +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext + +/** Encapsulates business logic to interact with guest user data and systems. */ +@SysUISingleton +class GuestUserInteractor +@Inject +constructor( + @Application private val applicationContext: Context, + @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val manager: UserManager, + private val repository: UserRepository, + private val deviceProvisionedController: DeviceProvisionedController, + private val devicePolicyManager: DevicePolicyManager, + private val refreshUsersScheduler: RefreshUsersScheduler, + private val uiEventLogger: UiEventLogger, +) { + /** Whether the device is configured to always have a guest user available. */ + val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated + + /** Whether the guest user is currently being reset. */ + val isGuestUserResetting: Boolean = repository.isGuestUserResetting + + /** Notifies that the device has finished booting. */ + fun onDeviceBootCompleted() { + applicationScope.launch { + if (isDeviceAllowedToAddGuest()) { + guaranteePresent() + return@launch + } + + suspendCancellableCoroutine<Unit> { continuation -> + val callback = + object : DeviceProvisionedController.DeviceProvisionedListener { + override fun onDeviceProvisionedChanged() { + continuation.resumeWith(Result.success(Unit)) + deviceProvisionedController.removeCallback(this) + } + } + + deviceProvisionedController.addCallback(callback) + } + + if (isDeviceAllowedToAddGuest()) { + guaranteePresent() + } + } + } + + /** Creates a guest user and switches to it. */ + fun createAndSwitchTo( + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + selectUser: (userId: Int) -> Unit, + ) { + applicationScope.launch { + val newGuestUserId = create(showDialog, dismissDialog) + if (newGuestUserId != UserHandle.USER_NULL) { + selectUser(newGuestUserId) + } + } + } + + /** Exits the guest user, switching back to the last non-guest user or to the default user. */ + fun exit( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + forceRemoveGuestOnExit: Boolean, + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + switchUser: (userId: Int) -> Unit, + ) { + val currentUserInfo = repository.getSelectedUserInfo() + if (currentUserInfo.id != guestUserId) { + Log.w( + TAG, + "User requesting to start a new session ($guestUserId) is not current user" + + " (${currentUserInfo.id})" + ) + return + } + + if (!currentUserInfo.isGuest) { + Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest") + return + } + + applicationScope.launch { + var newUserId = UserHandle.USER_SYSTEM + if (targetUserId == UserHandle.USER_NULL) { + // When a target user is not specified switch to last non guest user: + val lastSelectedNonGuestUserHandle = repository.lastSelectedNonGuestUserId + if (lastSelectedNonGuestUserHandle != UserHandle.USER_SYSTEM) { + val info = + withContext(backgroundDispatcher) { + manager.getUserInfo(lastSelectedNonGuestUserHandle) + } + if (info != null && info.isEnabled && info.supportsSwitchToByUser()) { + newUserId = info.id + } + } + } else { + newUserId = targetUserId + } + + if (currentUserInfo.isEphemeral || forceRemoveGuestOnExit) { + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE) + remove(currentUserInfo.id, newUserId, showDialog, dismissDialog, switchUser) + } else { + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH) + switchUser(newUserId) + } + } + } + + /** + * Guarantees that the guest user is present on the device, creating it if needed and if allowed + * to. + */ + suspend fun guaranteePresent() { + if (!isDeviceAllowedToAddGuest()) { + return + } + + val guestUser = withContext(backgroundDispatcher) { manager.findCurrentGuestUser() } + if (guestUser == null) { + scheduleCreation() + } + } + + /** Removes the guest user from the device. */ + private suspend fun remove( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + switchUser: (userId: Int) -> Unit, + ) { + val currentUser: UserInfo = repository.getSelectedUserInfo() + if (currentUser.id != guestUserId) { + Log.w( + TAG, + "User requesting to start a new session ($guestUserId) is not current user" + + " ($currentUser.id)" + ) + return + } + + if (!currentUser.isGuest) { + Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest") + return + } + + val marked = + withContext(backgroundDispatcher) { manager.markGuestForDeletion(currentUser.id) } + if (!marked) { + Log.w(TAG, "Couldn't mark the guest for deletion for user $guestUserId") + return + } + + if (targetUserId == UserHandle.USER_NULL) { + // Create a new guest in the foreground, and then immediately switch to it + val newGuestId = create(showDialog, dismissDialog) + if (newGuestId == UserHandle.USER_NULL) { + Log.e(TAG, "Could not create new guest, switching back to system user") + switchUser(UserHandle.USER_SYSTEM) + withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) } + try { + WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null) + } catch (e: RemoteException) { + Log.e( + TAG, + "Couldn't remove guest because ActivityManager or WindowManager is dead" + ) + } + return + } + + switchUser(newGuestId) + + withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) } + } else { + if (repository.isGuestUserAutoCreated) { + repository.isGuestUserResetting = true + } + switchUser(targetUserId) + manager.removeUser(currentUser.id) + } + } + + /** + * Creates the guest user and adds it to the device. + * + * @param showDialog A function to invoke to show a dialog. + * @param dismissDialog A function to invoke to dismiss a dialog. + * @return The user ID of the newly-created guest user. + */ + private suspend fun create( + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + ): Int { + return withContext(mainDispatcher) { + showDialog(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true)) + val guestUserId = createInBackground() + dismissDialog() + if (guestUserId != UserHandle.USER_NULL) { + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD) + } else { + Toast.makeText( + applicationContext, + com.android.settingslib.R.string.add_guest_failed, + Toast.LENGTH_SHORT, + ) + .show() + } + + guestUserId + } + } + + /** Schedules the creation of the guest user. */ + private suspend fun scheduleCreation() { + if (!repository.isGuestUserCreationScheduled.compareAndSet(false, true)) { + return + } + + withContext(backgroundDispatcher) { + val newGuestUserId = createInBackground() + repository.isGuestUserCreationScheduled.set(false) + repository.isGuestUserResetting = false + if (newGuestUserId == UserHandle.USER_NULL) { + Log.w(TAG, "Could not create new guest while exiting existing guest") + // Refresh users so that we still display "Guest" if + // config_guestUserAutoCreated=true + refreshUsersScheduler.refreshIfNotPaused() + } + } + } + + /** + * Creates a guest user and return its multi-user user ID. + * + * This method does not check if a guest already exists before it makes a call to [UserManager] + * to create a new one. + * + * @return The multi-user user ID of the newly created guest user, or [UserHandle.USER_NULL] if + * the guest couldn't be created. + */ + @UserIdInt + private suspend fun createInBackground(): Int { + return withContext(backgroundDispatcher) { + try { + val guestUser = manager.createGuest(applicationContext) + if (guestUser != null) { + guestUser.id + } else { + Log.e( + TAG, + "Couldn't create guest, most likely because there already exists one!" + ) + UserHandle.USER_NULL + } + } catch (e: UserManager.UserOperationException) { + Log.e(TAG, "Couldn't create guest user!", e) + UserHandle.USER_NULL + } + } + } + + private fun isDeviceAllowedToAddGuest(): Boolean { + return deviceProvisionedController.isDeviceProvisioned && + !devicePolicyManager.isDeviceManaged + } + + companion object { + private const val TAG = "GuestUserInteractor" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt new file mode 100644 index 000000000000..8f36821a955e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt @@ -0,0 +1,75 @@ +/* + * 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 com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** Encapsulates logic for pausing, unpausing, and scheduling a delayed job. */ +@SysUISingleton +class RefreshUsersScheduler +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, + private val repository: UserRepository, +) { + private var scheduledUnpauseJob: Job? = null + private var isPaused = false + + fun pause() { + applicationScope.launch(mainDispatcher) { + isPaused = true + scheduledUnpauseJob?.cancel() + scheduledUnpauseJob = + applicationScope.launch { + delay(PAUSE_REFRESH_USERS_TIMEOUT_MS) + unpauseAndRefresh() + } + } + } + + fun unpauseAndRefresh() { + applicationScope.launch(mainDispatcher) { + isPaused = false + refreshIfNotPaused() + } + } + + fun refreshIfNotPaused() { + applicationScope.launch(mainDispatcher) { + if (isPaused) { + return@launch + } + + repository.refreshUsers() + } + } + + companion object { + private const val PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt new file mode 100644 index 000000000000..1b4746a99f8f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import android.os.UserHandle +import android.os.UserManager +import com.android.systemui.user.data.repository.UserRepository + +/** Utilities related to user management actions. */ +object UserActionsUtil { + + /** Returns `true` if it's possible to add a guest user to the device; `false` otherwise. */ + fun canCreateGuest( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + if (!isUserSwitcherEnabled) { + return false + } + + return currentUserCanCreateUsers(manager, repository) || + anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled) + } + + /** Returns `true` if it's possible to add a user to the device; `false` otherwise. */ + fun canCreateUser( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + if (!isUserSwitcherEnabled) { + return false + } + + if ( + !currentUserCanCreateUsers(manager, repository) && + !anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled) + ) { + return false + } + + return manager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY) + } + + /** + * Returns `true` if it's possible to add a supervised user to the device; `false` otherwise. + */ + fun canCreateSupervisedUser( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + supervisedUserPackageName: String? + ): Boolean { + if (supervisedUserPackageName.isNullOrEmpty()) { + return false + } + + return canCreateUser( + manager, + repository, + isUserSwitcherEnabled, + isAddUsersFromLockScreenEnabled + ) + } + + /** + * Returns `true` if the current user is allowed to add users to the device; `false` otherwise. + */ + private fun currentUserCanCreateUsers( + manager: UserManager, + repository: UserRepository, + ): Boolean { + val currentUser = repository.getSelectedUserInfo() + if (!currentUser.isAdmin && currentUser.id != UserHandle.USER_SYSTEM) { + return false + } + + return systemCanCreateUsers(manager) + } + + /** Returns `true` if the system can add users to the device; `false` otherwise. */ + private fun systemCanCreateUsers( + manager: UserManager, + ): Boolean { + return !manager.hasBaseUserRestriction(UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM) + } + + /** Returns `true` if it's allowed to add users to the device at all; `false` otherwise. */ + private fun anyoneCanCreateUsers( + manager: UserManager, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + return systemCanCreateUsers(manager) && isAddUsersFromLockScreenEnabled + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index 3c5b9697c013..e6bb9bcbc264 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -17,94 +17,664 @@ package com.android.systemui.user.domain.interactor +import android.annotation.SuppressLint +import android.annotation.UserIdInt +import android.app.ActivityManager +import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.content.pm.UserInfo +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.RemoteException +import android.os.UserHandle +import android.os.UserManager import android.provider.Settings +import android.util.Log +import com.android.internal.util.UserIcons +import com.android.systemui.R +import com.android.systemui.SystemUISecondaryUserService +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.user.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.util.kotlin.pairwise +import java.util.Collections +import java.util.WeakHashMap import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.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.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext /** Encapsulates business logic to interact with user data and systems. */ @SysUISingleton class UserInteractor @Inject constructor( - repository: UserRepository, + @Application private val applicationContext: Context, + private val repository: UserRepository, private val controller: UserSwitcherController, private val activityStarter: ActivityStarter, - keyguardInteractor: KeyguardInteractor, + private val keyguardInteractor: KeyguardInteractor, + private val featureFlags: FeatureFlags, + private val manager: UserManager, + @Application private val applicationScope: CoroutineScope, + telephonyInteractor: TelephonyInteractor, + broadcastDispatcher: BroadcastDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val activityManager: ActivityManager, + private val refreshUsersScheduler: RefreshUsersScheduler, + private val guestUserInteractor: GuestUserInteractor, ) { + /** + * Defines interface for classes that can be notified when the state of users on the device is + * changed. + */ + fun interface UserCallback { + /** Notifies that the state of users on the device has changed. */ + fun onUserStateChanged() + } + + private val isNewImpl: Boolean + get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) + + private val supervisedUserPackageName: String? + get() = + applicationContext.getString( + com.android.internal.R.string.config_supervisedUserCreationPackage + ) + + private val callbacks = Collections.newSetFromMap(WeakHashMap<UserCallback, Boolean>()) + /** List of current on-device users to select from. */ - val users: Flow<List<UserModel>> = repository.users + val users: Flow<List<UserModel>> + get() = + if (isNewImpl) { + combine( + repository.userInfos, + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { userInfos, selectedUserInfo, settings -> + toUserModels( + userInfos = userInfos, + selectedUserId = selectedUserInfo.id, + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, + ) + } + } else { + repository.users + } /** The currently-selected user. */ - val selectedUser: Flow<UserModel> = repository.selectedUser + val selectedUser: Flow<UserModel> + get() = + if (isNewImpl) { + combine( + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { selectedUserInfo, settings -> + val selectedUserId = selectedUserInfo.id + checkNotNull( + toUserModel( + userInfo = selectedUserInfo, + selectedUserId = selectedUserId, + canSwitchUsers = canSwitchUsers(selectedUserId), + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, + ) + ) + } + } else { + repository.selectedUser + } /** List of user-switcher related actions that are available. */ - val actions: Flow<List<UserActionModel>> = - combine( - repository.isActionableWhenLocked, - keyguardInteractor.isKeyguardShowing, - ) { isActionableWhenLocked, isLocked -> - isActionableWhenLocked || !isLocked - } - .flatMapLatest { isActionable -> - if (isActionable) { - repository.actions.map { actions -> - actions + - if (actions.isNotEmpty()) { - // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because - // that's a user - // switcher specific action that is not known to the our data source - // or other - // features. - listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - } else { - // If no actions, don't add the navigate action. - emptyList() - } + val actions: Flow<List<UserActionModel>> + get() = + if (isNewImpl) { + combine( + repository.userInfos, + repository.userSwitcherSettings, + keyguardInteractor.isKeyguardShowing, + ) { userInfos, settings, isDeviceLocked -> + buildList { + val hasGuestUser = userInfos.any { it.isGuest } + if ( + !hasGuestUser && + (guestUserInteractor.isGuestUserAutoCreated || + UserActionsUtil.canCreateGuest( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + )) + ) { + add(UserActionModel.ENTER_GUEST_MODE) + } + + if (isDeviceLocked && !settings.isAddUsersFromLockscreen) { + // The device is locked and our setting to allow actions that add users + // from the lock-screen is not enabled. The guest action from above is + // always allowed, even when the device is locked, but the various "add + // user" actions below are not. We can finish building the list here. + return@buildList + } + + if ( + UserActionsUtil.canCreateUser( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + ) + ) { + add(UserActionModel.ADD_USER) + } + + if ( + UserActionsUtil.canCreateSupervisedUser( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + supervisedUserPackageName, + ) + ) { + add(UserActionModel.ADD_SUPERVISED_USER) + } } - } else { - // If not actionable it means that we're not allowed to show actions when locked - // and we - // are locked. Therefore, we should show no actions. - flowOf(emptyList()) } + } else { + combine( + repository.isActionableWhenLocked, + keyguardInteractor.isKeyguardShowing, + ) { isActionableWhenLocked, isLocked -> + isActionableWhenLocked || !isLocked + } + .flatMapLatest { isActionable -> + if (isActionable) { + repository.actions.map { actions -> + actions + + if (actions.isNotEmpty()) { + // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT + // because that's a user switcher specific action that is + // not known to the our data source or other features. + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + } else { + // If no actions, don't add the navigate action. + emptyList() + } + } + } else { + // If not actionable it means that we're not allowed to show actions + // when + // locked and we are locked. Therefore, we should show no actions. + flowOf(emptyList()) + } + } } + val userRecords: StateFlow<ArrayList<UserRecord>> = + if (isNewImpl) { + combine( + repository.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, + isAddFromLockscreenEnabled = settings.isAddUsersFromLockscreen, + ) + } + ) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = ArrayList(), + ) + } else { + MutableStateFlow(ArrayList()) + } + + val selectedUserRecord: StateFlow<UserRecord?> = + if (isNewImpl) { + repository.selectedUserInfo + .map { selectedUserInfo -> + toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + } else { + MutableStateFlow(null) + } + /** Whether the device is configured to always have a guest user available. */ - val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated + val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated /** Whether the guest user is currently being reset. */ - val isGuestUserResetting: Boolean = repository.isGuestUserResetting + val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting + + private val _dialogShowRequests = MutableStateFlow<ShowDialogRequestModel?>(null) + val dialogShowRequests: Flow<ShowDialogRequestModel?> = _dialogShowRequests.asStateFlow() + + private val _dialogDismissRequests = MutableStateFlow<Unit?>(null) + val dialogDismissRequests: Flow<Unit?> = _dialogDismissRequests.asStateFlow() + + val isSimpleUserSwitcher: Boolean + get() = + if (isNewImpl) { + repository.isSimpleUserSwitcher() + } else { + error("Not supported in the old implementation!") + } + + fun addCallback(callback: UserCallback) { + callbacks.add(callback) + } + + fun removeCallback(callback: UserCallback) { + callbacks.remove(callback) + } + + fun onDialogShown() { + _dialogShowRequests.value = null + } + + fun onDialogDismissed() { + _dialogDismissRequests.value = null + } + + private fun showDialog(request: ShowDialogRequestModel) { + _dialogShowRequests.value = request + } + + private fun dismissDialog() { + _dialogDismissRequests.value = Unit + } + + init { + if (isNewImpl) { + refreshUsersScheduler.refreshIfNotPaused() + telephonyInteractor.callState + .distinctUntilChanged() + .onEach { refreshUsersScheduler.refreshIfNotPaused() } + .launchIn(applicationScope) + + combine( + broadcastDispatcher.broadcastFlow( + filter = + IntentFilter().apply { + addAction(Intent.ACTION_USER_ADDED) + addAction(Intent.ACTION_USER_REMOVED) + addAction(Intent.ACTION_USER_INFO_CHANGED) + addAction(Intent.ACTION_USER_SWITCHED) + addAction(Intent.ACTION_USER_STOPPED) + addAction(Intent.ACTION_USER_UNLOCKED) + }, + user = UserHandle.SYSTEM, + map = { intent, _ -> intent }, + ), + repository.selectedUserInfo.pairwise(null), + ) { intent, selectedUserChange -> + Pair(intent, selectedUserChange.previousValue) + } + .onEach { (intent, previousSelectedUser) -> + onBroadcastReceived(intent, previousSelectedUser) + } + .launchIn(applicationScope) + } + } + + fun onDeviceBootCompleted() { + guestUserInteractor.onDeviceBootCompleted() + } /** Switches to the user with the given user ID. */ fun selectUser( - userId: Int, + newlySelectedUserId: Int, ) { - controller.onUserSelected(userId, /* dialogShower= */ null) + if (isNewImpl) { + val currentlySelectedUserInfo = repository.getSelectedUserInfo() + if ( + newlySelectedUserId == currentlySelectedUserInfo.id && + currentlySelectedUserInfo.isGuest + ) { + // Here when clicking on the currently-selected guest user to leave guest mode + // and return to the previously-selected non-guest user. + showDialog( + ShowDialogRequestModel.ShowExitGuestDialog( + guestUserId = currentlySelectedUserInfo.id, + targetUserId = repository.lastSelectedNonGuestUserId, + isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + onExitGuestUser = this::exitGuestUser, + ) + ) + return + } + + if (currentlySelectedUserInfo.isGuest) { + // Here when switching from guest to a non-guest user. + showDialog( + ShowDialogRequestModel.ShowExitGuestDialog( + guestUserId = currentlySelectedUserInfo.id, + targetUserId = newlySelectedUserId, + isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + onExitGuestUser = this::exitGuestUser, + ) + ) + return + } + + switchUser(newlySelectedUserId) + } else { + controller.onUserSelected(newlySelectedUserId, /* dialogShower= */ null) + } } /** Executes the given action. */ fun executeAction(action: UserActionModel) { - when (action) { - UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null) - UserActionModel.ADD_USER -> controller.showAddUserDialog(null) - UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity() - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> - activityStarter.startActivity( - Intent(Settings.ACTION_USER_SETTINGS), - /* dismissShade= */ false, + if (isNewImpl) { + when (action) { + UserActionModel.ENTER_GUEST_MODE -> + guestUserInteractor.createAndSwitchTo( + this::showDialog, + this::dismissDialog, + this::selectUser, + ) + UserActionModel.ADD_USER -> { + val currentUser = repository.getSelectedUserInfo() + showDialog( + ShowDialogRequestModel.ShowAddUserDialog( + userHandle = currentUser.userHandle, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral, + ) + ) + } + UserActionModel.ADD_SUPERVISED_USER -> + activityStarter.startActivity( + Intent() + .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) + .setPackage(supervisedUserPackageName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + /* dismissShade= */ false, + ) + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> + activityStarter.startActivity( + Intent(Settings.ACTION_USER_SETTINGS), + /* dismissShade= */ false, + ) + } + } else { + when (action) { + UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null) + UserActionModel.ADD_USER -> controller.showAddUserDialog(null) + UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity() + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> + activityStarter.startActivity( + Intent(Settings.ACTION_USER_SETTINGS), + /* dismissShade= */ false, + ) + } + } + } + + private 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, + isAddFromLockscreenEnabled: Boolean, + ): UserRecord { + return LegacyUserDataHelper.createRecord( + context = applicationContext, + selectedUserId = selectedUserId, + actionType = action, + isRestricted = + if (action == UserActionModel.ENTER_GUEST_MODE) { + // Entering guest mode is never restricted, so it's allowed to happen from the + // lockscreen even if the "add from lockscreen" system setting is off. + false + } else { + !isAddFromLockscreenEnabled + }, + isSwitchToEnabled = + canSwitchUsers(selectedUserId) && + // If the user is auto-created is must not be currently resetting. + !(isGuestUserAutoCreated && isGuestUserResetting), + ) + } + + private fun exitGuestUser( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + forceRemoveGuestOnExit: Boolean, + ) { + guestUserInteractor.exit( + guestUserId = guestUserId, + targetUserId = targetUserId, + forceRemoveGuestOnExit = forceRemoveGuestOnExit, + showDialog = this::showDialog, + dismissDialog = this::dismissDialog, + switchUser = this::switchUser, + ) + } + + private fun switchUser(userId: Int) { + // TODO(b/246631653): track jank and lantecy like in the old impl. + refreshUsersScheduler.pause() + try { + activityManager.switchUser(userId) + } catch (e: RemoteException) { + Log.e(TAG, "Couldn't switch user.", e) + } + } + + private suspend fun onBroadcastReceived( + intent: Intent, + previousUserInfo: UserInfo?, + ) { + val shouldRefreshAllUsers = + when (intent.action) { + Intent.ACTION_USER_SWITCHED -> { + dismissDialog() + val selectedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) + if (previousUserInfo?.id != selectedUserId) { + callbacks.forEach { it.onUserStateChanged() } + restartSecondaryService(selectedUserId) + } + if (guestUserInteractor.isGuestUserAutoCreated) { + guestUserInteractor.guaranteePresent() + } + true + } + Intent.ACTION_USER_INFO_CHANGED -> true + Intent.ACTION_USER_UNLOCKED -> { + // If we unlocked the system user, we should refresh all users. + intent.getIntExtra( + Intent.EXTRA_USER_HANDLE, + UserHandle.USER_NULL, + ) == UserHandle.USER_SYSTEM + } + else -> true + } + + if (shouldRefreshAllUsers) { + refreshUsersScheduler.unpauseAndRefresh() + } + } + + private fun restartSecondaryService(@UserIdInt userId: Int) { + val intent = Intent(applicationContext, SystemUISecondaryUserService::class.java) + // Disconnect from the old secondary user's service + val secondaryUserId = repository.secondaryUserId + if (secondaryUserId != UserHandle.USER_NULL) { + applicationContext.stopServiceAsUser( + intent, + UserHandle.of(secondaryUserId), + ) + repository.secondaryUserId = UserHandle.USER_NULL + } + + // Connect to the new secondary user's service (purely to ensure that a persistent + // SystemUI application is created for that user) + if (userId != UserHandle.USER_SYSTEM) { + applicationContext.startServiceAsUser( + intent, + UserHandle.of(userId), + ) + repository.secondaryUserId = userId + } + } + + private suspend fun toUserModels( + userInfos: List<UserInfo>, + selectedUserId: Int, + isUserSwitcherEnabled: Boolean, + ): List<UserModel> { + val canSwitchUsers = canSwitchUsers(selectedUserId) + + return userInfos + // The guest user should go in the last position. + .sortedBy { it.isGuest } + .mapNotNull { userInfo -> + toUserModel( + userInfo = userInfo, + selectedUserId = selectedUserId, + canSwitchUsers = canSwitchUsers, + isUserSwitcherEnabled = isUserSwitcherEnabled, ) + } + } + + private suspend fun toUserModel( + userInfo: UserInfo, + selectedUserId: Int, + canSwitchUsers: Boolean, + isUserSwitcherEnabled: Boolean, + ): UserModel? { + val userId = userInfo.id + val isSelected = userId == selectedUserId + + return when { + // When the user switcher is not enabled in settings, we only show the primary user. + !isUserSwitcherEnabled && !userInfo.isPrimary -> null + + // We avoid showing disabled users. + !userInfo.isEnabled -> null + userInfo.isGuest -> + UserModel( + id = userId, + name = Text.Loaded(userInfo.name), + image = + getUserImage( + isGuest = true, + userId = userId, + ), + isSelected = isSelected, + isSelectable = canSwitchUsers, + isGuest = true, + ) + userInfo.supportsSwitchToByUser() -> + UserModel( + id = userId, + name = Text.Loaded(userInfo.name), + image = + getUserImage( + isGuest = false, + userId = userId, + ), + isSelected = isSelected, + isSelectable = canSwitchUsers || isSelected, + isGuest = false, + ) + else -> null } } + + private suspend fun canSwitchUsers(selectedUserId: Int): Boolean { + return withContext(backgroundDispatcher) { + manager.getUserSwitchability(UserHandle.of(selectedUserId)) + } == UserManager.SWITCHABILITY_STATUS_OK + } + + @SuppressLint("UseCompatLoadingForDrawables") + private suspend fun getUserImage( + isGuest: Boolean, + userId: Int, + ): Drawable { + if (isGuest) { + return checkNotNull(applicationContext.getDrawable(R.drawable.ic_account_circle)) + } + + // TODO(b/246631653): cache the bitmaps to avoid the background work to fetch them. + // TODO(b/246631653): downscale the bitmaps to R.dimen.max_avatar_size if requested. + val userIcon = withContext(backgroundDispatcher) { manager.getUserIcon(userId) } + if (userIcon != null) { + return BitmapDrawable(userIcon) + } + + return UserIcons.getDefaultUserIcon( + applicationContext.resources, + userId, + /* light= */ false + ) + } + + companion object { + private const val TAG = "UserInteractor" + } } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt new file mode 100644 index 000000000000..08d7c5a26a25 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt @@ -0,0 +1,41 @@ +/* + * 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.model + +import android.os.UserHandle + +/** Encapsulates a request to show a dialog. */ +sealed class ShowDialogRequestModel { + data class ShowAddUserDialog( + val userHandle: UserHandle, + val isKeyguardShowing: Boolean, + val showEphemeralMessage: Boolean, + ) : ShowDialogRequestModel() + + data class ShowUserCreationDialog( + val isGuest: Boolean, + ) : ShowDialogRequestModel() + + data class ShowExitGuestDialog( + val guestUserId: Int, + val targetUserId: Int, + val isGuestEphemeral: Boolean, + val isKeyguardShowing: Boolean, + val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit, + ) : ShowDialogRequestModel() +} diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt new file mode 100644 index 000000000000..8f6662f7b192 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt @@ -0,0 +1,135 @@ +/* + * 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.legacyhelper.data + +import android.content.Context +import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.os.UserManager +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin +import com.android.settingslib.RestrictedLockUtilsInternal +import com.android.systemui.R +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.shared.model.UserActionModel + +/** + * Defines utility functions for helping with legacy data code for users. + * + * We need these to avoid code duplication between logic inside the UserSwitcherController and in + * modern architecture classes such as repositories, interactors, and view-models. If we ever + * simplify UserSwitcherController (or delete it), the code here could be moved into its call-sites. + */ +object LegacyUserDataHelper { + + @JvmStatic + fun createRecord( + context: Context, + manager: UserManager, + picture: Bitmap?, + userInfo: UserInfo, + isCurrent: Boolean, + canSwitchUsers: Boolean, + ): UserRecord { + val isGuest = userInfo.isGuest + return UserRecord( + info = userInfo, + picture = + getPicture( + manager = manager, + context = context, + userInfo = userInfo, + picture = picture, + ), + isGuest = isGuest, + isCurrent = isCurrent, + isSwitchToEnabled = canSwitchUsers || (isCurrent && !isGuest), + ) + } + + @JvmStatic + fun createRecord( + context: Context, + selectedUserId: Int, + actionType: UserActionModel, + isRestricted: Boolean, + isSwitchToEnabled: Boolean, + ): UserRecord { + return UserRecord( + isGuest = actionType == UserActionModel.ENTER_GUEST_MODE, + isAddUser = actionType == UserActionModel.ADD_USER, + isAddSupervisedUser = actionType == UserActionModel.ADD_SUPERVISED_USER, + isRestricted = isRestricted, + isSwitchToEnabled = isSwitchToEnabled, + enforcedAdmin = + getEnforcedAdmin( + context = context, + selectedUserId = selectedUserId, + ), + ) + } + + private fun getEnforcedAdmin( + context: Context, + selectedUserId: Int, + ): EnforcedAdmin? { + val admin = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + context, + UserManager.DISALLOW_ADD_USER, + selectedUserId, + ) + ?: return null + + return if ( + !RestrictedLockUtilsInternal.hasBaseUserRestriction( + context, + UserManager.DISALLOW_ADD_USER, + selectedUserId, + ) + ) { + admin + } else { + null + } + } + + private fun getPicture( + context: Context, + manager: UserManager, + userInfo: UserInfo, + picture: Bitmap?, + ): Bitmap? { + if (userInfo.isGuest) { + return null + } + + if (picture != null) { + return picture + } + + val unscaledOrNull = manager.getUserIcon(userInfo.id) ?: return null + + val avatarSize = context.resources.getDimensionPixelSize(R.dimen.max_avatar_size) + return Bitmap.createScaledBitmap( + unscaledOrNull, + avatarSize, + avatarSize, + /* filter= */ true, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt index bf7977a600e9..2095683ccb4c 100644 --- a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt @@ -32,4 +32,6 @@ data class UserModel( val isSelected: Boolean, /** Whether this use is selectable. A non-selectable user cannot be switched to. */ val isSelectable: Boolean, + /** Whether this model represents the guest user. */ + val isGuest: Boolean, ) diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt new file mode 100644 index 000000000000..a9d66de118e0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt @@ -0,0 +1,107 @@ +/* + * 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.ui.dialog + +import android.app.ActivityManager +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.os.UserHandle +import com.android.settingslib.R +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.user.CreateUserActivity + +/** Dialog for adding a new user to the device. */ +class AddUserDialog( + context: Context, + userHandle: UserHandle, + isKeyguardShowing: Boolean, + showEphemeralMessage: Boolean, + private val falsingManager: FalsingManager, + private val broadcastSender: BroadcastSender, + private val dialogLaunchAnimator: DialogLaunchAnimator +) : SystemUIDialog(context) { + + private val onClickListener = + object : DialogInterface.OnClickListener { + override fun onClick(dialog: DialogInterface, which: Int) { + val penalty = + if (which == BUTTON_NEGATIVE) { + FalsingManager.NO_PENALTY + } else { + FalsingManager.MODERATE_PENALTY + } + if (falsingManager.isFalseTap(penalty)) { + return + } + + if (which == BUTTON_NEUTRAL) { + cancel() + return + } + + dialogLaunchAnimator.dismissStack(this@AddUserDialog) + if (ActivityManager.isUserAMonkey()) { + return + } + + // Use broadcast instead of ShadeController, as this dialog may have started in + // another + // process where normal dagger bindings are not available. + broadcastSender.sendBroadcastAsUser( + Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), + UserHandle.CURRENT + ) + + context.startActivityAsUser( + CreateUserActivity.createIntentForStart(context), + userHandle, + ) + } + } + + init { + setTitle(R.string.user_add_user_title) + val message = + context.getString(R.string.user_add_user_message_short) + + if (showEphemeralMessage) { + context.getString( + com.android.systemui.R.string.user_add_user_message_guest_remove + ) + } else { + "" + } + setMessage(message) + + setButton( + BUTTON_NEUTRAL, + context.getString(android.R.string.cancel), + onClickListener, + ) + + setButton( + BUTTON_POSITIVE, + context.getString(android.R.string.ok), + onClickListener, + ) + + setWindowOnTop(this, isKeyguardShowing) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt new file mode 100644 index 000000000000..19ad44d8649f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt @@ -0,0 +1,132 @@ +/* + * 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.ui.dialog + +import android.annotation.UserIdInt +import android.content.Context +import android.content.DialogInterface +import com.android.settingslib.R +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.phone.SystemUIDialog + +/** Dialog for exiting the guest user. */ +class ExitGuestDialog( + context: Context, + private val guestUserId: Int, + private val isGuestEphemeral: Boolean, + private val targetUserId: Int, + isKeyguardShowing: Boolean, + private val falsingManager: FalsingManager, + private val dialogLaunchAnimator: DialogLaunchAnimator, + private val onExitGuestUserListener: OnExitGuestUserListener, +) : SystemUIDialog(context) { + + fun interface OnExitGuestUserListener { + fun onExitGuestUser( + @UserIdInt guestId: Int, + @UserIdInt targetId: Int, + forceRemoveGuest: Boolean, + ) + } + + private val onClickListener = + object : DialogInterface.OnClickListener { + override fun onClick(dialog: DialogInterface, which: Int) { + val penalty = + if (which == BUTTON_NEGATIVE) { + FalsingManager.NO_PENALTY + } else { + FalsingManager.MODERATE_PENALTY + } + if (falsingManager.isFalseTap(penalty)) { + return + } + + if (isGuestEphemeral) { + if (which == BUTTON_POSITIVE) { + dialogLaunchAnimator.dismissStack(this@ExitGuestDialog) + // Ephemeral guest: exit guest, guest is removed by the system + // on exit, since its marked ephemeral + onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, false) + } else if (which == BUTTON_NEGATIVE) { + // Cancel clicked, do nothing + cancel() + } + } else { + when (which) { + BUTTON_POSITIVE -> { + dialogLaunchAnimator.dismissStack(this@ExitGuestDialog) + // Non-ephemeral guest: exit guest, guest is not removed by the system + // on exit, since its marked non-ephemeral + onExitGuestUserListener.onExitGuestUser( + guestUserId, + targetUserId, + false + ) + } + BUTTON_NEGATIVE -> { + dialogLaunchAnimator.dismissStack(this@ExitGuestDialog) + // Non-ephemeral guest: remove guest and then exit + onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, true) + } + BUTTON_NEUTRAL -> { + // Cancel clicked, do nothing + cancel() + } + } + } + } + } + + init { + if (isGuestEphemeral) { + setTitle(context.getString(R.string.guest_exit_dialog_title)) + setMessage(context.getString(R.string.guest_exit_dialog_message)) + setButton( + BUTTON_NEUTRAL, + context.getString(android.R.string.cancel), + onClickListener, + ) + setButton( + BUTTON_POSITIVE, + context.getString(R.string.guest_exit_dialog_button), + onClickListener, + ) + } else { + setTitle(context.getString(R.string.guest_exit_dialog_title_non_ephemeral)) + setMessage(context.getString(R.string.guest_exit_dialog_message_non_ephemeral)) + setButton( + BUTTON_NEUTRAL, + context.getString(android.R.string.cancel), + onClickListener, + ) + setButton( + BUTTON_NEGATIVE, + context.getString(R.string.guest_exit_clear_data_button), + onClickListener, + ) + setButton( + BUTTON_POSITIVE, + context.getString(R.string.guest_exit_save_data_button), + onClickListener, + ) + } + setWindowOnTop(this, isKeyguardShowing) + setCanceledOnTouchOutside(false) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt new file mode 100644 index 000000000000..c1d2f4788147 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt @@ -0,0 +1,33 @@ +/* + * 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.ui.dialog + +import com.android.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface UserDialogModule { + + @Binds + @IntoMap + @ClassKey(UserSwitcherDialogCoordinator::class) + fun bindFeature(impl: UserSwitcherDialogCoordinator): CoreStartable +} 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 new file mode 100644 index 000000000000..6e7b5232d818 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -0,0 +1,122 @@ +/* + * 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.ui.dialog + +import android.app.Dialog +import android.content.Context +import com.android.settingslib.users.UserCreatingDialog +import com.android.systemui.CoreStartable +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch + +/** Coordinates dialogs for user switcher logic. */ +@SysUISingleton +class UserSwitcherDialogCoordinator +@Inject +constructor( + @Application private val context: Context, + @Application private val applicationScope: CoroutineScope, + private val falsingManager: FalsingManager, + private val broadcastSender: BroadcastSender, + private val dialogLaunchAnimator: DialogLaunchAnimator, + private val interactor: UserInteractor, + private val featureFlags: FeatureFlags, +) : CoreStartable(context) { + + private var currentDialog: Dialog? = null + + override fun start() { + if (featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) { + return + } + + startHandlingDialogShowRequests() + startHandlingDialogDismissRequests() + } + + private fun startHandlingDialogShowRequests() { + applicationScope.launch { + interactor.dialogShowRequests.filterNotNull().collect { request -> + currentDialog?.let { + if (it.isShowing) { + it.cancel() + } + } + + currentDialog = + when (request) { + is ShowDialogRequestModel.ShowAddUserDialog -> + AddUserDialog( + context = context, + userHandle = request.userHandle, + isKeyguardShowing = request.isKeyguardShowing, + showEphemeralMessage = request.showEphemeralMessage, + falsingManager = falsingManager, + broadcastSender = broadcastSender, + dialogLaunchAnimator = dialogLaunchAnimator, + ) + is ShowDialogRequestModel.ShowUserCreationDialog -> + UserCreatingDialog( + context, + request.isGuest, + ) + is ShowDialogRequestModel.ShowExitGuestDialog -> + ExitGuestDialog( + context = context, + guestUserId = request.guestUserId, + isGuestEphemeral = request.isGuestEphemeral, + targetUserId = request.targetUserId, + isKeyguardShowing = request.isKeyguardShowing, + falsingManager = falsingManager, + dialogLaunchAnimator = dialogLaunchAnimator, + onExitGuestUserListener = request.onExitGuestUser, + ) + } + + currentDialog?.show() + interactor.onDialogShown() + } + } + } + + private fun startHandlingDialogDismissRequests() { + applicationScope.launch { + interactor.dialogDismissRequests.filterNotNull().collect { + currentDialog?.let { + if (it.isShowing) { + it.cancel() + } + } + + interactor.onDialogDismissed() + } + } + } +} 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 398341d256d2..5b83df7b4a36 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 @@ -21,7 +21,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.android.systemui.R import com.android.systemui.common.ui.drawable.CircularDrawable +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.user.domain.interactor.GuestUserInteractor import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel @@ -36,9 +39,14 @@ import kotlinx.coroutines.flow.map class UserSwitcherViewModel private constructor( private val userInteractor: UserInteractor, + private val guestUserInteractor: GuestUserInteractor, private val powerInteractor: PowerInteractor, + private val featureFlags: FeatureFlags, ) : ViewModel() { + private val isNewImpl: Boolean + get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) + /** On-device users. */ val users: Flow<List<UserViewModel>> = userInteractor.users.map { models -> models.map { user -> toViewModel(user) } } @@ -47,9 +55,6 @@ private constructor( val maximumUserColumns: Flow<Int> = users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) } - /** Whether the button to open the user action menu is visible. */ - val isOpenMenuButtonVisible: Flow<Boolean> = userInteractor.actions.map { it.isNotEmpty() } - private val _isMenuVisible = MutableStateFlow(false) /** * Whether the user action menu should be shown. Once the action menu is dismissed/closed, the @@ -58,9 +63,23 @@ private constructor( val isMenuVisible: Flow<Boolean> = _isMenuVisible /** The user action menu. */ val menu: Flow<List<UserActionViewModel>> = - userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } } + userInteractor.actions.map { actions -> + if (isNewImpl && actions.isNotEmpty()) { + // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user + // switcher specific action that is not known to the our data source or other + // features. + actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + } else { + actions + } + .map { action -> toViewModel(action) } + } + + /** Whether the button to open the user action menu is visible. */ + val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() } private val hasCancelButtonBeenClicked = MutableStateFlow(false) + private val isFinishRequiredDueToExecutedAction = MutableStateFlow(false) /** * Whether the observer should finish the experience. Once consumed, [onFinished] must be called @@ -81,6 +100,7 @@ private constructor( */ fun onFinished() { hasCancelButtonBeenClicked.value = false + isFinishRequiredDueToExecutedAction.value = false } /** Notifies that the user has clicked the "open menu" button. */ @@ -120,8 +140,10 @@ private constructor( }, // When the cancel button is clicked, we should finish. hasCancelButtonBeenClicked, - ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked -> - selectedUserChanged || screenTurnedOff || cancelButtonClicked + // If an executed action told us to finish, we should finish, + isFinishRequiredDueToExecutedAction, + ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked, executedActionFinish -> + selectedUserChanged || screenTurnedOff || cancelButtonClicked || executedActionFinish } } @@ -164,13 +186,25 @@ private constructor( } else { LegacyUserUiHelper.getUserSwitcherActionTextResourceId( isGuest = model == UserActionModel.ENTER_GUEST_MODE, - isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated, - isGuestUserResetting = userInteractor.isGuestUserResetting, + isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated, + isGuestUserResetting = guestUserInteractor.isGuestUserResetting, isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER, isAddUser = model == UserActionModel.ADD_USER, ) }, - onClicked = { userInteractor.executeAction(action = model) }, + onClicked = { + userInteractor.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. + // + // We finish for all other actions because they navigate us away from the + // full-screen experience or are destructive (like changing to the guest user). + val shouldFinish = model != UserActionModel.ADD_USER + if (shouldFinish) { + isFinishRequiredDueToExecutedAction.value = true + } + }, ) } @@ -186,13 +220,17 @@ private constructor( @Inject constructor( private val userInteractor: UserInteractor, + private val guestUserInteractor: GuestUserInteractor, private val powerInteractor: PowerInteractor, + private val featureFlags: FeatureFlags, ) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") return UserSwitcherViewModel( userInteractor = userInteractor, + guestUserInteractor = guestUserInteractor, powerInteractor = powerInteractor, + featureFlags = featureFlags, ) as T } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt new file mode 100644 index 000000000000..0b8257da8fb5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt @@ -0,0 +1,48 @@ +/* + * 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.util.settings + +import android.annotation.UserIdInt +import android.database.ContentObserver +import android.os.UserHandle +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** Kotlin extension functions for [SettingsProxy]. */ +object SettingsProxyExt { + + /** Returns a flow of [Unit] that is invoked each time that content is updated. */ + fun SettingsProxy.observerFlow( + vararg names: String, + @UserIdInt userId: Int = UserHandle.USER_CURRENT, + ): Flow<Unit> { + return conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + names.forEach { name -> registerContentObserverForUser(name, observer, userId) } + + awaitClose { unregisterContentObserver(observer) } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java index 43f6f1aac097..c1036e356cfa 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java @@ -411,7 +411,7 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { 0 /* flags */); users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */, false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */)); + false /* isAddSupervisedUser */, null /* enforcedAdmin */)); } return users; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index ba1e168bc316..eea2e952c81f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -116,6 +116,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { val job = underTest.isKeyguardShowing.onEach { latest = it }.launchIn(this) assertThat(latest).isFalse() + assertThat(underTest.isKeyguardShowing()).isFalse() val captor = argumentCaptor<KeyguardStateController.Callback>() verify(keyguardStateController).addCallback(captor.capture()) @@ -123,10 +124,12 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { whenever(keyguardStateController.isShowing).thenReturn(true) captor.value.onKeyguardShowingChanged() assertThat(latest).isTrue() + assertThat(underTest.isKeyguardShowing()).isTrue() whenever(keyguardStateController.isShowing).thenReturn(false) captor.value.onKeyguardShowingChanged() assertThat(latest).isFalse() + assertThat(underTest.isKeyguardShowing()).isFalse() job.cancel() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt new file mode 100644 index 000000000000..773a0d8ceb64 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt @@ -0,0 +1,82 @@ +/* + * 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.telephony.data.repository + +import android.telephony.TelephonyCallback +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.telephony.TelephonyListenerManager +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class TelephonyRepositoryImplTest : SysuiTestCase() { + + @Mock private lateinit var manager: TelephonyListenerManager + + private lateinit var underTest: TelephonyRepositoryImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = + TelephonyRepositoryImpl( + manager = manager, + ) + } + + @Test + fun callState() = + runBlocking(IMMEDIATE) { + var callState: Int? = null + val job = underTest.callState.onEach { callState = it }.launchIn(this) + val listenerCaptor = kotlinArgumentCaptor<TelephonyCallback.CallStateListener>() + verify(manager).addCallStateListener(listenerCaptor.capture()) + val listener = listenerCaptor.value + + listener.onCallStateChanged(0) + assertThat(callState).isEqualTo(0) + + listener.onCallStateChanged(1) + assertThat(callState).isEqualTo(1) + + listener.onCallStateChanged(2) + assertThat(callState).isEqualTo(2) + + job.cancel() + + verify(manager).removeCallStateListener(listener) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt new file mode 100644 index 000000000000..4a8e0552d778 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt @@ -0,0 +1,204 @@ +/* + * 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.data.repository + +import android.content.pm.UserInfo +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.systemui.user.data.model.UserSwitcherSettingsModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(JUnit4::class) +class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() { + + @Before + fun setUp() { + super.setUp(isRefactored = true) + } + + @Test + fun userSwitcherSettings() = runSelfCancelingTest { + setUpGlobalSettings( + isSimpleUserSwitcher = true, + isAddUsersFromLockscreen = true, + isUserSwitcherEnabled = true, + ) + underTest = create(this) + + var value: UserSwitcherSettingsModel? = null + underTest.userSwitcherSettings.onEach { value = it }.launchIn(this) + + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = true, + expectedAddUsersFromLockscreen = true, + expectedUserSwitcherEnabled = true, + ) + + setUpGlobalSettings( + isSimpleUserSwitcher = false, + isAddUsersFromLockscreen = true, + isUserSwitcherEnabled = true, + ) + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = false, + expectedAddUsersFromLockscreen = true, + expectedUserSwitcherEnabled = true, + ) + } + + @Test + fun refreshUsers() = runSelfCancelingTest { + underTest = create(this) + val initialExpectedValue = + setUpUsers( + count = 3, + selectedIndex = 0, + ) + var userInfos: List<UserInfo>? = null + var selectedUserInfo: UserInfo? = null + underTest.userInfos.onEach { userInfos = it }.launchIn(this) + underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) + + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(initialExpectedValue) + assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0]) + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) + + val secondExpectedValue = + setUpUsers( + count = 4, + selectedIndex = 1, + ) + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(secondExpectedValue) + assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1]) + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) + + val selectedNonGuestUserId = selectedUserInfo?.id + val thirdExpectedValue = + setUpUsers( + count = 2, + hasGuest = true, + selectedIndex = 1, + ) + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(thirdExpectedValue) + assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1]) + assertThat(selectedUserInfo?.isGuest).isTrue() + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId) + } + + private fun setUpUsers( + count: Int, + hasGuest: Boolean = false, + selectedIndex: Int = 0, + ): List<UserInfo> { + val userInfos = + (0 until count).map { index -> + createUserInfo( + index, + isGuest = hasGuest && index == count - 1, + ) + } + whenever(manager.aliveUsers).thenReturn(userInfos) + tracker.set(userInfos, selectedIndex) + return userInfos + } + + private fun createUserInfo( + id: Int, + isGuest: Boolean, + ): UserInfo { + val flags = 0 + return UserInfo( + id, + "user_$id", + /* iconPath= */ "", + flags, + if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags), + ) + } + + private fun setUpGlobalSettings( + isSimpleUserSwitcher: Boolean = false, + isAddUsersFromLockscreen: Boolean = false, + isUserSwitcherEnabled: Boolean = true, + ) { + context.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_expandLockScreenUserSwitcher, + true, + ) + globalSettings.putIntForUser( + UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER, + if (isSimpleUserSwitcher) 1 else 0, + UserHandle.USER_SYSTEM, + ) + globalSettings.putIntForUser( + Settings.Global.ADD_USERS_WHEN_LOCKED, + if (isAddUsersFromLockscreen) 1 else 0, + UserHandle.USER_SYSTEM, + ) + globalSettings.putIntForUser( + Settings.Global.USER_SWITCHER_ENABLED, + if (isUserSwitcherEnabled) 1 else 0, + UserHandle.USER_SYSTEM, + ) + } + + private fun assertUserSwitcherSettings( + model: UserSwitcherSettingsModel?, + expectedSimpleUserSwitcher: Boolean, + expectedAddUsersFromLockscreen: Boolean, + expectedUserSwitcherEnabled: Boolean, + ) { + checkNotNull(model) + assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher) + assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen) + assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled) + } + + /** + * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which + * is then automatically canceled and cleaned-up. + */ + private fun runSelfCancelingTest( + block: suspend CoroutineScope.() -> Unit, + ) = + runBlocking(Dispatchers.Main.immediate) { + val scope = CoroutineScope(coroutineContext + Job()) + block(scope) + scope.cancel() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index 6fec343d036c..dcea83a55a74 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -17,201 +17,54 @@ package com.android.systemui.user.data.repository -import android.content.pm.UserInfo import android.os.UserManager -import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.settings.FakeUserTracker import com.android.systemui.statusbar.policy.UserSwitcherController -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.capture -import com.google.common.truth.Truth.assertThat +import com.android.systemui.util.settings.FakeSettings +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.ArgumentCaptor -import org.mockito.Captor +import kotlinx.coroutines.test.TestCoroutineScope import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations -@SmallTest -@RunWith(JUnit4::class) -class UserRepositoryImplTest : SysuiTestCase() { +abstract class UserRepositoryImplTest : SysuiTestCase() { - @Mock private lateinit var manager: UserManager - @Mock private lateinit var controller: UserSwitcherController - @Captor - private lateinit var userSwitchCallbackCaptor: - ArgumentCaptor<UserSwitcherController.UserSwitchCallback> + @Mock protected lateinit var manager: UserManager + @Mock protected lateinit var controller: UserSwitcherController - private lateinit var underTest: UserRepositoryImpl + protected lateinit var underTest: UserRepositoryImpl - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false)) - whenever(controller.isGuestUserAutoCreated).thenReturn(false) - whenever(controller.isGuestUserResetting).thenReturn(false) - - underTest = - UserRepositoryImpl( - appContext = context, - manager = manager, - controller = controller, - ) - } - - @Test - fun `users - registers for updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.users.onEach {}.launchIn(this) - - verify(controller).addUserSwitchCallback(any()) - - job.cancel() - } - - @Test - fun `users - unregisters from updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.users.onEach {}.launchIn(this) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - - job.cancel() - - verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) - } - - @Test - fun `users - does not include actions`() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createActionRecord(UserActionModel.ADD_USER), - createUserRecord(1), - createUserRecord(2), - createActionRecord(UserActionModel.ADD_SUPERVISED_USER), - createActionRecord(UserActionModel.ENTER_GUEST_MODE), - ) - ) - var models: List<UserModel>? = null - val job = underTest.users.onEach { models = it }.launchIn(this) - - assertThat(models).hasSize(3) - assertThat(models?.get(0)?.id).isEqualTo(0) - assertThat(models?.get(0)?.isSelected).isTrue() - assertThat(models?.get(1)?.id).isEqualTo(1) - assertThat(models?.get(1)?.isSelected).isFalse() - assertThat(models?.get(2)?.id).isEqualTo(2) - assertThat(models?.get(2)?.isSelected).isFalse() - job.cancel() - } - - @Test - fun selectedUser() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createUserRecord(1), - createUserRecord(2), - ) - ) - var id: Int? = null - val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this) + protected lateinit var globalSettings: FakeSettings + protected lateinit var tracker: FakeUserTracker + protected lateinit var featureFlags: FakeFeatureFlags - assertThat(id).isEqualTo(0) - - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0), - createUserRecord(1), - createUserRecord(2, isSelected = true), - ) - ) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - userSwitchCallbackCaptor.value.onUserSwitched() - assertThat(id).isEqualTo(2) - - job.cancel() - } - - @Test - fun `actions - unregisters from updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.actions.onEach {}.launchIn(this) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - - job.cancel() - - verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) - } - - @Test - fun `actions - registers for updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.actions.onEach {}.launchIn(this) - - verify(controller).addUserSwitchCallback(any()) - - job.cancel() - } - - @Test - fun `actopms - does not include users`() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createActionRecord(UserActionModel.ADD_USER), - createUserRecord(1), - createUserRecord(2), - createActionRecord(UserActionModel.ADD_SUPERVISED_USER), - createActionRecord(UserActionModel.ENTER_GUEST_MODE), - ) - ) - var models: List<UserActionModel>? = null - val job = underTest.actions.onEach { models = it }.launchIn(this) - - assertThat(models).hasSize(3) - assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER) - assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER) - assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE) - job.cancel() - } + protected fun setUp(isRefactored: Boolean) { + MockitoAnnotations.initMocks(this) - private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord { - return UserRecord( - info = UserInfo(id, "name$id", 0), - isCurrent = isSelected, - ) + globalSettings = FakeSettings() + tracker = FakeUserTracker() + featureFlags = FakeFeatureFlags() + featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored) } - private fun createActionRecord(action: UserActionModel): UserRecord { - return UserRecord( - isAddUser = action == UserActionModel.ADD_USER, - isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER, - isGuest = action == UserActionModel.ENTER_GUEST_MODE, + protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl { + return UserRepositoryImpl( + appContext = context, + manager = manager, + controller = controller, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + globalSettings = globalSettings, + tracker = tracker, + featureFlags = featureFlags, ) } companion object { - private val IMMEDIATE = Dispatchers.Main.immediate + @JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt new file mode 100644 index 000000000000..d4b41c18e123 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt @@ -0,0 +1,205 @@ +/* + * 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.data.repository + +import android.content.pm.UserInfo +import androidx.test.filters.SmallTest +import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(JUnit4::class) +class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() { + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } + + @Captor + private lateinit var userSwitchCallbackCaptor: + ArgumentCaptor<UserSwitcherController.UserSwitchCallback> + + @Before + fun setUp() { + super.setUp(isRefactored = false) + + whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false)) + whenever(controller.isGuestUserAutoCreated).thenReturn(false) + whenever(controller.isGuestUserResetting).thenReturn(false) + + underTest = create() + } + + @Test + fun `users - registers for updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.users.onEach {}.launchIn(this) + + verify(controller).addUserSwitchCallback(any()) + + job.cancel() + } + + @Test + fun `users - unregisters from updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.users.onEach {}.launchIn(this) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + + job.cancel() + + verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) + } + + @Test + fun `users - does not include actions`() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createActionRecord(UserActionModel.ADD_USER), + createUserRecord(1), + createUserRecord(2), + createActionRecord(UserActionModel.ADD_SUPERVISED_USER), + createActionRecord(UserActionModel.ENTER_GUEST_MODE), + ) + ) + var models: List<UserModel>? = null + val job = underTest.users.onEach { models = it }.launchIn(this) + + assertThat(models).hasSize(3) + assertThat(models?.get(0)?.id).isEqualTo(0) + assertThat(models?.get(0)?.isSelected).isTrue() + assertThat(models?.get(1)?.id).isEqualTo(1) + assertThat(models?.get(1)?.isSelected).isFalse() + assertThat(models?.get(2)?.id).isEqualTo(2) + assertThat(models?.get(2)?.isSelected).isFalse() + job.cancel() + } + + @Test + fun selectedUser() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createUserRecord(1), + createUserRecord(2), + ) + ) + var id: Int? = null + val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this) + + assertThat(id).isEqualTo(0) + + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0), + createUserRecord(1), + createUserRecord(2, isSelected = true), + ) + ) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + userSwitchCallbackCaptor.value.onUserSwitched() + assertThat(id).isEqualTo(2) + + job.cancel() + } + + @Test + fun `actions - unregisters from updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.actions.onEach {}.launchIn(this) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + + job.cancel() + + verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) + } + + @Test + fun `actions - registers for updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.actions.onEach {}.launchIn(this) + + verify(controller).addUserSwitchCallback(any()) + + job.cancel() + } + + @Test + fun `actions - does not include users`() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createActionRecord(UserActionModel.ADD_USER), + createUserRecord(1), + createUserRecord(2), + createActionRecord(UserActionModel.ADD_SUPERVISED_USER), + createActionRecord(UserActionModel.ENTER_GUEST_MODE), + ) + ) + var models: List<UserActionModel>? = null + val job = underTest.actions.onEach { models = it }.launchIn(this) + + assertThat(models).hasSize(3) + assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER) + assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER) + assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE) + job.cancel() + } + + private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord { + return UserRecord( + info = UserInfo(id, "name$id", 0), + isCurrent = isSelected, + ) + } + + private fun createActionRecord(action: UserActionModel): UserRecord { + return UserRecord( + isAddUser = action == UserActionModel.ADD_USER, + isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER, + isGuest = action == UserActionModel.ENTER_GUEST_MODE, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt new file mode 100644 index 000000000000..6b4c9ed38b47 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt @@ -0,0 +1,336 @@ +/* + * 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.admin.DevicePolicyManager +import android.content.pm.UserInfo +import android.os.UserHandle +import android.os.UserManager +import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineScope +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class GuestUserInteractorTest : SysuiTestCase() { + + @Mock private lateinit var manager: UserManager + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var uiEventLogger: UiEventLogger + @Mock private lateinit var showDialog: (ShowDialogRequestModel) -> Unit + @Mock private lateinit var dismissDialog: () -> Unit + @Mock private lateinit var selectUser: (Int) -> Unit + @Mock private lateinit var switchUser: (Int) -> Unit + + private lateinit var underTest: GuestUserInteractor + + private lateinit var scope: TestCoroutineScope + private lateinit var repository: FakeUserRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(manager.createGuest(any())).thenReturn(GUEST_USER_INFO) + + scope = TestCoroutineScope() + repository = FakeUserRepository() + repository.setUserInfos(ALL_USERS) + + underTest = + GuestUserInteractor( + applicationContext = context, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = repository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = scope, + mainDispatcher = IMMEDIATE, + repository = repository, + ), + uiEventLogger = uiEventLogger, + ) + } + + @Test + fun `onDeviceBootCompleted - allowed to add - create guest`() = + runBlocking(IMMEDIATE) { + setAllowedToAdd() + + underTest.onDeviceBootCompleted() + + verify(manager).createGuest(any()) + verify(deviceProvisionedController, never()).addCallback(any()) + } + + @Test + fun `onDeviceBootCompleted - await provisioning - and create guest`() = + runBlocking(IMMEDIATE) { + setAllowedToAdd(isAllowed = false) + underTest.onDeviceBootCompleted() + val captor = + kotlinArgumentCaptor<DeviceProvisionedController.DeviceProvisionedListener>() + verify(deviceProvisionedController).addCallback(captor.capture()) + + setAllowedToAdd(isAllowed = true) + captor.value.onDeviceProvisionedChanged() + + verify(manager).createGuest(any()) + verify(deviceProvisionedController).removeCallback(captor.value) + } + + @Test + fun createAndSwitchTo() = + runBlocking(IMMEDIATE) { + underTest.createAndSwitchTo( + showDialog = showDialog, + dismissDialog = dismissDialog, + selectUser = selectUser, + ) + + verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true)) + verify(manager).createGuest(any()) + verify(dismissDialog).invoke() + verify(selectUser).invoke(GUEST_USER_INFO.id) + } + + @Test + fun `createAndSwitchTo - fails to create - does not switch to`() = + runBlocking(IMMEDIATE) { + whenever(manager.createGuest(any())).thenReturn(null) + + underTest.createAndSwitchTo( + showDialog = showDialog, + dismissDialog = dismissDialog, + selectUser = selectUser, + ) + + verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true)) + verify(manager).createGuest(any()) + verify(dismissDialog).invoke() + verify(selectUser, never()).invoke(anyInt()) + } + + @Test + fun `exit - returns to target user`() = + runBlocking(IMMEDIATE) { + repository.setSelectedUserInfo(GUEST_USER_INFO) + + val targetUserId = NON_GUEST_USER_INFO.id + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = targetUserId, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(manager, never()).removeUser(anyInt()) + verify(switchUser).invoke(targetUserId) + } + + @Test + fun `exit - returns to last non-guest`() = + runBlocking(IMMEDIATE) { + val expectedUserId = NON_GUEST_USER_INFO.id + whenever(manager.getUserInfo(expectedUserId)).thenReturn(NON_GUEST_USER_INFO) + repository.lastSelectedNonGuestUserId = expectedUserId + repository.setSelectedUserInfo(GUEST_USER_INFO) + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = UserHandle.USER_NULL, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(manager, never()).removeUser(anyInt()) + verify(switchUser).invoke(expectedUserId) + } + + @Test + fun `exit - last non-guest was removed - returns to system`() = + runBlocking(IMMEDIATE) { + val removedUserId = 310 + repository.lastSelectedNonGuestUserId = removedUserId + repository.setSelectedUserInfo(GUEST_USER_INFO) + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = UserHandle.USER_NULL, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(manager, never()).removeUser(anyInt()) + verify(switchUser).invoke(UserHandle.USER_SYSTEM) + } + + @Test + fun `exit - guest was ephemeral - it is removed`() = + runBlocking(IMMEDIATE) { + whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) + repository.setUserInfos(listOf(NON_GUEST_USER_INFO, EPHEMERAL_GUEST_USER_INFO)) + repository.setSelectedUserInfo(EPHEMERAL_GUEST_USER_INFO) + val targetUserId = NON_GUEST_USER_INFO.id + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = targetUserId, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager).markGuestForDeletion(EPHEMERAL_GUEST_USER_INFO.id) + verify(manager).removeUser(EPHEMERAL_GUEST_USER_INFO.id) + verify(switchUser).invoke(targetUserId) + } + + @Test + fun `exit - force remove guest - it is removed`() = + runBlocking(IMMEDIATE) { + whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) + repository.setSelectedUserInfo(GUEST_USER_INFO) + val targetUserId = NON_GUEST_USER_INFO.id + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = targetUserId, + forceRemoveGuestOnExit = true, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager).markGuestForDeletion(GUEST_USER_INFO.id) + verify(manager).removeUser(GUEST_USER_INFO.id) + verify(switchUser).invoke(targetUserId) + } + + @Test + fun `exit - selected different from guest user - do nothing`() = + runBlocking(IMMEDIATE) { + repository.setSelectedUserInfo(NON_GUEST_USER_INFO) + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = 123, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verifyDidNotExit() + } + + @Test + fun `exit - selected is actually not a guest user - do nothing`() = + runBlocking(IMMEDIATE) { + repository.setSelectedUserInfo(NON_GUEST_USER_INFO) + + underTest.exit( + guestUserId = NON_GUEST_USER_INFO.id, + targetUserId = 123, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verifyDidNotExit() + } + + private fun setAllowedToAdd(isAllowed: Boolean = true) { + whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(isAllowed) + whenever(devicePolicyManager.isDeviceManaged).thenReturn(!isAllowed) + } + + private fun verifyDidNotExit() { + verify(manager, never()).getUserInfo(anyInt()) + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(uiEventLogger, never()).log(any()) + verify(showDialog, never()).invoke(any()) + verify(dismissDialog, never()).invoke() + verify(switchUser, never()).invoke(anyInt()) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private val NON_GUEST_USER_INFO = + UserInfo( + /* id= */ 818, + /* name= */ "non_guest", + /* flags= */ 0, + ) + private val GUEST_USER_INFO = + UserInfo( + /* id= */ 669, + /* name= */ "guest", + /* iconPath= */ "", + /* flags= */ 0, + UserManager.USER_TYPE_FULL_GUEST, + ) + private val EPHEMERAL_GUEST_USER_INFO = + UserInfo( + /* id= */ 669, + /* name= */ "guest", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_EPHEMERAL, + UserManager.USER_TYPE_FULL_GUEST, + ) + private val ALL_USERS = + listOf( + NON_GUEST_USER_INFO, + GUEST_USER_INFO, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt new file mode 100644 index 000000000000..593ce1f0a2f5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.user.data.repository.FakeUserRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class RefreshUsersSchedulerTest : SysuiTestCase() { + + private lateinit var underTest: RefreshUsersScheduler + + private lateinit var repository: FakeUserRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + repository = FakeUserRepository() + } + + @Test + fun `pause - prevents the next refresh from happening`() = + runBlocking(IMMEDIATE) { + underTest = + RefreshUsersScheduler( + applicationScope = this, + mainDispatcher = IMMEDIATE, + repository = repository, + ) + underTest.pause() + + underTest.refreshIfNotPaused() + assertThat(repository.refreshUsersCallCount).isEqualTo(0) + } + + @Test + fun `unpauseAndRefresh - forces the refresh even when paused`() = + runBlocking(IMMEDIATE) { + underTest = + RefreshUsersScheduler( + applicationScope = this, + mainDispatcher = IMMEDIATE, + repository = repository, + ) + underTest.pause() + + underTest.unpauseAndRefresh() + + assertThat(repository.refreshUsersCallCount).isEqualTo(1) + } + + @Test + fun `refreshIfNotPaused - refreshes when not paused`() = + runBlocking(IMMEDIATE) { + underTest = + RefreshUsersScheduler( + applicationScope = this, + mainDispatcher = IMMEDIATE, + repository = repository, + ) + underTest.refreshIfNotPaused() + + assertThat(repository.refreshUsersCallCount).isEqualTo(1) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt new file mode 100644 index 000000000000..3d5695a09ebc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt @@ -0,0 +1,658 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import android.content.Intent +import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.internal.R.drawable.ic_account_circle +import com.android.systemui.R +import com.android.systemui.common.shared.model.Text +import com.android.systemui.user.data.model.UserSwitcherSettingsModel +import com.android.systemui.user.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.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(JUnit4::class) +class UserInteractorRefactoredTest : UserInteractorTest() { + + override fun isRefactored(): Boolean { + return true + } + + @Before + override fun setUp() { + super.setUp() + + overrideResource(R.drawable.ic_account_circle, GUEST_ICON) + overrideResource(R.dimen.max_avatar_size, 10) + overrideResource( + com.android.internal.R.string.config_supervisedUserCreationPackage, + SUPERVISED_USER_CREATION_APP_PACKAGE, + ) + whenever(manager.getUserIcon(anyInt())).thenReturn(ICON) + whenever(manager.canAddMoreUsers(any())).thenReturn(true) + } + + @Test + fun `users - switcher enabled`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + assertUsers(models = value, count = 3, includeGuest = true) + + job.cancel() + } + + @Test + fun `users - switches to second user`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + userRepository.setSelectedUserInfo(userInfos[1]) + + assertUsers(models = value, count = 2, selectedIndex = 1) + job.cancel() + } + + @Test + fun `users - switcher not enabled`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + assertUsers(models = value, count = 1) + + job.cancel() + } + + @Test + fun selectedUser() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: UserModel? = null + val job = underTest.selectedUser.onEach { value = it }.launchIn(this) + assertUser(value, id = 0, isSelected = true) + + userRepository.setSelectedUserInfo(userInfos[1]) + assertUser(value, id = 1, isSelected = true) + + job.cancel() + } + + @Test + fun `actions - device unlocked`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + + job.cancel() + } + + @Test + fun `actions - device unlocked user not primary - empty list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(emptyList<UserActionModel>()) + + job.cancel() + } + + @Test + fun `actions - device unlocked user is guest - empty list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + assertThat(userInfos[1].isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(emptyList<UserActionModel>()) + + job.cancel() + } + + @Test + fun `actions - device locked add from lockscreen set - full list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings( + UserSwitcherSettingsModel( + isUserSwitcherEnabled = true, + isAddUsersFromLockscreen = true, + ) + ) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + + job.cancel() + } + + @Test + fun `actions - device locked - only guest action is shown`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(true) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(listOf(UserActionModel.ENTER_GUEST_MODE)) + + job.cancel() + } + + @Test + fun `executeAction - add user - dialog shown`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.executeAction(UserActionModel.ADD_USER) + assertThat(dialogRequest) + .isEqualTo( + ShowDialogRequestModel.ShowAddUserDialog( + userHandle = userInfos[0].userHandle, + isKeyguardShowing = false, + showEphemeralMessage = false, + ) + ) + + underTest.onDialogShown() + assertThat(dialogRequest).isNull() + + job.cancel() + } + + @Test + fun `executeAction - add supervised user - starts activity`() = + runBlocking(IMMEDIATE) { + underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) + + val intentCaptor = kotlinArgumentCaptor<Intent>() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(false)) + assertThat(intentCaptor.value.action) + .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER) + assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE) + } + + @Test + fun `executeAction - navigate to manage users`() = + runBlocking(IMMEDIATE) { + underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + + val intentCaptor = kotlinArgumentCaptor<Intent>() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(false)) + assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS) + } + + @Test + fun `executeAction - guest mode`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) + whenever(manager.createGuest(any())).thenReturn(guestUserInfo) + val dialogRequests = mutableListOf<ShowDialogRequestModel?>() + val showDialogsJob = + underTest.dialogShowRequests + .onEach { + dialogRequests.add(it) + if (it != null) { + underTest.onDialogShown() + } + } + .launchIn(this) + val dismissDialogsJob = + underTest.dialogDismissRequests + .onEach { + if (it != null) { + underTest.onDialogDismissed() + } + } + .launchIn(this) + + underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) + + assertThat(dialogRequests) + .contains( + ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true), + ) + verify(activityManager).switchUser(guestUserInfo.id) + + showDialogsJob.cancel() + dismissDialogsJob.cancel() + } + + @Test + fun `selectUser - already selected guest re-selected - exit guest dialog`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + val guestUserInfo = userInfos[1] + assertThat(guestUserInfo.isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(guestUserInfo) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = guestUserInfo.id) + + assertThat(dialogRequest) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + job.cancel() + } + + @Test + fun `selectUser - currently guest non-guest selected - exit guest dialog`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + val guestUserInfo = userInfos[1] + assertThat(guestUserInfo.isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(guestUserInfo) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = userInfos[0].id) + + assertThat(dialogRequest) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + job.cancel() + } + + @Test + fun `selectUser - not currently guest - switches users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = userInfos[1].id) + + assertThat(dialogRequest).isNull() + verify(activityManager).switchUser(userInfos[1].id) + job.cancel() + } + + @Test + fun `Telephony call state changes - refreshes users`() = + runBlocking(IMMEDIATE) { + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + telephonyRepository.setCallState(1) + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `User switched broadcast`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val callback1: UserInteractor.UserCallback = mock() + val callback2: UserInteractor.UserCallback = mock() + underTest.addCallback(callback1) + underTest.addCallback(callback2) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + userRepository.setSelectedUserInfo(userInfos[1]) + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_SWITCHED) + .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id), + ) + } + + verify(callback1).onUserStateChanged() + verify(callback2).onUserStateChanged() + assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id) + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `User info changed broadcast`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_INFO_CHANGED), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `System user unlocked broadcast - refresh users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_UNLOCKED) + .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `Non-system user unlocked broadcast - do not refresh users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount) + } + + @Test + fun userRecords() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + testCoroutineScope.advanceUntilIdle() + + 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, + ), + ) + } + + @Test + fun selectedUserRecord() = + runBlocking(IMMEDIATE) { + 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, + ) + } + + private fun assertUsers( + models: List<UserModel>?, + 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<UserRecord>, + userIds: List<Int>, + selectedUserIndex: Int = 0, + includeGuest: Boolean = false, + expectedActions: List<UserActionModel> = 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 createUserInfos( + count: Int, + includeGuest: Boolean, + ): List<UserInfo> { + return (0 until count).map { index -> + val isGuest = includeGuest && index == count - 1 + createUserInfo( + id = index, + name = + if (isGuest) { + "guest" + } else { + "user_$index" + }, + isPrimary = !isGuest && index == 0, + isGuest = isGuest, + ) + } + } + + private fun createUserInfo( + id: Int, + name: String, + isPrimary: Boolean = false, + isGuest: Boolean = false, + ): UserInfo { + return UserInfo( + id, + name, + /* iconPath= */ "", + /* flags= */ if (isPrimary) { + UserInfo.FLAG_PRIMARY + } else { + 0 + }, + if (isGuest) { + UserManager.USER_TYPE_FULL_GUEST + } else { + UserManager.USER_TYPE_FULL_SYSTEM + }, + ) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private val ICON = Bitmap.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/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index e914e2e0a1da..8465f4f46d62 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -17,51 +17,61 @@ package com.android.systemui.user.domain.interactor -import androidx.test.filters.SmallTest +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager +import android.os.UserManager +import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.data.repository.FakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.nullable -import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 +import kotlinx.coroutines.test.TestCoroutineScope import org.mockito.Mock -import org.mockito.Mockito.anyBoolean -import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -@SmallTest -@RunWith(JUnit4::class) -class UserInteractorTest : SysuiTestCase() { +abstract class UserInteractorTest : SysuiTestCase() { - @Mock private lateinit var controller: UserSwitcherController - @Mock private lateinit var activityStarter: ActivityStarter + @Mock protected lateinit var controller: UserSwitcherController + @Mock protected lateinit var activityStarter: ActivityStarter + @Mock protected lateinit var manager: UserManager + @Mock protected lateinit var activityManager: ActivityManager + @Mock protected lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock protected lateinit var devicePolicyManager: DevicePolicyManager + @Mock protected lateinit var uiEventLogger: UiEventLogger - private lateinit var underTest: UserInteractor + protected lateinit var underTest: UserInteractor - private lateinit var userRepository: FakeUserRepository - private lateinit var keyguardRepository: FakeKeyguardRepository + protected lateinit var testCoroutineScope: TestCoroutineScope + protected lateinit var userRepository: FakeUserRepository + protected lateinit var keyguardRepository: FakeKeyguardRepository + protected lateinit var telephonyRepository: FakeTelephonyRepository - @Before - fun setUp() { + abstract fun isRefactored(): Boolean + + open fun setUp() { MockitoAnnotations.initMocks(this) userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() + telephonyRepository = FakeTelephonyRepository() + testCoroutineScope = TestCoroutineScope() + val refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = testCoroutineScope, + mainDispatcher = IMMEDIATE, + repository = userRepository, + ) underTest = UserInteractor( + applicationContext = context, repository = userRepository, controller = controller, activityStarter = activityStarter, @@ -69,142 +79,34 @@ class UserInteractorTest : SysuiTestCase() { KeyguardInteractor( repository = keyguardRepository, ), - ) - } - - @Test - fun `actions - not actionable when locked and locked - no actions`() = - runBlocking(IMMEDIATE) { - userRepository.setActions(UserActionModel.values().toList()) - userRepository.setActionableWhenLocked(false) - keyguardRepository.setKeyguardShowing(true) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions).isEmpty() - job.cancel() - } - - @Test - fun `actions - not actionable when locked and not locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) - userRepository.setActionableWhenLocked(false) - keyguardRepository.setKeyguardShowing(false) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun `actions - actionable when locked and not locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) - userRepository.setActionableWhenLocked(true) - keyguardRepository.setKeyguardShowing(false) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + featureFlags = + FakeFeatureFlags().apply { + set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored()) + }, + manager = manager, + applicationScope = testCoroutineScope, + telephonyInteractor = + TelephonyInteractor( + repository = telephonyRepository, + ), + broadcastDispatcher = fakeBroadcastDispatcher, + backgroundDispatcher = IMMEDIATE, + activityManager = activityManager, + refreshUsersScheduler = refreshUsersScheduler, + guestUserInteractor = + GuestUserInteractor( + applicationContext = context, + applicationScope = testCoroutineScope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = userRepository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = refreshUsersScheduler, + uiEventLogger = uiEventLogger, ) - ) - job.cancel() - } - - @Test - fun `actions - actionable when locked and locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) ) - userRepository.setActionableWhenLocked(true) - keyguardRepository.setKeyguardShowing(true) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun selectUser() { - val userId = 3 - - underTest.selectUser(userId) - - verify(controller).onUserSelected(eq(userId), nullable()) - } - - @Test - fun `executeAction - guest`() { - underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) - - verify(controller).createAndSwitchToGuestUser(nullable()) - } - - @Test - fun `executeAction - add user`() { - underTest.executeAction(UserActionModel.ADD_USER) - - verify(controller).showAddUserDialog(nullable()) - } - - @Test - fun `executeAction - add supervised user`() { - underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) - - verify(controller).startSupervisedUserActivity() - } - - @Test - fun `executeAction - manage users`() { - underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - - verify(activityStarter).startActivity(any(), anyBoolean()) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt new file mode 100644 index 000000000000..c3a9705bf6ba --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.nullable +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(JUnit4::class) +open class UserInteractorUnrefactoredTest : UserInteractorTest() { + + override fun isRefactored(): Boolean { + return false + } + + @Before + override fun setUp() { + super.setUp() + } + + @Test + fun `actions - not actionable when locked and locked - no actions`() = + runBlocking(IMMEDIATE) { + userRepository.setActions(UserActionModel.values().toList()) + userRepository.setActionableWhenLocked(false) + keyguardRepository.setKeyguardShowing(true) + + var actions: List<UserActionModel>? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions).isEmpty() + job.cancel() + } + + @Test + fun `actions - not actionable when locked and not locked`() = + runBlocking(IMMEDIATE) { + userRepository.setActions( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + userRepository.setActionableWhenLocked(false) + keyguardRepository.setKeyguardShowing(false) + + var actions: List<UserActionModel>? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun `actions - actionable when locked and not locked`() = + runBlocking(IMMEDIATE) { + userRepository.setActions( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + userRepository.setActionableWhenLocked(true) + keyguardRepository.setKeyguardShowing(false) + + var actions: List<UserActionModel>? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun `actions - actionable when locked and locked`() = + runBlocking(IMMEDIATE) { + userRepository.setActions( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + userRepository.setActionableWhenLocked(true) + keyguardRepository.setKeyguardShowing(true) + + var actions: List<UserActionModel>? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun selectUser() { + val userId = 3 + + underTest.selectUser(userId) + + verify(controller).onUserSelected(eq(userId), nullable()) + } + + @Test + fun `executeAction - guest`() { + underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) + + verify(controller).createAndSwitchToGuestUser(nullable()) + } + + @Test + fun `executeAction - add user`() { + underTest.executeAction(UserActionModel.ADD_USER) + + verify(controller).showAddUserDialog(nullable()) + } + + @Test + fun `executeAction - add supervised user`() { + underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) + + verify(controller).startSupervisedUserActivity() + } + + @Test + fun `executeAction - manage users`() { + underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + + verify(activityStarter).startActivity(any(), anyBoolean()) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index ef4500df3600..0344e3f991e2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -17,17 +17,28 @@ package com.android.systemui.user.ui.viewmodel +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager import android.graphics.drawable.Drawable +import android.os.UserManager import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Text +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.power.data.repository.FakePowerRepository import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.data.repository.FakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.domain.interactor.GuestUserInteractor +import com.android.systemui.user.domain.interactor.RefreshUsersScheduler import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel @@ -38,6 +49,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.yield import org.junit.Before import org.junit.Test @@ -52,6 +64,11 @@ class UserSwitcherViewModelTest : SysuiTestCase() { @Mock private lateinit var controller: UserSwitcherController @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var activityManager: ActivityManager + @Mock private lateinit var manager: UserManager + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var uiEventLogger: UiEventLogger private lateinit var underTest: UserSwitcherViewModel @@ -66,22 +83,60 @@ class UserSwitcherViewModelTest : SysuiTestCase() { userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() powerRepository = FakePowerRepository() + val featureFlags = FakeFeatureFlags() + featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, true) + val scope = TestCoroutineScope() + val refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = scope, + mainDispatcher = IMMEDIATE, + repository = userRepository, + ) + val guestUserInteractor = + GuestUserInteractor( + applicationContext = context, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = userRepository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = refreshUsersScheduler, + uiEventLogger = uiEventLogger, + ) + underTest = UserSwitcherViewModel.Factory( userInteractor = UserInteractor( + applicationContext = context, repository = userRepository, controller = controller, activityStarter = activityStarter, keyguardInteractor = KeyguardInteractor( repository = keyguardRepository, - ) + ), + featureFlags = featureFlags, + manager = manager, + applicationScope = scope, + telephonyInteractor = + TelephonyInteractor( + repository = FakeTelephonyRepository(), + ), + broadcastDispatcher = fakeBroadcastDispatcher, + backgroundDispatcher = IMMEDIATE, + activityManager = activityManager, + refreshUsersScheduler = refreshUsersScheduler, + guestUserInteractor = guestUserInteractor, ), powerInteractor = PowerInteractor( repository = powerRepository, ), + featureFlags = featureFlags, + guestUserInteractor = guestUserInteractor, ) .create(UserSwitcherViewModel::class.java) } @@ -97,6 +152,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = true, isSelectable = true, + isGuest = false, ), UserModel( id = 1, @@ -104,6 +160,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = false, isSelectable = true, + isGuest = false, ), UserModel( id = 2, @@ -111,6 +168,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = false, isSelectable = false, + isGuest = false, ), ) ) @@ -260,7 +318,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { job.cancel() } - private fun setUsers(count: Int) { + private suspend fun setUsers(count: Int) { userRepository.setUsers( (0 until count).map { index -> UserModel( @@ -269,6 +327,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = index == 0, isSelectable = true, + isGuest = false, ) } ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt index 53dcc8d269c9..bb646f09b774 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt @@ -37,10 +37,18 @@ class FakeBroadcastDispatcher( dumpManager: DumpManager, logger: BroadcastDispatcherLogger, userTracker: UserTracker -) : BroadcastDispatcher( - context, looper, executor, dumpManager, logger, userTracker, PendingRemovalStore(logger)) { +) : + BroadcastDispatcher( + context, + looper, + executor, + dumpManager, + logger, + userTracker, + PendingRemovalStore(logger) + ) { - private val registeredReceivers = ArraySet<BroadcastReceiver>() + val registeredReceivers = ArraySet<BroadcastReceiver>() override fun registerReceiverWithHandler( receiver: BroadcastReceiver, @@ -78,4 +86,4 @@ class FakeBroadcastDispatcher( } registeredReceivers.clear() } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 42b434a9deaf..725b1f41372c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -44,6 +44,10 @@ class FakeKeyguardRepository : KeyguardRepository { private val _dozeAmount = MutableStateFlow(0f) override val dozeAmount: Flow<Float> = _dozeAmount + override fun isKeyguardShowing(): Boolean { + return _isKeyguardShowing.value + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.tryEmit(animate) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt index b2b176420e40..9726bf83b263 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt @@ -26,20 +26,24 @@ import java.util.concurrent.Executor /** A fake [UserTracker] to be used in tests. */ class FakeUserTracker( - userId: Int = 0, - userHandle: UserHandle = UserHandle.of(userId), - userInfo: UserInfo = mock(), - userProfiles: List<UserInfo> = emptyList(), + private var _userId: Int = 0, + private var _userHandle: UserHandle = UserHandle.of(_userId), + private var _userInfo: UserInfo = mock(), + private var _userProfiles: List<UserInfo> = emptyList(), userContentResolver: ContentResolver = MockContentResolver(), userContext: Context = mock(), private val onCreateCurrentUserContext: (Context) -> Context = { mock() }, ) : UserTracker { val callbacks = mutableListOf<UserTracker.Callback>() - override val userId: Int = userId - override val userHandle: UserHandle = userHandle - override val userInfo: UserInfo = userInfo - override val userProfiles: List<UserInfo> = userProfiles + override val userId: Int + get() = _userId + override val userHandle: UserHandle + get() = _userHandle + override val userInfo: UserInfo + get() = _userInfo + override val userProfiles: List<UserInfo> + get() = _userProfiles override val userContentResolver: ContentResolver = userContentResolver override val userContext: Context = userContext @@ -55,4 +59,13 @@ class FakeUserTracker( override fun createCurrentUserContext(context: Context): Context { return onCreateCurrentUserContext(context) } + + fun set(userInfos: List<UserInfo>, selectedUserIndex: Int) { + _userProfiles = userInfos + _userInfo = userInfos[selectedUserIndex] + _userId = _userInfo.id + _userHandle = UserHandle.of(_userId) + + callbacks.forEach { it.onUserChanged(_userId, userContext) } + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt new file mode 100644 index 000000000000..59f24ef2a706 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt @@ -0,0 +1,32 @@ +/* + * 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.telephony.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeTelephonyRepository : TelephonyRepository { + + private val _callState = MutableStateFlow(0) + override val callState: Flow<Int> = _callState.asStateFlow() + + fun setCallState(value: Int) { + _callState.value = value + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index 20f1e367944f..4df8aa42ea2f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -17,12 +17,18 @@ package com.android.systemui.user.data.repository +import android.content.pm.UserInfo +import android.os.UserHandle +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel +import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map +import kotlinx.coroutines.yield class FakeUserRepository : UserRepository { @@ -34,21 +40,71 @@ class FakeUserRepository : UserRepository { private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList()) override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow() + private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel()) + override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> = + _userSwitcherSettings.asStateFlow() + + private val _userInfos = MutableStateFlow<List<UserInfo>>(emptyList()) + override val userInfos: Flow<List<UserInfo>> = _userInfos.asStateFlow() + + private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null) + override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull() + + override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM + private val _isActionableWhenLocked = MutableStateFlow(false) override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow() private var _isGuestUserAutoCreated: Boolean = false override val isGuestUserAutoCreated: Boolean get() = _isGuestUserAutoCreated - private var _isGuestUserResetting: Boolean = false - override val isGuestUserResetting: Boolean - get() = _isGuestUserResetting + + override var isGuestUserResetting: Boolean = false + + override val isGuestUserCreationScheduled = AtomicBoolean() + + override var secondaryUserId: Int = UserHandle.USER_NULL + + override var isRefreshUsersPaused: Boolean = false + + var refreshUsersCallCount: Int = 0 + private set + + override fun refreshUsers() { + refreshUsersCallCount++ + } + + override fun getSelectedUserInfo(): UserInfo { + return checkNotNull(_selectedUserInfo.value) + } + + override fun isSimpleUserSwitcher(): Boolean { + return _userSwitcherSettings.value.isSimpleUserSwitcher + } + + fun setUserInfos(infos: List<UserInfo>) { + _userInfos.value = infos + } + + suspend fun setSelectedUserInfo(userInfo: UserInfo) { + check(_userInfos.value.contains(userInfo)) { + "Cannot select the following user, it is not in the list of user infos: $userInfo!" + } + + _selectedUserInfo.value = userInfo + yield() + } + + suspend fun setSettings(settings: UserSwitcherSettingsModel) { + _userSwitcherSettings.value = settings + yield() + } fun setUsers(models: List<UserModel>) { _users.value = models } - fun setSelectedUser(userId: Int) { + suspend fun setSelectedUser(userId: Int) { check(_users.value.find { it.id == userId } != null) { "Cannot select a user with ID $userId - no user with that ID found!" } @@ -62,6 +118,7 @@ class FakeUserRepository : UserRepository { } } ) + yield() } fun setActions(models: List<UserActionModel>) { @@ -75,8 +132,4 @@ class FakeUserRepository : UserRepository { fun setGuestUserAutoCreated(value: Boolean) { _isGuestUserAutoCreated = value } - - fun setGuestUserResetting(value: Boolean) { - _isGuestUserResetting = value - } } |