Merge changes from topics "usc-less-domain", "usc-less-split-flag", "usc-less-ui" into tm-qpr-dev

* changes:
  Splits up the flag.
  UserRecord support in user interactor.
  user UI layer changes to support USC dep removal
  UserInteractor changes to not depend on USC
  GuestUserInteractor.
  RefreshScheduler.
  User data layer without dependency on USC
  Extracts dialogs out of old UserSwitcherController.
  Adds snapshot function for isKeyguardShowing.
  Adds telephony repository and interactor.
  Moves enforcedAdmin into UserRecord.
This commit is contained in:
Ale Nijamkin
2022-10-04 21:58:24 +00:00
committed by Android (Google) Code Review
46 changed files with 4364 additions and 688 deletions

View File

@@ -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.SmartRepliesInflationModule;
import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule; import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule;
import com.android.systemui.statusbar.window.StatusBarWindowModule; 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.tuner.dagger.TunerModule;
import com.android.systemui.unfold.SysUIUnfoldModule; import com.android.systemui.unfold.SysUIUnfoldModule;
import com.android.systemui.user.UserModule; import com.android.systemui.user.UserModule;
@@ -145,6 +146,7 @@ import dagger.Provides;
StatusBarWindowModule.class, StatusBarWindowModule.class,
SysUIConcurrencyModule.class, SysUIConcurrencyModule.class,
SysUIUnfoldModule.class, SysUIUnfoldModule.class,
TelephonyRepositoryModule.class,
TunerModule.class, TunerModule.class,
UserModule.class, UserModule.class,
UtilModule.class, UtilModule.class,

View File

@@ -104,9 +104,26 @@ public class Flags {
public static final ReleasedFlag MODERN_USER_SWITCHER_ACTIVITY = public static final ReleasedFlag MODERN_USER_SWITCHER_ACTIVITY =
new ReleasedFlag(209, true); new ReleasedFlag(209, true);
/** Whether the new implementation of UserSwitcherController should be used. */ /**
public static final UnreleasedFlag REFACTORED_USER_SWITCHER_CONTROLLER = * Whether the user interactor and repository should use `UserSwitcherController`.
new UnreleasedFlag(210, false); *
* <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 // 300 - power menu

View File

@@ -85,6 +85,15 @@ interface KeyguardRepository {
*/ */
val dozeAmount: Flow<Float> 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. */ /** Sets whether the bottom area UI should animate the transition out of doze state. */
fun setAnimateDozingTransitions(animate: Boolean) fun setAnimateDozingTransitions(animate: Boolean)
@@ -103,7 +112,7 @@ class KeyguardRepositoryImpl
@Inject @Inject
constructor( constructor(
statusBarStateController: StatusBarStateController, statusBarStateController: StatusBarStateController,
keyguardStateController: KeyguardStateController, private val keyguardStateController: KeyguardStateController,
dozeHost: DozeHost, dozeHost: DozeHost,
) : KeyguardRepository { ) : KeyguardRepository {
private val _animateBottomAreaDozingTransitions = MutableStateFlow(false) private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
@@ -168,6 +177,10 @@ constructor(
awaitClose { statusBarStateController.removeCallback(callback) } awaitClose { statusBarStateController.removeCallback(callback) }
} }
override fun isKeyguardShowing(): Boolean {
return keyguardStateController.isShowing
}
override fun setAnimateDozingTransitions(animate: Boolean) { override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.value = animate _animateBottomAreaDozingTransitions.value = animate
} }

View File

@@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.Flow
class KeyguardInteractor class KeyguardInteractor
@Inject @Inject
constructor( 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 * 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 val isDozing: Flow<Boolean> = repository.isDozing
/** Whether the keyguard is showing ot not. */ /** Whether the keyguard is showing ot not. */
val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
fun isKeyguardShowing(): Boolean {
return repository.isKeyguardShowing()
}
} }

View File

@@ -134,7 +134,7 @@ public class UserDetailView extends PseudoGridView {
v.bind(name, drawable, item.info.id); v.bind(name, drawable, item.info.id);
} }
v.setActivated(item.isCurrent); v.setActivated(item.isCurrent);
v.setDisabledByAdmin(mController.isDisabledByAdmin(item)); v.setDisabledByAdmin(item.isDisabledByAdmin());
v.setEnabled(item.isSwitchToEnabled); v.setEnabled(item.isSwitchToEnabled);
UserSwitcherController.setSelectableAlpha(v); UserSwitcherController.setSelectableAlpha(v);
@@ -173,16 +173,16 @@ public class UserDetailView extends PseudoGridView {
Trace.beginSection("UserDetailView.Adapter#onClick"); Trace.beginSection("UserDetailView.Adapter#onClick");
UserRecord userRecord = UserRecord userRecord =
(UserRecord) view.getTag(); (UserRecord) view.getTag();
if (mController.isDisabledByAdmin(userRecord)) { if (userRecord.isDisabledByAdmin()) {
final Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent( final Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
mContext, mController.getEnforcedAdmin(userRecord)); mContext, userRecord.enforcedAdmin);
mController.startActivity(intent); mController.startActivity(intent);
} else if (userRecord.isSwitchToEnabled) { } else if (userRecord.isSwitchToEnabled) {
MetricsLogger.action(mContext, MetricsEvent.QS_SWITCH_USER); MetricsLogger.action(mContext, MetricsEvent.QS_SWITCH_USER);
mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH); mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH);
if (!userRecord.isAddUser if (!userRecord.isAddUser
&& !userRecord.isRestricted && !userRecord.isRestricted
&& !mController.isDisabledByAdmin(userRecord)) { && !userRecord.isDisabledByAdmin()) {
if (mCurrentUserView != null) { if (mCurrentUserView != null) {
mCurrentUserView.setActivated(false); mCurrentUserView.setActivated(false);
} }

View File

@@ -505,7 +505,7 @@ public class KeyguardUserSwitcherController extends ViewController<KeyguardUserS
v.bind(name, drawable, item.info.id); v.bind(name, drawable, item.info.id);
} }
v.setActivated(item.isCurrent); v.setActivated(item.isCurrent);
v.setDisabledByAdmin(getController().isDisabledByAdmin(item)); v.setDisabledByAdmin(item.isDisabledByAdmin());
v.setEnabled(item.isSwitchToEnabled); v.setEnabled(item.isSwitchToEnabled);
UserSwitcherController.setSelectableAlpha(v); UserSwitcherController.setSelectableAlpha(v);

