summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Ale Nijamkin <nijamkin@google.com> 2022-10-04 22:19:28 +0000
committer Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> 2022-10-04 22:19:28 +0000
commit6e543ea69dc51a21c0e6d96c2f87d0c3785dbced (patch)
treed8bd076af1fb5ac857441f9e15ac13cb7128034b
parenta60e9c21a3bd0d116f80a805f626d3f6021722a8 (diff)
parent3cd2429e55b995d140d696788465cdf4d8de440d (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>
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/Flags.java23
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt70
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java278
-rw-r--r--packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/UserModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt224
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt322
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt75
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt114
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt656
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt135
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt107
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt132
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt122
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt48
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt82
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt204
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt209
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt205
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt336
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt95
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt658
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt220
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt188
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt63
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt16
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt32
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt69
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
- }
}