View File

@@ -19,7 +19,6 @@ package com.android.systemui.statusbar.policy
import android.annotation.UserIdInt import android.annotation.UserIdInt
import android.content.Intent import android.content.Intent
import android.view.View import android.view.View
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
import com.android.systemui.Dumpable import com.android.systemui.Dumpable
import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
import com.android.systemui.user.data.source.UserRecord import com.android.systemui.user.data.source.UserRecord
@@ -130,12 +129,6 @@ interface UserSwitcherController : Dumpable {
/** Whether keyguard is showing. */ /** Whether keyguard is showing. */
val isKeyguardShowing: Boolean 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]. */ /** Starts an activity with the given [Intent]. */
fun startActivity(intent: Intent) fun startActivity(intent: Intent)

View File

@@ -19,7 +19,6 @@ package com.android.systemui.statusbar.policy
import android.content.Intent import android.content.Intent
import android.view.View import android.view.View
import com.android.settingslib.RestrictedLockUtils
import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags import com.android.systemui.flags.Flags
import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.qs.user.UserSwitchDialogController
@@ -38,8 +37,9 @@ constructor(
@Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>, @Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>,
) : UserSwitcherController { ) : UserSwitcherController {
private val isNewImpl: Boolean private val useInteractor: Boolean =
get() = flags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER) flags.isEnabled(Flags.USER_CONTROLLER_USES_INTERACTOR) &&
!flags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
private val _oldImpl: UserSwitcherControllerOldImpl private val _oldImpl: UserSwitcherControllerOldImpl
get() = oldImpl.get() get() = oldImpl.get()
@@ -49,7 +49,7 @@ constructor(
override val users: ArrayList<UserRecord> override val users: ArrayList<UserRecord>
get() = get() =
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.users _oldImpl.users
@@ -57,14 +57,14 @@ constructor(
override val isSimpleUserSwitcher: Boolean override val isSimpleUserSwitcher: Boolean
get() = get() =
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.isSimpleUserSwitcher _oldImpl.isSimpleUserSwitcher
} }
override fun init(view: View) { override fun init(view: View) {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.init(view) _oldImpl.init(view)
@@ -73,7 +73,7 @@ constructor(
override val currentUserRecord: UserRecord? override val currentUserRecord: UserRecord?
get() = get() =
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.currentUserRecord _oldImpl.currentUserRecord
@@ -81,7 +81,7 @@ constructor(
override val currentUserName: String? override val currentUserName: String?
get() = get() =
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.currentUserName _oldImpl.currentUserName
@@ -91,7 +91,7 @@ constructor(
userId: Int, userId: Int,
dialogShower: UserSwitchDialogController.DialogShower? dialogShower: UserSwitchDialogController.DialogShower?
) { ) {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.onUserSelected(userId, dialogShower) _oldImpl.onUserSelected(userId, dialogShower)
@@ -100,7 +100,7 @@ constructor(
override val isAddUsersFromLockScreenEnabled: Flow<Boolean> override val isAddUsersFromLockScreenEnabled: Flow<Boolean>
get() = get() =
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.isAddUsersFromLockScreenEnabled _oldImpl.isAddUsersFromLockScreenEnabled
@@ -108,7 +108,7 @@ constructor(
override val isGuestUserAutoCreated: Boolean override val isGuestUserAutoCreated: Boolean
get() = get() =
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.isGuestUserAutoCreated _oldImpl.isGuestUserAutoCreated
@@ -116,7 +116,7 @@ constructor(
override val isGuestUserResetting: Boolean override val isGuestUserResetting: Boolean
get() = get() =
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.isGuestUserResetting _oldImpl.isGuestUserResetting
@@ -125,7 +125,7 @@ constructor(
override fun createAndSwitchToGuestUser( override fun createAndSwitchToGuestUser(
dialogShower: UserSwitchDialogController.DialogShower?, dialogShower: UserSwitchDialogController.DialogShower?,
) { ) {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.createAndSwitchToGuestUser(dialogShower) _oldImpl.createAndSwitchToGuestUser(dialogShower)
@@ -133,7 +133,7 @@ constructor(
} }
override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) { override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.showAddUserDialog(dialogShower) _oldImpl.showAddUserDialog(dialogShower)
@@ -141,7 +141,7 @@ constructor(
} }
override fun startSupervisedUserActivity() { override fun startSupervisedUserActivity() {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.startSupervisedUserActivity() _oldImpl.startSupervisedUserActivity()
@@ -149,7 +149,7 @@ constructor(
} }
override fun onDensityOrFontScaleChanged() { override fun onDensityOrFontScaleChanged() {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.onDensityOrFontScaleChanged() _oldImpl.onDensityOrFontScaleChanged()
@@ -157,7 +157,7 @@ constructor(
} }
override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) { override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.addAdapter(adapter) _oldImpl.addAdapter(adapter)
@@ -168,7 +168,7 @@ constructor(
record: UserRecord, record: UserRecord,
dialogShower: UserSwitchDialogController.DialogShower?, dialogShower: UserSwitchDialogController.DialogShower?,
) { ) {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.onUserListItemClicked(record, dialogShower) _oldImpl.onUserListItemClicked(record, dialogShower)
@@ -176,7 +176,7 @@ constructor(
} }
override fun removeGuestUser(guestUserId: Int, targetUserId: Int) { override fun removeGuestUser(guestUserId: Int, targetUserId: Int) {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.removeGuestUser(guestUserId, targetUserId) _oldImpl.removeGuestUser(guestUserId, targetUserId)
@@ -188,7 +188,7 @@ constructor(
targetUserId: Int, targetUserId: Int,
forceRemoveGuestOnExit: Boolean forceRemoveGuestOnExit: Boolean
) { ) {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) _oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
@@ -196,7 +196,7 @@ constructor(
} }
override fun schedulePostBootGuestCreation() { override fun schedulePostBootGuestCreation() {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.schedulePostBootGuestCreation() _oldImpl.schedulePostBootGuestCreation()
@@ -205,30 +205,14 @@ constructor(
override val isKeyguardShowing: Boolean override val isKeyguardShowing: Boolean
get() = get() =
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.isKeyguardShowing _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) { override fun startActivity(intent: Intent) {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.startActivity(intent) _oldImpl.startActivity(intent)
@@ -236,7 +220,7 @@ constructor(
} }
override fun refreshUsers(forcePictureLoadForId: Int) { override fun refreshUsers(forcePictureLoadForId: Int) {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.refreshUsers(forcePictureLoadForId) _oldImpl.refreshUsers(forcePictureLoadForId)
@@ -244,7 +228,7 @@ constructor(
} }
override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) { override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.addUserSwitchCallback(callback) _oldImpl.addUserSwitchCallback(callback)
@@ -252,7 +236,7 @@ constructor(
} }
override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) { override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.removeUserSwitchCallback(callback) _oldImpl.removeUserSwitchCallback(callback)
@@ -260,7 +244,7 @@ constructor(
} }
override fun dump(pw: PrintWriter, args: Array<out String>) { override fun dump(pw: PrintWriter, args: Array<out String>) {
if (isNewImpl) { if (useInteractor) {
notYetImplemented() notYetImplemented()
} else { } else {
_oldImpl.dump(pw, args) _oldImpl.dump(pw, args)

View File

@@ -17,17 +17,13 @@ package com.android.systemui.statusbar.policy;
import static android.os.UserManager.SWITCHABILITY_STATUS_OK; import static android.os.UserManager.SWITCHABILITY_STATUS_OK;
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import android.annotation.UserIdInt; import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.app.IActivityManager; import android.app.IActivityManager;
import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.pm.UserInfo; import android.content.pm.UserInfo;
@@ -40,7 +36,6 @@ import android.os.UserManager;
import android.provider.Settings; import android.provider.Settings;
import android.telephony.TelephonyCallback; import android.telephony.TelephonyCallback;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log; import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
import android.util.SparseBooleanArray; import android.util.SparseBooleanArray;
@@ -49,17 +44,14 @@ import android.view.WindowManagerGlobal;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.collection.SimpleArrayMap;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLogger;
import com.android.internal.util.LatencyTracker; import com.android.internal.util.LatencyTracker;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.users.UserCreatingDialog; import com.android.settingslib.users.UserCreatingDialog;
import com.android.systemui.GuestResetOrExitSessionReceiver; import com.android.systemui.GuestResetOrExitSessionReceiver;
import com.android.systemui.GuestResumeSessionReceiver; import com.android.systemui.GuestResumeSessionReceiver;
import com.android.systemui.R;
import com.android.systemui.SystemUISecondaryUserService; import com.android.systemui.SystemUISecondaryUserService;
import com.android.systemui.animation.DialogCuj; import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogLaunchAnimator; 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.QSUserSwitcherEvent;
import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower; import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower;
import com.android.systemui.settings.UserTracker; import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.telephony.TelephonyListenerManager; 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.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.GlobalSettings;
import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.settings.SecureSettings;
@@ -139,9 +133,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
private final InteractionJankMonitor mInteractionJankMonitor; private final InteractionJankMonitor mInteractionJankMonitor;
private final LatencyTracker mLatencyTracker; private final LatencyTracker mLatencyTracker;
private final DialogLaunchAnimator mDialogLaunchAnimator; 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<>(); private ArrayList<UserRecord> mUsers = new ArrayList<>();
@VisibleForTesting @VisibleForTesting
@@ -334,7 +325,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
for (UserInfo info : infos) { for (UserInfo info : infos) {
boolean isCurrent = currentId == info.id; boolean isCurrent = currentId == info.id;
boolean switchToEnabled = canSwitchUsers || isCurrent;
if (!mUserSwitcherEnabled && !info.isPrimary()) { if (!mUserSwitcherEnabled && !info.isPrimary()) {
continue; continue;
} }
@@ -343,25 +333,22 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
if (info.isGuest()) { if (info.isGuest()) {
// Tapping guest icon triggers remove and a user switch therefore // Tapping guest icon triggers remove and a user switch therefore
// the icon shouldn't be enabled even if the user is current // the icon shouldn't be enabled even if the user is current
guestRecord = new UserRecord(info, null /* picture */, guestRecord = LegacyUserDataHelper.createRecord(
true /* isGuest */, isCurrent, false /* isAddUser */, mContext,
false /* isRestricted */, canSwitchUsers, mUserManager,
false /* isAddSupervisedUser */); null /* picture */,
info,
isCurrent,
canSwitchUsers);
} else if (info.supportsSwitchToByUser()) { } else if (info.supportsSwitchToByUser()) {
Bitmap picture = bitmaps.get(info.id); records.add(
if (picture == null) { LegacyUserDataHelper.createRecord(
picture = mUserManager.getUserIcon(info.id); mContext,
mUserManager,
if (picture != null) { bitmaps.get(info.id),
int avatarSize = mContext.getResources() info,
.getDimensionPixelSize(R.dimen.max_avatar_size); isCurrent,
picture = Bitmap.createScaledBitmap( canSwitchUsers));
picture, avatarSize, avatarSize, true);
}
}
records.add(new UserRecord(info, picture, false /* isGuest */,
isCurrent, false /* isAddUser */, false /* isRestricted */,
switchToEnabled, false /* isAddSupervisedUser */));
} }
} }
} }
@@ -372,18 +359,20 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
// we will just use it as an indicator for "Resetting guest...". // we will just use it as an indicator for "Resetting guest...".
// Otherwise, default to canSwitchUsers. // Otherwise, default to canSwitchUsers.
boolean isSwitchToGuestEnabled = !mGuestIsResetting.get() && canSwitchUsers; boolean isSwitchToGuestEnabled = !mGuestIsResetting.get() && canSwitchUsers;
guestRecord = new UserRecord(null /* info */, null /* picture */, guestRecord = LegacyUserDataHelper.createRecord(
true /* isGuest */, false /* isCurrent */, mContext,
false /* isAddUser */, false /* isRestricted */, currentId,
isSwitchToGuestEnabled, false /* isAddSupervisedUser */); UserActionModel.ENTER_GUEST_MODE,
checkIfAddUserDisallowedByAdminOnly(guestRecord); false /* isRestricted */,
isSwitchToGuestEnabled);
records.add(guestRecord); records.add(guestRecord);
} else if (canCreateGuest(guestRecord != null)) { } else if (canCreateGuest(guestRecord != null)) {
guestRecord = new UserRecord(null /* info */, null /* picture */, guestRecord = LegacyUserDataHelper.createRecord(
true /* isGuest */, false /* isCurrent */, mContext,
false /* isAddUser */, createIsRestricted(), canSwitchUsers, currentId,
false /* isAddSupervisedUser */); UserActionModel.ENTER_GUEST_MODE,
checkIfAddUserDisallowedByAdminOnly(guestRecord); false /* isRestricted */,
canSwitchUsers);
records.add(guestRecord); records.add(guestRecord);
} }
} else { } else {
@@ -391,20 +380,23 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
} }
if (canCreateUser()) { if (canCreateUser()) {
UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */, final UserRecord userRecord = LegacyUserDataHelper.createRecord(
false /* isGuest */, false /* isCurrent */, true /* isAddUser */, mContext,
createIsRestricted(), canSwitchUsers, currentId,
false /* isAddSupervisedUser */); UserActionModel.ADD_USER,
checkIfAddUserDisallowedByAdminOnly(addUserRecord); createIsRestricted(),
records.add(addUserRecord); canSwitchUsers);
records.add(userRecord);
} }
if (canCreateSupervisedUser()) { if (canCreateSupervisedUser()) {
UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */, final UserRecord userRecord = LegacyUserDataHelper.createRecord(
false /* isGuest */, false /* isCurrent */, false /* isAddUser */, mContext,
createIsRestricted(), canSwitchUsers, true /* isAddSupervisedUser */); currentId,
checkIfAddUserDisallowedByAdminOnly(addUserRecord); UserActionModel.ADD_SUPERVISED_USER,
records.add(addUserRecord); createIsRestricted(),
canSwitchUsers);
records.add(userRecord);
} }
mUiExecutor.execute(() -> { mUiExecutor.execute(() -> {
@@ -591,12 +583,23 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
showExitGuestDialog(id, isGuestEphemeral, newId, dialogShower); showExitGuestDialog(id, isGuestEphemeral, newId, dialogShower);
} }
private void showExitGuestDialog(int id, boolean isGuestEphemeral, private void showExitGuestDialog(
int targetId, DialogShower dialogShower) { int id,
boolean isGuestEphemeral,
int targetId,
DialogShower dialogShower) {
if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) { if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
mExitGuestDialog.cancel(); 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) { if (dialogShower != null) {
dialogShower.showDialog(mExitGuestDialog, new DialogCuj( dialogShower.showDialog(mExitGuestDialog, new DialogCuj(
InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
@@ -622,7 +625,15 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
if (mAddUserDialog != null && mAddUserDialog.isShowing()) { if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
mAddUserDialog.cancel(); 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) { if (dialogShower != null) {
dialogShower.showDialog(mAddUserDialog, dialogShower.showDialog(mAddUserDialog,
new DialogCuj( new DialogCuj(
@@ -964,30 +975,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
return mKeyguardStateController.isShowing(); 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() { private boolean shouldUseSimpleUserSwitcher() {
int defaultSimpleUserSwitcher = mContext.getResources().getBoolean( int defaultSimpleUserSwitcher = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_expandLockScreenUserSwitcher) ? 1 : 0; 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());
}
}
}
} }

View File

@@ -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) }
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -20,6 +20,7 @@ import android.app.Activity;
import com.android.settingslib.users.EditUserInfoController; import com.android.settingslib.users.EditUserInfoController;
import com.android.systemui.user.data.repository.UserRepositoryModule; import com.android.systemui.user.data.repository.UserRepositoryModule;
import com.android.systemui.user.ui.dialog.UserDialogModule;
import dagger.Binds; import dagger.Binds;
import dagger.Module; import dagger.Module;
@@ -32,6 +33,7 @@ import dagger.multibindings.IntoMap;
*/ */
@Module( @Module(
includes = { includes = {
UserDialogModule.class,
UserRepositoryModule.class, UserRepositoryModule.class,
} }
) )

View File

@@ -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,
)

View File

@@ -18,9 +18,13 @@
package com.android.systemui.user.data.repository package com.android.systemui.user.data.repository
import android.content.Context import android.content.Context
import android.content.pm.UserInfo
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.UserHandle
import android.os.UserManager import android.os.UserManager
import android.provider.Settings
import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import com.android.internal.util.UserIcons import com.android.internal.util.UserIcons
import com.android.systemui.R 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.common.shared.model.Text
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application 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.statusbar.policy.UserSwitcherController
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.user.data.source.UserRecord import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel 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 javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow 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.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. * Acts as source of truth for user related data.
@@ -55,6 +80,18 @@ interface UserRepository {
/** List of available user-related actions. */ /** List of available user-related actions. */
val actions: Flow<List<UserActionModel>> 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. */ /** Whether actions are available even when locked. */
val isActionableWhenLocked: Flow<Boolean> val isActionableWhenLocked: Flow<Boolean>
@@ -62,7 +99,23 @@ interface UserRepository {
val isGuestUserAutoCreated: Boolean val isGuestUserAutoCreated: Boolean
/** Whether the guest user is currently being reset. */ /** 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 @SysUISingleton
@@ -71,9 +124,31 @@ class UserRepositoryImpl
constructor( constructor(
@Application private val appContext: Context, @Application private val appContext: Context,
private val manager: UserManager, 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 { ) : 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 { private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow {
fun send() { fun send() {
trySendWithFailureLogging( trySendWithFailureLogging(
@@ -99,11 +174,148 @@ constructor(
override val actions: Flow<List<UserActionModel>> = override val actions: Flow<List<UserActionModel>> =
userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } } 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 = controller.isGuestUserAutoCreated override val isGuestUserAutoCreated: Boolean =
if (isNewImpl) {
appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated)
} else {
controller.isGuestUserAutoCreated
}
override val isGuestUserResetting: Boolean = controller.isGuestUserResetting 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 var isRefreshUsersPaused: Boolean = false
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 { private fun UserRecord.isUser(): Boolean {
return when { return when {
@@ -125,6 +337,7 @@ constructor(
image = getUserImage(this), image = getUserImage(this),
isSelected = isCurrent, isSelected = isCurrent,
isSelectable = isSwitchToEnabled || isGuest, isSelectable = isSwitchToEnabled || isGuest,
isGuest = isGuest,
) )
} }
@@ -162,5 +375,6 @@ constructor(
companion object { companion object {
private const val TAG = "UserRepository" private const val TAG = "UserRepository"
@VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher"
} }
} }

View File

@@ -19,6 +19,7 @@ package com.android.systemui.user.data.source
import android.content.pm.UserInfo import android.content.pm.UserInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.UserHandle 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. */ /** Encapsulates raw data for a user or an option item related to managing users on the device. */
data class UserRecord( data class UserRecord(
@@ -41,6 +42,11 @@ data class UserRecord(
@JvmField val isSwitchToEnabled: Boolean = false, @JvmField val isSwitchToEnabled: Boolean = false,
/** Whether this record represents an option to add another supervised user to the device. */ /** Whether this record represents an option to add another supervised user to the device. */
@JvmField val isAddSupervisedUser: Boolean = false, @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. */ /** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */
fun copyWithIsCurrent(isCurrent: Boolean): UserRecord { 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 { companion object {
@JvmStatic @JvmStatic
fun createForGuest(): UserRecord { fun createForGuest(): UserRecord {

View File

@@ -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"
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -17,94 +17,664 @@
package com.android.systemui.user.domain.interactor 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.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.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.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.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.policy.UserSwitcherController 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.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.UserActionModel
import com.android.systemui.user.shared.model.UserModel 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 javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow 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.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map 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. */ /** Encapsulates business logic to interact with user data and systems. */
@SysUISingleton @SysUISingleton
class UserInteractor class UserInteractor
@Inject @Inject
constructor( constructor(
repository: UserRepository, @Application private val applicationContext: Context,
private val repository: UserRepository,
private val controller: UserSwitcherController, private val controller: UserSwitcherController,
private val activityStarter: ActivityStarter, 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. */ /** 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. */ /** 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. */ /** List of user-switcher related actions that are available. */
val actions: Flow<List<UserActionModel>> = val actions: Flow<List<UserActionModel>>
combine( get() =
repository.isActionableWhenLocked, if (isNewImpl) {
keyguardInteractor.isKeyguardShowing, combine(
) { isActionableWhenLocked, isLocked -> repository.userInfos,
isActionableWhenLocked || !isLocked repository.userSwitcherSettings,
} keyguardInteractor.isKeyguardShowing,
.flatMapLatest { isActionable -> ) { userInfos, settings, isDeviceLocked ->
if (isActionable) { buildList {
repository.actions.map { actions -> val hasGuestUser = userInfos.any { it.isGuest }
actions + if (
if (actions.isNotEmpty()) { !hasGuestUser &&
// If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because (guestUserInteractor.isGuestUserAutoCreated ||
// that's a user UserActionsUtil.canCreateGuest(
// switcher specific action that is not known to the our data source manager,
// or other repository,
// features. settings.isUserSwitcherEnabled,
listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) settings.isAddUsersFromLockscreen,
} else { ))
// If no actions, don't add the navigate action. ) {
emptyList() 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. */ /** 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. */ /** 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. */ /** Switches to the user with the given user ID. */
fun selectUser( 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. */ /** Executes the given action. */
fun executeAction(action: UserActionModel) { fun executeAction(action: UserActionModel) {
when (action) { if (isNewImpl) {
UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null) when (action) {
UserActionModel.ADD_USER -> controller.showAddUserDialog(null) UserActionModel.ENTER_GUEST_MODE ->
UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity() guestUserInteractor.createAndSwitchTo(
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> this::showDialog,
activityStarter.startActivity( this::dismissDialog,
Intent(Settings.ACTION_USER_SETTINGS), this::selectUser,
/* dismissShade= */ false, )
) 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"
}
} }

View File

@@ -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()
}

View File

@@ -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,
)
}
}

View File

@@ -32,4 +32,6 @@ data class UserModel(
val isSelected: Boolean, val isSelected: Boolean,
/** Whether this use is selectable. A non-selectable user cannot be switched to. */ /** Whether this use is selectable. A non-selectable user cannot be switched to. */
val isSelectable: Boolean, val isSelectable: Boolean,
/** Whether this model represents the guest user. */
val isGuest: Boolean,
) )

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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()
}
}
}
}

View File

@@ -21,7 +21,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.common.ui.drawable.CircularDrawable 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.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.domain.interactor.UserInteractor
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserActionModel
@@ -36,9 +39,14 @@ import kotlinx.coroutines.flow.map
class UserSwitcherViewModel class UserSwitcherViewModel
private constructor( private constructor(
private val userInteractor: UserInteractor, private val userInteractor: UserInteractor,
private val guestUserInteractor: GuestUserInteractor,
private val powerInteractor: PowerInteractor, private val powerInteractor: PowerInteractor,
private val featureFlags: FeatureFlags,
) : ViewModel() { ) : ViewModel() {
private val isNewImpl: Boolean
get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
/** On-device users. */ /** On-device users. */
val users: Flow<List<UserViewModel>> = val users: Flow<List<UserViewModel>> =
userInteractor.users.map { models -> models.map { user -> toViewModel(user) } } userInteractor.users.map { models -> models.map { user -> toViewModel(user) } }
@@ -47,9 +55,6 @@ private constructor(
val maximumUserColumns: Flow<Int> = val maximumUserColumns: Flow<Int> =
users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) } 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) private val _isMenuVisible = MutableStateFlow(false)
/** /**
* Whether the user action menu should be shown. Once the action menu is dismissed/closed, the * 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 val isMenuVisible: Flow<Boolean> = _isMenuVisible
/** The user action menu. */ /** The user action menu. */
val menu: Flow<List<UserActionViewModel>> = 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 hasCancelButtonBeenClicked = MutableStateFlow(false)
private val isFinishRequiredDueToExecutedAction = MutableStateFlow(false)
/** /**
* Whether the observer should finish the experience. Once consumed, [onFinished] must be called * Whether the observer should finish the experience. Once consumed, [onFinished] must be called
@@ -81,6 +100,7 @@ private constructor(
*/ */
fun onFinished() { fun onFinished() {
hasCancelButtonBeenClicked.value = false hasCancelButtonBeenClicked.value = false
isFinishRequiredDueToExecutedAction.value = false
} }
/** Notifies that the user has clicked the "open menu" button. */ /** 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. // When the cancel button is clicked, we should finish.
hasCancelButtonBeenClicked, hasCancelButtonBeenClicked,
) { selectedUserChanged, screenTurnedOff, cancelButtonClicked -> // If an executed action told us to finish, we should finish,
selectedUserChanged || screenTurnedOff || cancelButtonClicked isFinishRequiredDueToExecutedAction,
) { selectedUserChanged, screenTurnedOff, cancelButtonClicked, executedActionFinish ->
selectedUserChanged || screenTurnedOff || cancelButtonClicked || executedActionFinish
} }
} }
@@ -164,13 +186,25 @@ private constructor(
} else { } else {
LegacyUserUiHelper.getUserSwitcherActionTextResourceId( LegacyUserUiHelper.getUserSwitcherActionTextResourceId(
isGuest = model == UserActionModel.ENTER_GUEST_MODE, isGuest = model == UserActionModel.ENTER_GUEST_MODE,
isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated, isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated,
isGuestUserResetting = userInteractor.isGuestUserResetting, isGuestUserResetting = guestUserInteractor.isGuestUserResetting,
isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER, isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
isAddUser = model == UserActionModel.ADD_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 @Inject
constructor( constructor(
private val userInteractor: UserInteractor, private val userInteractor: UserInteractor,
private val guestUserInteractor: GuestUserInteractor,
private val powerInteractor: PowerInteractor, private val powerInteractor: PowerInteractor,
private val featureFlags: FeatureFlags,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return UserSwitcherViewModel( return UserSwitcherViewModel(
userInteractor = userInteractor, userInteractor = userInteractor,
guestUserInteractor = guestUserInteractor,
powerInteractor = powerInteractor, powerInteractor = powerInteractor,
featureFlags = featureFlags,
) )
as T as T
} }

View File

@@ -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) }
}
}
}

View File

@@ -411,7 +411,7 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase {
0 /* flags */); 0 /* flags */);
users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */, users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */,
false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */, false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */,
false /* isAddSupervisedUser */)); false /* isAddSupervisedUser */, null /* enforcedAdmin */));
} }
return users; return users;
} }

View File

@@ -116,6 +116,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
val job = underTest.isKeyguardShowing.onEach { latest = it }.launchIn(this) val job = underTest.isKeyguardShowing.onEach { latest = it }.launchIn(this)
assertThat(latest).isFalse() assertThat(latest).isFalse()
assertThat(underTest.isKeyguardShowing()).isFalse()
val captor = argumentCaptor<KeyguardStateController.Callback>() val captor = argumentCaptor<KeyguardStateController.Callback>()
verify(keyguardStateController).addCallback(captor.capture()) verify(keyguardStateController).addCallback(captor.capture())
@@ -123,10 +124,12 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
whenever(keyguardStateController.isShowing).thenReturn(true) whenever(keyguardStateController.isShowing).thenReturn(true)
captor.value.onKeyguardShowingChanged() captor.value.onKeyguardShowingChanged()
assertThat(latest).isTrue() assertThat(latest).isTrue()
assertThat(underTest.isKeyguardShowing()).isTrue()
whenever(keyguardStateController.isShowing).thenReturn(false) whenever(keyguardStateController.isShowing).thenReturn(false)
captor.value.onKeyguardShowingChanged() captor.value.onKeyguardShowingChanged()
assertThat(latest).isFalse() assertThat(latest).isFalse()
assertThat(underTest.isKeyguardShowing()).isFalse()
job.cancel() job.cancel()
} }

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -17,201 +17,54 @@
package com.android.systemui.user.data.repository package com.android.systemui.user.data.repository
import android.content.pm.UserInfo
import android.os.UserManager import android.os.UserManager
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase 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.statusbar.policy.UserSwitcherController
import com.android.systemui.user.data.source.UserRecord import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.user.shared.model.UserActionModel import kotlinx.coroutines.CoroutineScope
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.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestCoroutineScope
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.Mock import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@SmallTest abstract class UserRepositoryImplTest : SysuiTestCase() {
@RunWith(JUnit4::class)
class UserRepositoryImplTest : SysuiTestCase() {
@Mock private lateinit var manager: UserManager @Mock protected lateinit var manager: UserManager
@Mock private lateinit var controller: UserSwitcherController @Mock protected lateinit var controller: UserSwitcherController
@Captor
private lateinit var userSwitchCallbackCaptor:
ArgumentCaptor<UserSwitcherController.UserSwitchCallback>
private lateinit var underTest: UserRepositoryImpl protected lateinit var underTest: UserRepositoryImpl
@Before protected lateinit var globalSettings: FakeSettings
fun setUp() { protected lateinit var tracker: FakeUserTracker
protected lateinit var featureFlags: FakeFeatureFlags
protected fun setUp(isRefactored: Boolean) {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false))
whenever(controller.isGuestUserAutoCreated).thenReturn(false)
whenever(controller.isGuestUserResetting).thenReturn(false)
underTest = globalSettings = FakeSettings()
UserRepositoryImpl( tracker = FakeUserTracker()
appContext = context, featureFlags = FakeFeatureFlags()
manager = manager, featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored)
controller = controller,
)
} }
@Test protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl {
fun `users - registers for updates`() = return UserRepositoryImpl(
runBlocking(IMMEDIATE) { appContext = context,
val job = underTest.users.onEach {}.launchIn(this) manager = manager,
controller = controller,
verify(controller).addUserSwitchCallback(any()) applicationScope = scope,
mainDispatcher = IMMEDIATE,
job.cancel() backgroundDispatcher = IMMEDIATE,
} globalSettings = globalSettings,
tracker = tracker,
@Test featureFlags = featureFlags,
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 `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()
}
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,
) )
} }
companion object { companion object {
private val IMMEDIATE = Dispatchers.Main.immediate @JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate
} }
} }

View File

@@ -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,
)
}
}

View File

@@ -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,
)
}
}

View File

@@ -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
}
}

View File

@@ -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"
}
}

View File

@@ -17,51 +17,61 @@
package com.android.systemui.user.domain.interactor 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.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.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.UserSwitcherController 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.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.Dispatchers
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.test.TestCoroutineScope
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.Mock
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@SmallTest abstract class UserInteractorTest : SysuiTestCase() {
@RunWith(JUnit4::class)
class UserInteractorTest : SysuiTestCase() {
@Mock private lateinit var controller: UserSwitcherController @Mock protected lateinit var controller: UserSwitcherController
@Mock private lateinit var activityStarter: ActivityStarter @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 protected lateinit var testCoroutineScope: TestCoroutineScope
private lateinit var keyguardRepository: FakeKeyguardRepository protected lateinit var userRepository: FakeUserRepository
protected lateinit var keyguardRepository: FakeKeyguardRepository
protected lateinit var telephonyRepository: FakeTelephonyRepository
@Before abstract fun isRefactored(): Boolean
fun setUp() {
open fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
userRepository = FakeUserRepository() userRepository = FakeUserRepository()
keyguardRepository = FakeKeyguardRepository() keyguardRepository = FakeKeyguardRepository()
telephonyRepository = FakeTelephonyRepository()
testCoroutineScope = TestCoroutineScope()
val refreshUsersScheduler =
RefreshUsersScheduler(
applicationScope = testCoroutineScope,
mainDispatcher = IMMEDIATE,
repository = userRepository,
)
underTest = underTest =
UserInteractor( UserInteractor(
applicationContext = context,
repository = userRepository, repository = userRepository,
controller = controller, controller = controller,
activityStarter = activityStarter, activityStarter = activityStarter,
@@ -69,142 +79,34 @@ class UserInteractorTest : SysuiTestCase() {
KeyguardInteractor( KeyguardInteractor(
repository = keyguardRepository, repository = keyguardRepository,
), ),
) featureFlags =
} FakeFeatureFlags().apply {
set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored())
@Test },
fun `actions - not actionable when locked and locked - no actions`() = manager = manager,
runBlocking(IMMEDIATE) { applicationScope = testCoroutineScope,
userRepository.setActions(UserActionModel.values().toList()) telephonyInteractor =
userRepository.setActionableWhenLocked(false) TelephonyInteractor(
keyguardRepository.setKeyguardShowing(true) repository = telephonyRepository,
),
var actions: List<UserActionModel>? = null broadcastDispatcher = fakeBroadcastDispatcher,
val job = underTest.actions.onEach { actions = it }.launchIn(this) backgroundDispatcher = IMMEDIATE,
activityManager = activityManager,
assertThat(actions).isEmpty() refreshUsersScheduler = refreshUsersScheduler,
job.cancel() guestUserInteractor =
} GuestUserInteractor(
applicationContext = context,
@Test applicationScope = testCoroutineScope,
fun `actions - not actionable when locked and not locked`() = mainDispatcher = IMMEDIATE,
runBlocking(IMMEDIATE) { backgroundDispatcher = IMMEDIATE,
userRepository.setActions( manager = manager,
listOf( repository = userRepository,
UserActionModel.ENTER_GUEST_MODE, deviceProvisionedController = deviceProvisionedController,
UserActionModel.ADD_USER, devicePolicyManager = devicePolicyManager,
UserActionModel.ADD_SUPERVISED_USER, refreshUsersScheduler = refreshUsersScheduler,
) uiEventLogger = uiEventLogger,
)
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 { companion object {

View File

@@ -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
}
}

View File

@@ -17,17 +17,28 @@
package com.android.systemui.user.ui.viewmodel package com.android.systemui.user.ui.viewmodel
import android.app.ActivityManager
import android.app.admin.DevicePolicyManager
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.UserManager
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Text 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.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.power.data.repository.FakePowerRepository import com.android.systemui.power.data.repository.FakePowerRepository
import com.android.systemui.power.domain.interactor.PowerInteractor 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.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.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.domain.interactor.UserInteractor
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel 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.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@@ -52,6 +64,11 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
@Mock private lateinit var controller: UserSwitcherController @Mock private lateinit var controller: UserSwitcherController
@Mock private lateinit var activityStarter: ActivityStarter @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 private lateinit var underTest: UserSwitcherViewModel
@@ -66,22 +83,60 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
userRepository = FakeUserRepository() userRepository = FakeUserRepository()
keyguardRepository = FakeKeyguardRepository() keyguardRepository = FakeKeyguardRepository()
powerRepository = FakePowerRepository() 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 = underTest =
UserSwitcherViewModel.Factory( UserSwitcherViewModel.Factory(
userInteractor = userInteractor =
UserInteractor( UserInteractor(
applicationContext = context,
repository = userRepository, repository = userRepository,
controller = controller, controller = controller,
activityStarter = activityStarter, activityStarter = activityStarter,
keyguardInteractor = keyguardInteractor =
KeyguardInteractor( KeyguardInteractor(
repository = keyguardRepository, repository = keyguardRepository,
) ),
featureFlags = featureFlags,
manager = manager,
applicationScope = scope,
telephonyInteractor =
TelephonyInteractor(
repository = FakeTelephonyRepository(),
),
broadcastDispatcher = fakeBroadcastDispatcher,
backgroundDispatcher = IMMEDIATE,
activityManager = activityManager,
refreshUsersScheduler = refreshUsersScheduler,
guestUserInteractor = guestUserInteractor,
), ),
powerInteractor = powerInteractor =
PowerInteractor( PowerInteractor(
repository = powerRepository, repository = powerRepository,
), ),
featureFlags = featureFlags,
guestUserInteractor = guestUserInteractor,
) )
.create(UserSwitcherViewModel::class.java) .create(UserSwitcherViewModel::class.java)
} }
@@ -97,6 +152,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
image = USER_IMAGE, image = USER_IMAGE,
isSelected = true, isSelected = true,
isSelectable = true, isSelectable = true,
isGuest = false,
), ),
UserModel( UserModel(
id = 1, id = 1,
@@ -104,6 +160,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
image = USER_IMAGE, image = USER_IMAGE,
isSelected = false, isSelected = false,
isSelectable = true, isSelectable = true,
isGuest = false,
), ),
UserModel( UserModel(
id = 2, id = 2,
@@ -111,6 +168,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
image = USER_IMAGE, image = USER_IMAGE,
isSelected = false, isSelected = false,
isSelectable = false, isSelectable = false,
isGuest = false,
), ),
) )
) )
@@ -260,7 +318,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
job.cancel() job.cancel()
} }
private fun setUsers(count: Int) { private suspend fun setUsers(count: Int) {
userRepository.setUsers( userRepository.setUsers(
(0 until count).map { index -> (0 until count).map { index ->
UserModel( UserModel(
@@ -269,6 +327,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
image = USER_IMAGE, image = USER_IMAGE,
isSelected = index == 0, isSelected = index == 0,
isSelectable = true, isSelectable = true,
isGuest = false,
) )
} }
) )

View File

@@ -37,10 +37,18 @@ class FakeBroadcastDispatcher(
dumpManager: DumpManager, dumpManager: DumpManager,
logger: BroadcastDispatcherLogger, logger: BroadcastDispatcherLogger,
userTracker: UserTracker 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( override fun registerReceiverWithHandler(
receiver: BroadcastReceiver, receiver: BroadcastReceiver,
@@ -78,4 +86,4 @@ class FakeBroadcastDispatcher(
} }
registeredReceivers.clear() registeredReceivers.clear()
} }
} }

View File

@@ -44,6 +44,10 @@ class FakeKeyguardRepository : KeyguardRepository {
private val _dozeAmount = MutableStateFlow(0f) private val _dozeAmount = MutableStateFlow(0f)
override val dozeAmount: Flow<Float> = _dozeAmount override val dozeAmount: Flow<Float> = _dozeAmount
override fun isKeyguardShowing(): Boolean {
return _isKeyguardShowing.value
}
override fun setAnimateDozingTransitions(animate: Boolean) { override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.tryEmit(animate) _animateBottomAreaDozingTransitions.tryEmit(animate)
} }

View File

@@ -26,20 +26,24 @@ import java.util.concurrent.Executor
/** A fake [UserTracker] to be used in tests. */ /** A fake [UserTracker] to be used in tests. */
class FakeUserTracker( class FakeUserTracker(
userId: Int = 0, private var _userId: Int = 0,
userHandle: UserHandle = UserHandle.of(userId), private var _userHandle: UserHandle = UserHandle.of(_userId),
userInfo: UserInfo = mock(), private var _userInfo: UserInfo = mock(),
userProfiles: List<UserInfo> = emptyList(), private var _userProfiles: List<UserInfo> = emptyList(),
userContentResolver: ContentResolver = MockContentResolver(), userContentResolver: ContentResolver = MockContentResolver(),
userContext: Context = mock(), userContext: Context = mock(),
private val onCreateCurrentUserContext: (Context) -> Context = { mock() }, private val onCreateCurrentUserContext: (Context) -> Context = { mock() },
) : UserTracker { ) : UserTracker {
val callbacks = mutableListOf<UserTracker.Callback>() val callbacks = mutableListOf<UserTracker.Callback>()
override val userId: Int = userId override val userId: Int
override val userHandle: UserHandle = userHandle get() = _userId
override val userInfo: UserInfo = userInfo override val userHandle: UserHandle
override val userProfiles: List<UserInfo> = userProfiles get() = _userHandle
override val userInfo: UserInfo
get() = _userInfo
override val userProfiles: List<UserInfo>
get() = _userProfiles
override val userContentResolver: ContentResolver = userContentResolver override val userContentResolver: ContentResolver = userContentResolver
override val userContext: Context = userContext override val userContext: Context = userContext
@@ -55,4 +59,13 @@ class FakeUserTracker(
override fun createCurrentUserContext(context: Context): Context { override fun createCurrentUserContext(context: Context): Context {
return onCreateCurrentUserContext(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) }
}
} }

View File

@@ -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
}
}

View File

@@ -17,12 +17,18 @@
package com.android.systemui.user.data.repository 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.UserActionModel
import com.android.systemui.user.shared.model.UserModel import com.android.systemui.user.shared.model.UserModel
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.yield
class FakeUserRepository : UserRepository { class FakeUserRepository : UserRepository {
@@ -34,21 +40,71 @@ class FakeUserRepository : UserRepository {
private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList()) private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList())
override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow() 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) private val _isActionableWhenLocked = MutableStateFlow(false)
override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow() override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow()
private var _isGuestUserAutoCreated: Boolean = false private var _isGuestUserAutoCreated: Boolean = false
override val isGuestUserAutoCreated: Boolean override val isGuestUserAutoCreated: Boolean
get() = _isGuestUserAutoCreated get() = _isGuestUserAutoCreated
private var _isGuestUserResetting: Boolean = false
override val isGuestUserResetting: Boolean override var isGuestUserResetting: Boolean = false
get() = _isGuestUserResetting
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>) { fun setUsers(models: List<UserModel>) {
_users.value = models _users.value = models
} }
fun setSelectedUser(userId: Int) { suspend fun setSelectedUser(userId: Int) {
check(_users.value.find { it.id == userId } != null) { check(_users.value.find { it.id == userId } != null) {
"Cannot select a user with ID $userId - no user with that ID found!" "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>) { fun setActions(models: List<UserActionModel>) {
@@ -75,8 +132,4 @@ class FakeUserRepository : UserRepository {
fun setGuestUserAutoCreated(value: Boolean) { fun setGuestUserAutoCreated(value: Boolean) {
_isGuestUserAutoCreated = value _isGuestUserAutoCreated = value
} }
fun setGuestUserResetting(value: Boolean) {
_isGuestUserResetting = value
}
} }