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:
@@ -81,6 +81,7 @@ import com.android.systemui.statusbar.policy.ZenModeController;
|
||||
import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule;
|
||||
import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule;
|
||||
import com.android.systemui.statusbar.window.StatusBarWindowModule;
|
||||
import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule;
|
||||
import com.android.systemui.tuner.dagger.TunerModule;
|
||||
import com.android.systemui.unfold.SysUIUnfoldModule;
|
||||
import com.android.systemui.user.UserModule;
|
||||
@@ -145,6 +146,7 @@ import dagger.Provides;
|
||||
StatusBarWindowModule.class,
|
||||
SysUIConcurrencyModule.class,
|
||||
SysUIUnfoldModule.class,
|
||||
TelephonyRepositoryModule.class,
|
||||
TunerModule.class,
|
||||
UserModule.class,
|
||||
UtilModule.class,
|
||||
|
||||
@@ -104,9 +104,26 @@ public class Flags {
|
||||
public static final ReleasedFlag MODERN_USER_SWITCHER_ACTIVITY =
|
||||
new ReleasedFlag(209, true);
|
||||
|
||||
/** Whether the new implementation of UserSwitcherController should be used. */
|
||||
public static final UnreleasedFlag REFACTORED_USER_SWITCHER_CONTROLLER =
|
||||
new UnreleasedFlag(210, false);
|
||||
/**
|
||||
* Whether the user interactor and repository should use `UserSwitcherController`.
|
||||
*
|
||||
* <p>If this is {@code false}, the interactor and repo skip the controller and directly access
|
||||
* the framework APIs.
|
||||
*/
|
||||
public static final UnreleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER =
|
||||
new UnreleasedFlag(210, true);
|
||||
|
||||
/**
|
||||
* Whether `UserSwitcherController` should use the user interactor.
|
||||
*
|
||||
* <p>When this is {@code true}, the controller does not directly access framework APIs.
|
||||
* Instead, it goes through the interactor.
|
||||
*
|
||||
* <p>Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is
|
||||
* {@code true} as it would created a cycle between controller -> interactor -> controller.
|
||||
*/
|
||||
public static final UnreleasedFlag USER_CONTROLLER_USES_INTERACTOR =
|
||||
new UnreleasedFlag(211, false);
|
||||
|
||||
/***************************************/
|
||||
// 300 - power menu
|
||||
|
||||
@@ -85,6 +85,15 @@ interface KeyguardRepository {
|
||||
*/
|
||||
val dozeAmount: Flow<Float>
|
||||
|
||||
/**
|
||||
* Returns `true` if the keyguard is showing; `false` otherwise.
|
||||
*
|
||||
* Note: this is also `true` when the lock-screen is occluded with an `Activity` "above" it in
|
||||
* the z-order (which is not really above the system UI window, but rather - the lock-screen
|
||||
* becomes invisible to reveal the "occluding activity").
|
||||
*/
|
||||
fun isKeyguardShowing(): Boolean
|
||||
|
||||
/** Sets whether the bottom area UI should animate the transition out of doze state. */
|
||||
fun setAnimateDozingTransitions(animate: Boolean)
|
||||
|
||||
@@ -103,7 +112,7 @@ class KeyguardRepositoryImpl
|
||||
@Inject
|
||||
constructor(
|
||||
statusBarStateController: StatusBarStateController,
|
||||
keyguardStateController: KeyguardStateController,
|
||||
private val keyguardStateController: KeyguardStateController,
|
||||
dozeHost: DozeHost,
|
||||
) : KeyguardRepository {
|
||||
private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
|
||||
@@ -168,6 +177,10 @@ constructor(
|
||||
awaitClose { statusBarStateController.removeCallback(callback) }
|
||||
}
|
||||
|
||||
override fun isKeyguardShowing(): Boolean {
|
||||
return keyguardStateController.isShowing
|
||||
}
|
||||
|
||||
override fun setAnimateDozingTransitions(animate: Boolean) {
|
||||
_animateBottomAreaDozingTransitions.value = animate
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
class KeyguardInteractor
|
||||
@Inject
|
||||
constructor(
|
||||
repository: KeyguardRepository,
|
||||
private val repository: KeyguardRepository,
|
||||
) {
|
||||
/**
|
||||
* The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at
|
||||
@@ -40,4 +40,8 @@ constructor(
|
||||
val isDozing: Flow<Boolean> = repository.isDozing
|
||||
/** Whether the keyguard is showing ot not. */
|
||||
val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
|
||||
|
||||
fun isKeyguardShowing(): Boolean {
|
||||
return repository.isKeyguardShowing()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ public class UserDetailView extends PseudoGridView {
|
||||
v.bind(name, drawable, item.info.id);
|
||||
}
|
||||
v.setActivated(item.isCurrent);
|
||||
v.setDisabledByAdmin(mController.isDisabledByAdmin(item));
|
||||
v.setDisabledByAdmin(item.isDisabledByAdmin());
|
||||
v.setEnabled(item.isSwitchToEnabled);
|
||||
UserSwitcherController.setSelectableAlpha(v);
|
||||
|
||||
@@ -173,16 +173,16 @@ public class UserDetailView extends PseudoGridView {
|
||||
Trace.beginSection("UserDetailView.Adapter#onClick");
|
||||
UserRecord userRecord =
|
||||
(UserRecord) view.getTag();
|
||||
if (mController.isDisabledByAdmin(userRecord)) {
|
||||
if (userRecord.isDisabledByAdmin()) {
|
||||
final Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
|
||||
mContext, mController.getEnforcedAdmin(userRecord));
|
||||
mContext, userRecord.enforcedAdmin);
|
||||
mController.startActivity(intent);
|
||||
} else if (userRecord.isSwitchToEnabled) {
|
||||
MetricsLogger.action(mContext, MetricsEvent.QS_SWITCH_USER);
|
||||
mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH);
|
||||
if (!userRecord.isAddUser
|
||||
&& !userRecord.isRestricted
|
||||
&& !mController.isDisabledByAdmin(userRecord)) {
|
||||
&& !userRecord.isDisabledByAdmin()) {
|
||||
if (mCurrentUserView != null) {
|
||||
mCurrentUserView.setActivated(false);
|
||||
}
|
||||
|
||||
@@ -505,7 +505,7 @@ public class KeyguardUserSwitcherController extends ViewController<KeyguardUserS
|
||||
v.bind(name, drawable, item.info.id);
|
||||
}
|
||||
v.setActivated(item.isCurrent);
|
||||
v.setDisabledByAdmin(getController().isDisabledByAdmin(item));
|
||||
v.setDisabledByAdmin(item.isDisabledByAdmin());
|
||||
v.setEnabled(item.isSwitchToEnabled);
|
||||
UserSwitcherController.setSelectableAlpha(v);
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ package com.android.systemui.statusbar.policy
|
||||
import android.annotation.UserIdInt
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
|
||||
import com.android.systemui.Dumpable
|
||||
import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
|
||||
import com.android.systemui.user.data.source.UserRecord
|
||||
@@ -130,12 +129,6 @@ interface UserSwitcherController : Dumpable {
|
||||
/** Whether keyguard is showing. */
|
||||
val isKeyguardShowing: Boolean
|
||||
|
||||
/** Returns the [EnforcedAdmin] for the given record, or `null` if there isn't one. */
|
||||
fun getEnforcedAdmin(record: UserRecord): EnforcedAdmin?
|
||||
|
||||
/** Returns `true` if the given record is disabled by the admin; `false` otherwise. */
|
||||
fun isDisabledByAdmin(record: UserRecord): Boolean
|
||||
|
||||
/** Starts an activity with the given [Intent]. */
|
||||
fun startActivity(intent: Intent)
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ package com.android.systemui.statusbar.policy
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import com.android.settingslib.RestrictedLockUtils
|
||||
import com.android.systemui.flags.FeatureFlags
|
||||
import com.android.systemui.flags.Flags
|
||||
import com.android.systemui.qs.user.UserSwitchDialogController
|
||||
@@ -38,8 +37,9 @@ constructor(
|
||||
@Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>,
|
||||
) : UserSwitcherController {
|
||||
|
||||
private val isNewImpl: Boolean
|
||||
get() = flags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER)
|
||||
private val useInteractor: Boolean =
|
||||
flags.isEnabled(Flags.USER_CONTROLLER_USES_INTERACTOR) &&
|
||||
!flags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
|
||||
private val _oldImpl: UserSwitcherControllerOldImpl
|
||||
get() = oldImpl.get()
|
||||
|
||||
@@ -49,7 +49,7 @@ constructor(
|
||||
|
||||
override val users: ArrayList<UserRecord>
|
||||
get() =
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.users
|
||||
@@ -57,14 +57,14 @@ constructor(
|
||||
|
||||
override val isSimpleUserSwitcher: Boolean
|
||||
get() =
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.isSimpleUserSwitcher
|
||||
}
|
||||
|
||||
override fun init(view: View) {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.init(view)
|
||||
@@ -73,7 +73,7 @@ constructor(
|
||||
|
||||
override val currentUserRecord: UserRecord?
|
||||
get() =
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.currentUserRecord
|
||||
@@ -81,7 +81,7 @@ constructor(
|
||||
|
||||
override val currentUserName: String?
|
||||
get() =
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.currentUserName
|
||||
@@ -91,7 +91,7 @@ constructor(
|
||||
userId: Int,
|
||||
dialogShower: UserSwitchDialogController.DialogShower?
|
||||
) {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.onUserSelected(userId, dialogShower)
|
||||
@@ -100,7 +100,7 @@ constructor(
|
||||
|
||||
override val isAddUsersFromLockScreenEnabled: Flow<Boolean>
|
||||
get() =
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.isAddUsersFromLockScreenEnabled
|
||||
@@ -108,7 +108,7 @@ constructor(
|
||||
|
||||
override val isGuestUserAutoCreated: Boolean
|
||||
get() =
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.isGuestUserAutoCreated
|
||||
@@ -116,7 +116,7 @@ constructor(
|
||||
|
||||
override val isGuestUserResetting: Boolean
|
||||
get() =
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.isGuestUserResetting
|
||||
@@ -125,7 +125,7 @@ constructor(
|
||||
override fun createAndSwitchToGuestUser(
|
||||
dialogShower: UserSwitchDialogController.DialogShower?,
|
||||
) {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.createAndSwitchToGuestUser(dialogShower)
|
||||
@@ -133,7 +133,7 @@ constructor(
|
||||
}
|
||||
|
||||
override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.showAddUserDialog(dialogShower)
|
||||
@@ -141,7 +141,7 @@ constructor(
|
||||
}
|
||||
|
||||
override fun startSupervisedUserActivity() {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.startSupervisedUserActivity()
|
||||
@@ -149,7 +149,7 @@ constructor(
|
||||
}
|
||||
|
||||
override fun onDensityOrFontScaleChanged() {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.onDensityOrFontScaleChanged()
|
||||
@@ -157,7 +157,7 @@ constructor(
|
||||
}
|
||||
|
||||
override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.addAdapter(adapter)
|
||||
@@ -168,7 +168,7 @@ constructor(
|
||||
record: UserRecord,
|
||||
dialogShower: UserSwitchDialogController.DialogShower?,
|
||||
) {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.onUserListItemClicked(record, dialogShower)
|
||||
@@ -176,7 +176,7 @@ constructor(
|
||||
}
|
||||
|
||||
override fun removeGuestUser(guestUserId: Int, targetUserId: Int) {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.removeGuestUser(guestUserId, targetUserId)
|
||||
@@ -188,7 +188,7 @@ constructor(
|
||||
targetUserId: Int,
|
||||
forceRemoveGuestOnExit: Boolean
|
||||
) {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
|
||||
@@ -196,7 +196,7 @@ constructor(
|
||||
}
|
||||
|
||||
override fun schedulePostBootGuestCreation() {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.schedulePostBootGuestCreation()
|
||||
@@ -205,30 +205,14 @@ constructor(
|
||||
|
||||
override val isKeyguardShowing: Boolean
|
||||
get() =
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.isKeyguardShowing
|
||||
}
|
||||
|
||||
override fun getEnforcedAdmin(record: UserRecord): RestrictedLockUtils.EnforcedAdmin? {
|
||||
return if (isNewImpl) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.getEnforcedAdmin(record)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isDisabledByAdmin(record: UserRecord): Boolean {
|
||||
return if (isNewImpl) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.isDisabledByAdmin(record)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startActivity(intent: Intent) {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.startActivity(intent)
|
||||
@@ -236,7 +220,7 @@ constructor(
|
||||
}
|
||||
|
||||
override fun refreshUsers(forcePictureLoadForId: Int) {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.refreshUsers(forcePictureLoadForId)
|
||||
@@ -244,7 +228,7 @@ constructor(
|
||||
}
|
||||
|
||||
override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.addUserSwitchCallback(callback)
|
||||
@@ -252,7 +236,7 @@ constructor(
|
||||
}
|
||||
|
||||
override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.removeUserSwitchCallback(callback)
|
||||
@@ -260,7 +244,7 @@ constructor(
|
||||
}
|
||||
|
||||
override fun dump(pw: PrintWriter, args: Array<out String>) {
|
||||
if (isNewImpl) {
|
||||
if (useInteractor) {
|
||||
notYetImplemented()
|
||||
} else {
|
||||
_oldImpl.dump(pw, args)
|
||||
|
||||
@@ -17,17 +17,13 @@ package com.android.systemui.statusbar.policy;
|
||||
|
||||
import static android.os.UserManager.SWITCHABILITY_STATUS_OK;
|
||||
|
||||
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
|
||||
|
||||
import android.annotation.UserIdInt;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.IActivityManager;
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.UserInfo;
|
||||
@@ -40,7 +36,6 @@ import android.os.UserManager;
|
||||
import android.provider.Settings;
|
||||
import android.telephony.TelephonyCallback;
|
||||
import android.text.TextUtils;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.util.SparseBooleanArray;
|
||||
@@ -49,17 +44,14 @@ import android.view.WindowManagerGlobal;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.collection.SimpleArrayMap;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.jank.InteractionJankMonitor;
|
||||
import com.android.internal.logging.UiEventLogger;
|
||||
import com.android.internal.util.LatencyTracker;
|
||||
import com.android.settingslib.RestrictedLockUtilsInternal;
|
||||
import com.android.settingslib.users.UserCreatingDialog;
|
||||
import com.android.systemui.GuestResetOrExitSessionReceiver;
|
||||
import com.android.systemui.GuestResumeSessionReceiver;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.SystemUISecondaryUserService;
|
||||
import com.android.systemui.animation.DialogCuj;
|
||||
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||
@@ -75,10 +67,12 @@ import com.android.systemui.plugins.FalsingManager;
|
||||
import com.android.systemui.qs.QSUserSwitcherEvent;
|
||||
import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower;
|
||||
import com.android.systemui.settings.UserTracker;
|
||||
import com.android.systemui.statusbar.phone.SystemUIDialog;
|
||||
import com.android.systemui.telephony.TelephonyListenerManager;
|
||||
import com.android.systemui.user.CreateUserActivity;
|
||||
import com.android.systemui.user.data.source.UserRecord;
|
||||
import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper;
|
||||
import com.android.systemui.user.shared.model.UserActionModel;
|
||||
import com.android.systemui.user.ui.dialog.AddUserDialog;
|
||||
import com.android.systemui.user.ui.dialog.ExitGuestDialog;
|
||||
import com.android.systemui.util.settings.GlobalSettings;
|
||||
import com.android.systemui.util.settings.SecureSettings;
|
||||
|
||||
@@ -139,9 +133,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
|
||||
private final InteractionJankMonitor mInteractionJankMonitor;
|
||||
private final LatencyTracker mLatencyTracker;
|
||||
private final DialogLaunchAnimator mDialogLaunchAnimator;
|
||||
private final SimpleArrayMap<UserRecord, EnforcedAdmin> mEnforcedAdminByUserRecord =
|
||||
new SimpleArrayMap<>();
|
||||
private final ArraySet<UserRecord> mDisabledByAdmin = new ArraySet<>();
|
||||
|
||||
private ArrayList<UserRecord> mUsers = new ArrayList<>();
|
||||
@VisibleForTesting
|
||||
@@ -334,7 +325,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
|
||||
|
||||
for (UserInfo info : infos) {
|
||||
boolean isCurrent = currentId == info.id;
|
||||
boolean switchToEnabled = canSwitchUsers || isCurrent;
|
||||
if (!mUserSwitcherEnabled && !info.isPrimary()) {
|
||||
continue;
|
||||
}
|
||||
@@ -343,25 +333,22 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
|
||||
if (info.isGuest()) {
|
||||
// Tapping guest icon triggers remove and a user switch therefore
|
||||
// the icon shouldn't be enabled even if the user is current
|
||||
guestRecord = new UserRecord(info, null /* picture */,
|
||||
true /* isGuest */, isCurrent, false /* isAddUser */,
|
||||
false /* isRestricted */, canSwitchUsers,
|
||||
false /* isAddSupervisedUser */);
|
||||
guestRecord = LegacyUserDataHelper.createRecord(
|
||||
mContext,
|
||||
mUserManager,
|
||||
null /* picture */,
|
||||
info,
|
||||
isCurrent,
|
||||
canSwitchUsers);
|
||||
} else if (info.supportsSwitchToByUser()) {
|
||||
Bitmap picture = bitmaps.get(info.id);
|
||||
if (picture == null) {
|
||||
picture = mUserManager.getUserIcon(info.id);
|
||||
|
||||
if (picture != null) {
|
||||
int avatarSize = mContext.getResources()
|
||||
.getDimensionPixelSize(R.dimen.max_avatar_size);
|
||||
picture = Bitmap.createScaledBitmap(
|
||||
picture, avatarSize, avatarSize, true);
|
||||
}
|
||||
}
|
||||
records.add(new UserRecord(info, picture, false /* isGuest */,
|
||||
isCurrent, false /* isAddUser */, false /* isRestricted */,
|
||||
switchToEnabled, false /* isAddSupervisedUser */));
|
||||
records.add(
|
||||
LegacyUserDataHelper.createRecord(
|
||||
mContext,
|
||||
mUserManager,
|
||||
bitmaps.get(info.id),
|
||||
info,
|
||||
isCurrent,
|
||||
canSwitchUsers));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -372,18 +359,20 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
|
||||
// we will just use it as an indicator for "Resetting guest...".
|
||||
// Otherwise, default to canSwitchUsers.
|
||||
boolean isSwitchToGuestEnabled = !mGuestIsResetting.get() && canSwitchUsers;
|
||||
guestRecord = new UserRecord(null /* info */, null /* picture */,
|
||||
true /* isGuest */, false /* isCurrent */,
|
||||
false /* isAddUser */, false /* isRestricted */,
|
||||
isSwitchToGuestEnabled, false /* isAddSupervisedUser */);
|
||||
checkIfAddUserDisallowedByAdminOnly(guestRecord);
|
||||
guestRecord = LegacyUserDataHelper.createRecord(
|
||||
mContext,
|
||||
currentId,
|
||||
UserActionModel.ENTER_GUEST_MODE,
|
||||
false /* isRestricted */,
|
||||
isSwitchToGuestEnabled);
|
||||
records.add(guestRecord);
|
||||
} else if (canCreateGuest(guestRecord != null)) {
|
||||
guestRecord = new UserRecord(null /* info */, null /* picture */,
|
||||
true /* isGuest */, false /* isCurrent */,
|
||||
false /* isAddUser */, createIsRestricted(), canSwitchUsers,
|
||||
false /* isAddSupervisedUser */);
|
||||
checkIfAddUserDisallowedByAdminOnly(guestRecord);
|
||||
guestRecord = LegacyUserDataHelper.createRecord(
|
||||
mContext,
|
||||
currentId,
|
||||
UserActionModel.ENTER_GUEST_MODE,
|
||||
false /* isRestricted */,
|
||||
canSwitchUsers);
|
||||
records.add(guestRecord);
|
||||
}
|
||||
} else {
|
||||
@@ -391,20 +380,23 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
|
||||
}
|
||||
|
||||
if (canCreateUser()) {
|
||||
UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */,
|
||||
false /* isGuest */, false /* isCurrent */, true /* isAddUser */,
|
||||
createIsRestricted(), canSwitchUsers,
|
||||
false /* isAddSupervisedUser */);
|
||||
checkIfAddUserDisallowedByAdminOnly(addUserRecord);
|
||||
records.add(addUserRecord);
|
||||
final UserRecord userRecord = LegacyUserDataHelper.createRecord(
|
||||
mContext,
|
||||
currentId,
|
||||
UserActionModel.ADD_USER,
|
||||
createIsRestricted(),
|
||||
canSwitchUsers);
|
||||
records.add(userRecord);
|
||||
}
|
||||
|
||||
if (canCreateSupervisedUser()) {
|
||||
UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */,
|
||||
false /* isGuest */, false /* isCurrent */, false /* isAddUser */,
|
||||
createIsRestricted(), canSwitchUsers, true /* isAddSupervisedUser */);
|
||||
checkIfAddUserDisallowedByAdminOnly(addUserRecord);
|
||||
records.add(addUserRecord);
|
||||
final UserRecord userRecord = LegacyUserDataHelper.createRecord(
|
||||
mContext,
|
||||
currentId,
|
||||
UserActionModel.ADD_SUPERVISED_USER,
|
||||
createIsRestricted(),
|
||||
canSwitchUsers);
|
||||
records.add(userRecord);
|
||||
}
|
||||
|
||||
mUiExecutor.execute(() -> {
|
||||
@@ -591,12 +583,23 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
|
||||
showExitGuestDialog(id, isGuestEphemeral, newId, dialogShower);
|
||||
}
|
||||
|
||||
private void showExitGuestDialog(int id, boolean isGuestEphemeral,
|
||||
int targetId, DialogShower dialogShower) {
|
||||
private void showExitGuestDialog(
|
||||
int id,
|
||||
boolean isGuestEphemeral,
|
||||
int targetId,
|
||||
DialogShower dialogShower) {
|
||||
if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
|
||||
mExitGuestDialog.cancel();
|
||||
}
|
||||
mExitGuestDialog = new ExitGuestDialog(mContext, id, isGuestEphemeral, targetId);
|
||||
mExitGuestDialog = new ExitGuestDialog(
|
||||
mContext,
|
||||
id,
|
||||
isGuestEphemeral,
|
||||
targetId,
|
||||
mKeyguardStateController.isShowing(),
|
||||
mFalsingManager,
|
||||
mDialogLaunchAnimator,
|
||||
this::exitGuestUser);
|
||||
if (dialogShower != null) {
|
||||
dialogShower.showDialog(mExitGuestDialog, new DialogCuj(
|
||||
InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
|
||||
@@ -622,7 +625,15 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
|
||||
if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
|
||||
mAddUserDialog.cancel();
|
||||
}
|
||||
mAddUserDialog = new AddUserDialog(mContext);
|
||||
final UserInfo currentUser = mUserTracker.getUserInfo();
|
||||
mAddUserDialog = new AddUserDialog(
|
||||
mContext,
|
||||
currentUser.getUserHandle(),
|
||||
mKeyguardStateController.isShowing(),
|
||||
/* showEphemeralMessage= */currentUser.isGuest() && currentUser.isEphemeral(),
|
||||
mFalsingManager,
|
||||
mBroadcastSender,
|
||||
mDialogLaunchAnimator);
|
||||
if (dialogShower != null) {
|
||||
dialogShower.showDialog(mAddUserDialog,
|
||||
new DialogCuj(
|
||||
@@ -964,30 +975,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
|
||||
return mKeyguardStateController.isShowing();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public EnforcedAdmin getEnforcedAdmin(UserRecord record) {
|
||||
return mEnforcedAdminByUserRecord.get(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisabledByAdmin(UserRecord record) {
|
||||
return mDisabledByAdmin.contains(record);
|
||||
}
|
||||
|
||||
private void checkIfAddUserDisallowedByAdminOnly(UserRecord record) {
|
||||
EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
|
||||
UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId());
|
||||
if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext,
|
||||
UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId())) {
|
||||
mDisabledByAdmin.add(record);
|
||||
mEnforcedAdminByUserRecord.put(record, admin);
|
||||
} else {
|
||||
mDisabledByAdmin.remove(record);
|
||||
mEnforcedAdminByUserRecord.put(record, null);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldUseSimpleUserSwitcher() {
|
||||
int defaultSimpleUserSwitcher = mContext.getResources().getBoolean(
|
||||
com.android.internal.R.bool.config_expandLockScreenUserSwitcher) ? 1 : 0;
|
||||
@@ -1052,133 +1039,4 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private final class ExitGuestDialog extends SystemUIDialog implements
|
||||
DialogInterface.OnClickListener {
|
||||
|
||||
private final int mGuestId;
|
||||
private final int mTargetId;
|
||||
private final boolean mIsGuestEphemeral;
|
||||
|
||||
ExitGuestDialog(Context context, int guestId, boolean isGuestEphemeral,
|
||||
int targetId) {
|
||||
super(context);
|
||||
if (isGuestEphemeral) {
|
||||
setTitle(context.getString(
|
||||
com.android.settingslib.R.string.guest_exit_dialog_title));
|
||||
setMessage(context.getString(
|
||||
com.android.settingslib.R.string.guest_exit_dialog_message));
|
||||
setButton(DialogInterface.BUTTON_NEUTRAL,
|
||||
context.getString(android.R.string.cancel), this);
|
||||
setButton(DialogInterface.BUTTON_POSITIVE,
|
||||
context.getString(
|
||||
com.android.settingslib.R.string.guest_exit_dialog_button), this);
|
||||
} else {
|
||||
setTitle(context.getString(
|
||||
com.android.settingslib
|
||||
.R.string.guest_exit_dialog_title_non_ephemeral));
|
||||
setMessage(context.getString(
|
||||
com.android.settingslib
|
||||
.R.string.guest_exit_dialog_message_non_ephemeral));
|
||||
setButton(DialogInterface.BUTTON_NEUTRAL,
|
||||
context.getString(android.R.string.cancel), this);
|
||||
setButton(DialogInterface.BUTTON_NEGATIVE,
|
||||
context.getString(
|
||||
com.android.settingslib.R.string.guest_exit_clear_data_button),
|
||||
this);
|
||||
setButton(DialogInterface.BUTTON_POSITIVE,
|
||||
context.getString(
|
||||
com.android.settingslib.R.string.guest_exit_save_data_button),
|
||||
this);
|
||||
}
|
||||
SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing());
|
||||
setCanceledOnTouchOutside(false);
|
||||
mGuestId = guestId;
|
||||
mTargetId = targetId;
|
||||
mIsGuestEphemeral = isGuestEphemeral;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY
|
||||
: FalsingManager.HIGH_PENALTY;
|
||||
if (mFalsingManager.isFalseTap(penalty)) {
|
||||
return;
|
||||
}
|
||||
if (mIsGuestEphemeral) {
|
||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||
mDialogLaunchAnimator.dismissStack(this);
|
||||
// Ephemeral guest: exit guest, guest is removed by the system
|
||||
// on exit, since its marked ephemeral
|
||||
exitGuestUser(mGuestId, mTargetId, false);
|
||||
} else if (which == DialogInterface.BUTTON_NEGATIVE) {
|
||||
// Cancel clicked, do nothing
|
||||
cancel();
|
||||
}
|
||||
} else {
|
||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||
mDialogLaunchAnimator.dismissStack(this);
|
||||
// Non-ephemeral guest: exit guest, guest is not removed by the system
|
||||
// on exit, since its marked non-ephemeral
|
||||
exitGuestUser(mGuestId, mTargetId, false);
|
||||
} else if (which == DialogInterface.BUTTON_NEGATIVE) {
|
||||
mDialogLaunchAnimator.dismissStack(this);
|
||||
// Non-ephemeral guest: remove guest and then exit
|
||||
exitGuestUser(mGuestId, mTargetId, true);
|
||||
} else if (which == DialogInterface.BUTTON_NEUTRAL) {
|
||||
// Cancel clicked, do nothing
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
final class AddUserDialog extends SystemUIDialog implements
|
||||
DialogInterface.OnClickListener {
|
||||
|
||||
AddUserDialog(Context context) {
|
||||
super(context);
|
||||
|
||||
setTitle(com.android.settingslib.R.string.user_add_user_title);
|
||||
String message = context.getString(
|
||||
com.android.settingslib.R.string.user_add_user_message_short);
|
||||
UserInfo currentUser = mUserTracker.getUserInfo();
|
||||
if (currentUser != null && currentUser.isGuest() && currentUser.isEphemeral()) {
|
||||
message += context.getString(R.string.user_add_user_message_guest_remove);
|
||||
}
|
||||
setMessage(message);
|
||||
setButton(DialogInterface.BUTTON_NEUTRAL,
|
||||
context.getString(android.R.string.cancel), this);
|
||||
setButton(DialogInterface.BUTTON_POSITIVE,
|
||||
context.getString(android.R.string.ok), this);
|
||||
SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY
|
||||
: FalsingManager.MODERATE_PENALTY;
|
||||
if (mFalsingManager.isFalseTap(penalty)) {
|
||||
return;
|
||||
}
|
||||
if (which == BUTTON_NEUTRAL) {
|
||||
cancel();
|
||||
} else {
|
||||
mDialogLaunchAnimator.dismissStack(this);
|
||||
if (ActivityManager.isUserAMonkey()) {
|
||||
return;
|
||||
}
|
||||
// Use broadcast instead of ShadeController, as this dialog may have started in
|
||||
// another process and normal dagger bindings are not available
|
||||
mBroadcastSender.sendBroadcastAsUser(
|
||||
new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), UserHandle.CURRENT);
|
||||
getContext().startActivityAsUser(
|
||||
CreateUserActivity.createIntentForStart(getContext()),
|
||||
mUserTracker.getUserHandle());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import android.app.Activity;
|
||||
|
||||
import com.android.settingslib.users.EditUserInfoController;
|
||||
import com.android.systemui.user.data.repository.UserRepositoryModule;
|
||||
import com.android.systemui.user.ui.dialog.UserDialogModule;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
@@ -32,6 +33,7 @@ import dagger.multibindings.IntoMap;
|
||||
*/
|
||||
@Module(
|
||||
includes = {
|
||||
UserDialogModule.class,
|
||||
UserRepositoryModule.class,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -18,9 +18,13 @@
|
||||
package com.android.systemui.user.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.UserInfo
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.UserHandle
|
||||
import android.os.UserManager
|
||||
import android.provider.Settings
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import com.android.internal.util.UserIcons
|
||||
import com.android.systemui.R
|
||||
@@ -29,15 +33,36 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall
|
||||
import com.android.systemui.common.shared.model.Text
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.dagger.qualifiers.Main
|
||||
import com.android.systemui.flags.FeatureFlags
|
||||
import com.android.systemui.flags.Flags
|
||||
import com.android.systemui.settings.UserTracker
|
||||
import com.android.systemui.statusbar.policy.UserSwitcherController
|
||||
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
|
||||
import com.android.systemui.user.data.source.UserRecord
|
||||
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
|
||||
import com.android.systemui.user.shared.model.UserActionModel
|
||||
import com.android.systemui.user.shared.model.UserModel
|
||||
import com.android.systemui.util.settings.GlobalSettings
|
||||
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Acts as source of truth for user related data.
|
||||
@@ -55,6 +80,18 @@ interface UserRepository {
|
||||
/** List of available user-related actions. */
|
||||
val actions: Flow<List<UserActionModel>>
|
||||
|
||||
/** User switcher related settings. */
|
||||
val userSwitcherSettings: Flow<UserSwitcherSettingsModel>
|
||||
|
||||
/** List of all users on the device. */
|
||||
val userInfos: Flow<List<UserInfo>>
|
||||
|
||||
/** [UserInfo] of the currently-selected user. */
|
||||
val selectedUserInfo: Flow<UserInfo>
|
||||
|
||||
/** User ID of the last non-guest selected user. */
|
||||
val lastSelectedNonGuestUserId: Int
|
||||
|
||||
/** Whether actions are available even when locked. */
|
||||
val isActionableWhenLocked: Flow<Boolean>
|
||||
|
||||
@@ -62,7 +99,23 @@ interface UserRepository {
|
||||
val isGuestUserAutoCreated: Boolean
|
||||
|
||||
/** Whether the guest user is currently being reset. */
|
||||
val isGuestUserResetting: Boolean
|
||||
var isGuestUserResetting: Boolean
|
||||
|
||||
/** Whether we've scheduled the creation of a guest user. */
|
||||
val isGuestUserCreationScheduled: AtomicBoolean
|
||||
|
||||
/** The user of the secondary service. */
|
||||
var secondaryUserId: Int
|
||||
|
||||
/** Whether refresh users should be paused. */
|
||||
var isRefreshUsersPaused: Boolean
|
||||
|
||||
/** Asynchronously refresh the list of users. This will cause [userInfos] to be updated. */
|
||||
fun refreshUsers()
|
||||
|
||||
fun getSelectedUserInfo(): UserInfo
|
||||
|
||||
fun isSimpleUserSwitcher(): Boolean
|
||||
}
|
||||
|
||||
@SysUISingleton
|
||||
@@ -71,9 +124,31 @@ class UserRepositoryImpl
|
||||
constructor(
|
||||
@Application private val appContext: Context,
|
||||
private val manager: UserManager,
|
||||
controller: UserSwitcherController,
|
||||
private val controller: UserSwitcherController,
|
||||
@Application private val applicationScope: CoroutineScope,
|
||||
@Main private val mainDispatcher: CoroutineDispatcher,
|
||||
@Background private val backgroundDispatcher: CoroutineDispatcher,
|
||||
private val globalSettings: GlobalSettings,
|
||||
private val tracker: UserTracker,
|
||||
private val featureFlags: FeatureFlags,
|
||||
) : UserRepository {
|
||||
|
||||
private val isNewImpl: Boolean
|
||||
get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
|
||||
|
||||
private val _userSwitcherSettings = MutableStateFlow<UserSwitcherSettingsModel?>(null)
|
||||
override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
|
||||
_userSwitcherSettings.asStateFlow().filterNotNull()
|
||||
|
||||
private val _userInfos = MutableStateFlow<List<UserInfo>?>(null)
|
||||
override val userInfos: Flow<List<UserInfo>> = _userInfos.filterNotNull()
|
||||
|
||||
private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
|
||||
override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
|
||||
|
||||
override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
|
||||
private set
|
||||
|
||||
private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow {
|
||||
fun send() {
|
||||
trySendWithFailureLogging(
|
||||
@@ -99,11 +174,148 @@ constructor(
|
||||
override val actions: Flow<List<UserActionModel>> =
|
||||
userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } }
|
||||
|
||||
override val isActionableWhenLocked: Flow<Boolean> = controller.isAddUsersFromLockScreenEnabled
|
||||
override val isActionableWhenLocked: Flow<Boolean> =
|
||||
if (isNewImpl) {
|
||||
emptyFlow()
|
||||
} else {
|
||||
controller.isAddUsersFromLockScreenEnabled
|
||||
}
|
||||
|
||||
override val isGuestUserAutoCreated: Boolean = 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 {
|
||||
return when {
|
||||
@@ -125,6 +337,7 @@ constructor(
|
||||
image = getUserImage(this),
|
||||
isSelected = isCurrent,
|
||||
isSelectable = isSwitchToEnabled || isGuest,
|
||||
isGuest = isGuest,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -162,5 +375,6 @@ constructor(
|
||||
|
||||
companion object {
|
||||
private const val TAG = "UserRepository"
|
||||
@VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package com.android.systemui.user.data.source
|
||||
import android.content.pm.UserInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.os.UserHandle
|
||||
import com.android.settingslib.RestrictedLockUtils
|
||||
|
||||
/** Encapsulates raw data for a user or an option item related to managing users on the device. */
|
||||
data class UserRecord(
|
||||
@@ -41,6 +42,11 @@ data class UserRecord(
|
||||
@JvmField val isSwitchToEnabled: Boolean = false,
|
||||
/** Whether this record represents an option to add another supervised user to the device. */
|
||||
@JvmField val isAddSupervisedUser: Boolean = false,
|
||||
/**
|
||||
* An enforcing admin, if the user action represented by this record is disabled by the admin.
|
||||
* If not disabled, this is `null`.
|
||||
*/
|
||||
@JvmField val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin? = null,
|
||||
) {
|
||||
/** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */
|
||||
fun copyWithIsCurrent(isCurrent: Boolean): UserRecord {
|
||||
@@ -59,6 +65,14 @@ data class UserRecord(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the user action represented by this record has been disabled by an admin;
|
||||
* `false` otherwise.
|
||||
*/
|
||||
fun isDisabledByAdmin(): Boolean {
|
||||
return enforcedAdmin != null
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun createForGuest(): UserRecord {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -17,94 +17,664 @@
|
||||
|
||||
package com.android.systemui.user.domain.interactor
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.UserIdInt
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.UserInfo
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.RemoteException
|
||||
import android.os.UserHandle
|
||||
import android.os.UserManager
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import com.android.internal.util.UserIcons
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SystemUISecondaryUserService
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.common.shared.model.Text
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.flags.FeatureFlags
|
||||
import com.android.systemui.flags.Flags
|
||||
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
|
||||
import com.android.systemui.plugins.ActivityStarter
|
||||
import com.android.systemui.statusbar.policy.UserSwitcherController
|
||||
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
|
||||
import com.android.systemui.user.data.repository.UserRepository
|
||||
import com.android.systemui.user.data.source.UserRecord
|
||||
import com.android.systemui.user.domain.model.ShowDialogRequestModel
|
||||
import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper
|
||||
import com.android.systemui.user.shared.model.UserActionModel
|
||||
import com.android.systemui.user.shared.model.UserModel
|
||||
import com.android.systemui.util.kotlin.pairwise
|
||||
import java.util.Collections
|
||||
import java.util.WeakHashMap
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/** Encapsulates business logic to interact with user data and systems. */
|
||||
@SysUISingleton
|
||||
class UserInteractor
|
||||
@Inject
|
||||
constructor(
|
||||
repository: UserRepository,
|
||||
@Application private val applicationContext: Context,
|
||||
private val repository: UserRepository,
|
||||
private val controller: UserSwitcherController,
|
||||
private val activityStarter: ActivityStarter,
|
||||
keyguardInteractor: KeyguardInteractor,
|
||||
private val keyguardInteractor: KeyguardInteractor,
|
||||
private val featureFlags: FeatureFlags,
|
||||
private val manager: UserManager,
|
||||
@Application private val applicationScope: CoroutineScope,
|
||||
telephonyInteractor: TelephonyInteractor,
|
||||
broadcastDispatcher: BroadcastDispatcher,
|
||||
@Background private val backgroundDispatcher: CoroutineDispatcher,
|
||||
private val activityManager: ActivityManager,
|
||||
private val refreshUsersScheduler: RefreshUsersScheduler,
|
||||
private val guestUserInteractor: GuestUserInteractor,
|
||||
) {
|
||||
/**
|
||||
* Defines interface for classes that can be notified when the state of users on the device is
|
||||
* changed.
|
||||
*/
|
||||
fun interface UserCallback {
|
||||
/** Notifies that the state of users on the device has changed. */
|
||||
fun onUserStateChanged()
|
||||
}
|
||||
|
||||
private val isNewImpl: Boolean
|
||||
get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
|
||||
|
||||
private val supervisedUserPackageName: String?
|
||||
get() =
|
||||
applicationContext.getString(
|
||||
com.android.internal.R.string.config_supervisedUserCreationPackage
|
||||
)
|
||||
|
||||
private val callbacks = Collections.newSetFromMap(WeakHashMap<UserCallback, Boolean>())
|
||||
|
||||
/** List of current on-device users to select from. */
|
||||
val users: Flow<List<UserModel>> = repository.users
|
||||
val users: Flow<List<UserModel>>
|
||||
get() =
|
||||
if (isNewImpl) {
|
||||
combine(
|
||||
repository.userInfos,
|
||||
repository.selectedUserInfo,
|
||||
repository.userSwitcherSettings,
|
||||
) { userInfos, selectedUserInfo, settings ->
|
||||
toUserModels(
|
||||
userInfos = userInfos,
|
||||
selectedUserId = selectedUserInfo.id,
|
||||
isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
repository.users
|
||||
}
|
||||
|
||||
/** The currently-selected user. */
|
||||
val selectedUser: Flow<UserModel> = repository.selectedUser
|
||||
val selectedUser: Flow<UserModel>
|
||||
get() =
|
||||
if (isNewImpl) {
|
||||
combine(
|
||||
repository.selectedUserInfo,
|
||||
repository.userSwitcherSettings,
|
||||
) { selectedUserInfo, settings ->
|
||||
val selectedUserId = selectedUserInfo.id
|
||||
checkNotNull(
|
||||
toUserModel(
|
||||
userInfo = selectedUserInfo,
|
||||
selectedUserId = selectedUserId,
|
||||
canSwitchUsers = canSwitchUsers(selectedUserId),
|
||||
isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
repository.selectedUser
|
||||
}
|
||||
|
||||
/** List of user-switcher related actions that are available. */
|
||||
val actions: Flow<List<UserActionModel>> =
|
||||
combine(
|
||||
repository.isActionableWhenLocked,
|
||||
keyguardInteractor.isKeyguardShowing,
|
||||
) { isActionableWhenLocked, isLocked ->
|
||||
isActionableWhenLocked || !isLocked
|
||||
}
|
||||
.flatMapLatest { isActionable ->
|
||||
if (isActionable) {
|
||||
repository.actions.map { actions ->
|
||||
actions +
|
||||
if (actions.isNotEmpty()) {
|
||||
// If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because
|
||||
// that's a user
|
||||
// switcher specific action that is not known to the our data source
|
||||
// or other
|
||||
// features.
|
||||
listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
|
||||
} else {
|
||||
// If no actions, don't add the navigate action.
|
||||
emptyList()
|
||||
}
|
||||
val actions: Flow<List<UserActionModel>>
|
||||
get() =
|
||||
if (isNewImpl) {
|
||||
combine(
|
||||
repository.userInfos,
|
||||
repository.userSwitcherSettings,
|
||||
keyguardInteractor.isKeyguardShowing,
|
||||
) { userInfos, settings, isDeviceLocked ->
|
||||
buildList {
|
||||
val hasGuestUser = userInfos.any { it.isGuest }
|
||||
if (
|
||||
!hasGuestUser &&
|
||||
(guestUserInteractor.isGuestUserAutoCreated ||
|
||||
UserActionsUtil.canCreateGuest(
|
||||
manager,
|
||||
repository,
|
||||
settings.isUserSwitcherEnabled,
|
||||
settings.isAddUsersFromLockscreen,
|
||||
))
|
||||
) {
|
||||
add(UserActionModel.ENTER_GUEST_MODE)
|
||||
}
|
||||
|
||||
if (isDeviceLocked && !settings.isAddUsersFromLockscreen) {
|
||||
// The device is locked and our setting to allow actions that add users
|
||||
// from the lock-screen is not enabled. The guest action from above is
|
||||
// always allowed, even when the device is locked, but the various "add
|
||||
// user" actions below are not. We can finish building the list here.
|
||||
return@buildList
|
||||
}
|
||||
|
||||
if (
|
||||
UserActionsUtil.canCreateUser(
|
||||
manager,
|
||||
repository,
|
||||
settings.isUserSwitcherEnabled,
|
||||
settings.isAddUsersFromLockscreen,
|
||||
)
|
||||
) {
|
||||
add(UserActionModel.ADD_USER)
|
||||
}
|
||||
|
||||
if (
|
||||
UserActionsUtil.canCreateSupervisedUser(
|
||||
manager,
|
||||
repository,
|
||||
settings.isUserSwitcherEnabled,
|
||||
settings.isAddUsersFromLockscreen,
|
||||
supervisedUserPackageName,
|
||||
)
|
||||
) {
|
||||
add(UserActionModel.ADD_SUPERVISED_USER)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If not actionable it means that we're not allowed to show actions when locked
|
||||
// and we
|
||||
// are locked. Therefore, we should show no actions.
|
||||
flowOf(emptyList())
|
||||
}
|
||||
} else {
|
||||
combine(
|
||||
repository.isActionableWhenLocked,
|
||||
keyguardInteractor.isKeyguardShowing,
|
||||
) { isActionableWhenLocked, isLocked ->
|
||||
isActionableWhenLocked || !isLocked
|
||||
}
|
||||
.flatMapLatest { isActionable ->
|
||||
if (isActionable) {
|
||||
repository.actions.map { actions ->
|
||||
actions +
|
||||
if (actions.isNotEmpty()) {
|
||||
// If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT
|
||||
// because that's a user switcher specific action that is
|
||||
// not known to the our data source or other features.
|
||||
listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
|
||||
} else {
|
||||
// If no actions, don't add the navigate action.
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If not actionable it means that we're not allowed to show actions
|
||||
// when
|
||||
// locked and we are locked. Therefore, we should show no actions.
|
||||
flowOf(emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val userRecords: StateFlow<ArrayList<UserRecord>> =
|
||||
if (isNewImpl) {
|
||||
combine(
|
||||
repository.userInfos,
|
||||
repository.selectedUserInfo,
|
||||
actions,
|
||||
repository.userSwitcherSettings,
|
||||
) { userInfos, selectedUserInfo, actionModels, settings ->
|
||||
ArrayList(
|
||||
userInfos.map {
|
||||
toRecord(
|
||||
userInfo = it,
|
||||
selectedUserId = selectedUserInfo.id,
|
||||
)
|
||||
} +
|
||||
actionModels.map {
|
||||
toRecord(
|
||||
action = it,
|
||||
selectedUserId = selectedUserInfo.id,
|
||||
isAddFromLockscreenEnabled = settings.isAddUsersFromLockscreen,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
scope = applicationScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = ArrayList(),
|
||||
)
|
||||
} else {
|
||||
MutableStateFlow(ArrayList())
|
||||
}
|
||||
|
||||
val selectedUserRecord: StateFlow<UserRecord?> =
|
||||
if (isNewImpl) {
|
||||
repository.selectedUserInfo
|
||||
.map { selectedUserInfo ->
|
||||
toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id)
|
||||
}
|
||||
.stateIn(
|
||||
scope = applicationScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = null,
|
||||
)
|
||||
} else {
|
||||
MutableStateFlow(null)
|
||||
}
|
||||
|
||||
/** Whether the device is configured to always have a guest user available. */
|
||||
val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated
|
||||
val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated
|
||||
|
||||
/** Whether the guest user is currently being reset. */
|
||||
val isGuestUserResetting: Boolean = repository.isGuestUserResetting
|
||||
val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting
|
||||
|
||||
private val _dialogShowRequests = MutableStateFlow<ShowDialogRequestModel?>(null)
|
||||
val dialogShowRequests: Flow<ShowDialogRequestModel?> = _dialogShowRequests.asStateFlow()
|
||||
|
||||
private val _dialogDismissRequests = MutableStateFlow<Unit?>(null)
|
||||
val dialogDismissRequests: Flow<Unit?> = _dialogDismissRequests.asStateFlow()
|
||||
|
||||
val isSimpleUserSwitcher: Boolean
|
||||
get() =
|
||||
if (isNewImpl) {
|
||||
repository.isSimpleUserSwitcher()
|
||||
} else {
|
||||
error("Not supported in the old implementation!")
|
||||
}
|
||||
|
||||
fun addCallback(callback: UserCallback) {
|
||||
callbacks.add(callback)
|
||||
}
|
||||
|
||||
fun removeCallback(callback: UserCallback) {
|
||||
callbacks.remove(callback)
|
||||
}
|
||||
|
||||
fun onDialogShown() {
|
||||
_dialogShowRequests.value = null
|
||||
}
|
||||
|
||||
fun onDialogDismissed() {
|
||||
_dialogDismissRequests.value = null
|
||||
}
|
||||
|
||||
private fun showDialog(request: ShowDialogRequestModel) {
|
||||
_dialogShowRequests.value = request
|
||||
}
|
||||
|
||||
private fun dismissDialog() {
|
||||
_dialogDismissRequests.value = Unit
|
||||
}
|
||||
|
||||
init {
|
||||
if (isNewImpl) {
|
||||
refreshUsersScheduler.refreshIfNotPaused()
|
||||
telephonyInteractor.callState
|
||||
.distinctUntilChanged()
|
||||
.onEach { refreshUsersScheduler.refreshIfNotPaused() }
|
||||
.launchIn(applicationScope)
|
||||
|
||||
combine(
|
||||
broadcastDispatcher.broadcastFlow(
|
||||
filter =
|
||||
IntentFilter().apply {
|
||||
addAction(Intent.ACTION_USER_ADDED)
|
||||
addAction(Intent.ACTION_USER_REMOVED)
|
||||
addAction(Intent.ACTION_USER_INFO_CHANGED)
|
||||
addAction(Intent.ACTION_USER_SWITCHED)
|
||||
addAction(Intent.ACTION_USER_STOPPED)
|
||||
addAction(Intent.ACTION_USER_UNLOCKED)
|
||||
},
|
||||
user = UserHandle.SYSTEM,
|
||||
map = { intent, _ -> intent },
|
||||
),
|
||||
repository.selectedUserInfo.pairwise(null),
|
||||
) { intent, selectedUserChange ->
|
||||
Pair(intent, selectedUserChange.previousValue)
|
||||
}
|
||||
.onEach { (intent, previousSelectedUser) ->
|
||||
onBroadcastReceived(intent, previousSelectedUser)
|
||||
}
|
||||
.launchIn(applicationScope)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeviceBootCompleted() {
|
||||
guestUserInteractor.onDeviceBootCompleted()
|
||||
}
|
||||
|
||||
/** Switches to the user with the given user ID. */
|
||||
fun selectUser(
|
||||
userId: Int,
|
||||
newlySelectedUserId: Int,
|
||||
) {
|
||||
controller.onUserSelected(userId, /* dialogShower= */ null)
|
||||
if (isNewImpl) {
|
||||
val currentlySelectedUserInfo = repository.getSelectedUserInfo()
|
||||
if (
|
||||
newlySelectedUserId == currentlySelectedUserInfo.id &&
|
||||
currentlySelectedUserInfo.isGuest
|
||||
) {
|
||||
// Here when clicking on the currently-selected guest user to leave guest mode
|
||||
// and return to the previously-selected non-guest user.
|
||||
showDialog(
|
||||
ShowDialogRequestModel.ShowExitGuestDialog(
|
||||
guestUserId = currentlySelectedUserInfo.id,
|
||||
targetUserId = repository.lastSelectedNonGuestUserId,
|
||||
isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
|
||||
isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
|
||||
onExitGuestUser = this::exitGuestUser,
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (currentlySelectedUserInfo.isGuest) {
|
||||
// Here when switching from guest to a non-guest user.
|
||||
showDialog(
|
||||
ShowDialogRequestModel.ShowExitGuestDialog(
|
||||
guestUserId = currentlySelectedUserInfo.id,
|
||||
targetUserId = newlySelectedUserId,
|
||||
isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
|
||||
isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
|
||||
onExitGuestUser = this::exitGuestUser,
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
switchUser(newlySelectedUserId)
|
||||
} else {
|
||||
controller.onUserSelected(newlySelectedUserId, /* dialogShower= */ null)
|
||||
}
|
||||
}
|
||||
|
||||
/** Executes the given action. */
|
||||
fun executeAction(action: UserActionModel) {
|
||||
when (action) {
|
||||
UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null)
|
||||
UserActionModel.ADD_USER -> controller.showAddUserDialog(null)
|
||||
UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity()
|
||||
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
|
||||
activityStarter.startActivity(
|
||||
Intent(Settings.ACTION_USER_SETTINGS),
|
||||
/* dismissShade= */ false,
|
||||
)
|
||||
if (isNewImpl) {
|
||||
when (action) {
|
||||
UserActionModel.ENTER_GUEST_MODE ->
|
||||
guestUserInteractor.createAndSwitchTo(
|
||||
this::showDialog,
|
||||
this::dismissDialog,
|
||||
this::selectUser,
|
||||
)
|
||||
UserActionModel.ADD_USER -> {
|
||||
val currentUser = repository.getSelectedUserInfo()
|
||||
showDialog(
|
||||
ShowDialogRequestModel.ShowAddUserDialog(
|
||||
userHandle = currentUser.userHandle,
|
||||
isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
|
||||
showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral,
|
||||
)
|
||||
)
|
||||
}
|
||||
UserActionModel.ADD_SUPERVISED_USER ->
|
||||
activityStarter.startActivity(
|
||||
Intent()
|
||||
.setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
|
||||
.setPackage(supervisedUserPackageName)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
/* dismissShade= */ false,
|
||||
)
|
||||
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
|
||||
activityStarter.startActivity(
|
||||
Intent(Settings.ACTION_USER_SETTINGS),
|
||||
/* dismissShade= */ false,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
when (action) {
|
||||
UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null)
|
||||
UserActionModel.ADD_USER -> controller.showAddUserDialog(null)
|
||||
UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity()
|
||||
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
|
||||
activityStarter.startActivity(
|
||||
Intent(Settings.ACTION_USER_SETTINGS),
|
||||
/* dismissShade= */ false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun toRecord(
|
||||
userInfo: UserInfo,
|
||||
selectedUserId: Int,
|
||||
): UserRecord {
|
||||
return LegacyUserDataHelper.createRecord(
|
||||
context = applicationContext,
|
||||
manager = manager,
|
||||
userInfo = userInfo,
|
||||
picture = null,
|
||||
isCurrent = userInfo.id == selectedUserId,
|
||||
canSwitchUsers = canSwitchUsers(selectedUserId),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun toRecord(
|
||||
action: UserActionModel,
|
||||
selectedUserId: Int,
|
||||
isAddFromLockscreenEnabled: Boolean,
|
||||
): UserRecord {
|
||||
return LegacyUserDataHelper.createRecord(
|
||||
context = applicationContext,
|
||||
selectedUserId = selectedUserId,
|
||||
actionType = action,
|
||||
isRestricted =
|
||||
if (action == UserActionModel.ENTER_GUEST_MODE) {
|
||||
// Entering guest mode is never restricted, so it's allowed to happen from the
|
||||
// lockscreen even if the "add from lockscreen" system setting is off.
|
||||
false
|
||||
} else {
|
||||
!isAddFromLockscreenEnabled
|
||||
},
|
||||
isSwitchToEnabled =
|
||||
canSwitchUsers(selectedUserId) &&
|
||||
// If the user is auto-created is must not be currently resetting.
|
||||
!(isGuestUserAutoCreated && isGuestUserResetting),
|
||||
)
|
||||
}
|
||||
|
||||
private fun exitGuestUser(
|
||||
@UserIdInt guestUserId: Int,
|
||||
@UserIdInt targetUserId: Int,
|
||||
forceRemoveGuestOnExit: Boolean,
|
||||
) {
|
||||
guestUserInteractor.exit(
|
||||
guestUserId = guestUserId,
|
||||
targetUserId = targetUserId,
|
||||
forceRemoveGuestOnExit = forceRemoveGuestOnExit,
|
||||
showDialog = this::showDialog,
|
||||
dismissDialog = this::dismissDialog,
|
||||
switchUser = this::switchUser,
|
||||
)
|
||||
}
|
||||
|
||||
private fun switchUser(userId: Int) {
|
||||
// TODO(b/246631653): track jank and lantecy like in the old impl.
|
||||
refreshUsersScheduler.pause()
|
||||
try {
|
||||
activityManager.switchUser(userId)
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Couldn't switch user.", e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onBroadcastReceived(
|
||||
intent: Intent,
|
||||
previousUserInfo: UserInfo?,
|
||||
) {
|
||||
val shouldRefreshAllUsers =
|
||||
when (intent.action) {
|
||||
Intent.ACTION_USER_SWITCHED -> {
|
||||
dismissDialog()
|
||||
val selectedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
|
||||
if (previousUserInfo?.id != selectedUserId) {
|
||||
callbacks.forEach { it.onUserStateChanged() }
|
||||
restartSecondaryService(selectedUserId)
|
||||
}
|
||||
if (guestUserInteractor.isGuestUserAutoCreated) {
|
||||
guestUserInteractor.guaranteePresent()
|
||||
}
|
||||
true
|
||||
}
|
||||
Intent.ACTION_USER_INFO_CHANGED -> true
|
||||
Intent.ACTION_USER_UNLOCKED -> {
|
||||
// If we unlocked the system user, we should refresh all users.
|
||||
intent.getIntExtra(
|
||||
Intent.EXTRA_USER_HANDLE,
|
||||
UserHandle.USER_NULL,
|
||||
) == UserHandle.USER_SYSTEM
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
|
||||
if (shouldRefreshAllUsers) {
|
||||
refreshUsersScheduler.unpauseAndRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
private fun restartSecondaryService(@UserIdInt userId: Int) {
|
||||
val intent = Intent(applicationContext, SystemUISecondaryUserService::class.java)
|
||||
// Disconnect from the old secondary user's service
|
||||
val secondaryUserId = repository.secondaryUserId
|
||||
if (secondaryUserId != UserHandle.USER_NULL) {
|
||||
applicationContext.stopServiceAsUser(
|
||||
intent,
|
||||
UserHandle.of(secondaryUserId),
|
||||
)
|
||||
repository.secondaryUserId = UserHandle.USER_NULL
|
||||
}
|
||||
|
||||
// Connect to the new secondary user's service (purely to ensure that a persistent
|
||||
// SystemUI application is created for that user)
|
||||
if (userId != UserHandle.USER_SYSTEM) {
|
||||
applicationContext.startServiceAsUser(
|
||||
intent,
|
||||
UserHandle.of(userId),
|
||||
)
|
||||
repository.secondaryUserId = userId
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun toUserModels(
|
||||
userInfos: List<UserInfo>,
|
||||
selectedUserId: Int,
|
||||
isUserSwitcherEnabled: Boolean,
|
||||
): List<UserModel> {
|
||||
val canSwitchUsers = canSwitchUsers(selectedUserId)
|
||||
|
||||
return userInfos
|
||||
// The guest user should go in the last position.
|
||||
.sortedBy { it.isGuest }
|
||||
.mapNotNull { userInfo ->
|
||||
toUserModel(
|
||||
userInfo = userInfo,
|
||||
selectedUserId = selectedUserId,
|
||||
canSwitchUsers = canSwitchUsers,
|
||||
isUserSwitcherEnabled = isUserSwitcherEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun toUserModel(
|
||||
userInfo: UserInfo,
|
||||
selectedUserId: Int,
|
||||
canSwitchUsers: Boolean,
|
||||
isUserSwitcherEnabled: Boolean,
|
||||
): UserModel? {
|
||||
val userId = userInfo.id
|
||||
val isSelected = userId == selectedUserId
|
||||
|
||||
return when {
|
||||
// When the user switcher is not enabled in settings, we only show the primary user.
|
||||
!isUserSwitcherEnabled && !userInfo.isPrimary -> null
|
||||
|
||||
// We avoid showing disabled users.
|
||||
!userInfo.isEnabled -> null
|
||||
userInfo.isGuest ->
|
||||
UserModel(
|
||||
id = userId,
|
||||
name = Text.Loaded(userInfo.name),
|
||||
image =
|
||||
getUserImage(
|
||||
isGuest = true,
|
||||
userId = userId,
|
||||
),
|
||||
isSelected = isSelected,
|
||||
isSelectable = canSwitchUsers,
|
||||
isGuest = true,
|
||||
)
|
||||
userInfo.supportsSwitchToByUser() ->
|
||||
UserModel(
|
||||
id = userId,
|
||||
name = Text.Loaded(userInfo.name),
|
||||
image =
|
||||
getUserImage(
|
||||
isGuest = false,
|
||||
userId = userId,
|
||||
),
|
||||
isSelected = isSelected,
|
||||
isSelectable = canSwitchUsers || isSelected,
|
||||
isGuest = false,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun canSwitchUsers(selectedUserId: Int): Boolean {
|
||||
return withContext(backgroundDispatcher) {
|
||||
manager.getUserSwitchability(UserHandle.of(selectedUserId))
|
||||
} == UserManager.SWITCHABILITY_STATUS_OK
|
||||
}
|
||||
|
||||
@SuppressLint("UseCompatLoadingForDrawables")
|
||||
private suspend fun getUserImage(
|
||||
isGuest: Boolean,
|
||||
userId: Int,
|
||||
): Drawable {
|
||||
if (isGuest) {
|
||||
return checkNotNull(applicationContext.getDrawable(R.drawable.ic_account_circle))
|
||||
}
|
||||
|
||||
// TODO(b/246631653): cache the bitmaps to avoid the background work to fetch them.
|
||||
// TODO(b/246631653): downscale the bitmaps to R.dimen.max_avatar_size if requested.
|
||||
val userIcon = withContext(backgroundDispatcher) { manager.getUserIcon(userId) }
|
||||
if (userIcon != null) {
|
||||
return BitmapDrawable(userIcon)
|
||||
}
|
||||
|
||||
return UserIcons.getDefaultUserIcon(
|
||||
applicationContext.resources,
|
||||
userId,
|
||||
/* light= */ false
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "UserInteractor"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -32,4 +32,6 @@ data class UserModel(
|
||||
val isSelected: Boolean,
|
||||
/** Whether this use is selectable. A non-selectable user cannot be switched to. */
|
||||
val isSelectable: Boolean,
|
||||
/** Whether this model represents the guest user. */
|
||||
val isGuest: Boolean,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,10 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.common.ui.drawable.CircularDrawable
|
||||
import com.android.systemui.flags.FeatureFlags
|
||||
import com.android.systemui.flags.Flags
|
||||
import com.android.systemui.power.domain.interactor.PowerInteractor
|
||||
import com.android.systemui.user.domain.interactor.GuestUserInteractor
|
||||
import com.android.systemui.user.domain.interactor.UserInteractor
|
||||
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
|
||||
import com.android.systemui.user.shared.model.UserActionModel
|
||||
@@ -36,9 +39,14 @@ import kotlinx.coroutines.flow.map
|
||||
class UserSwitcherViewModel
|
||||
private constructor(
|
||||
private val userInteractor: UserInteractor,
|
||||
private val guestUserInteractor: GuestUserInteractor,
|
||||
private val powerInteractor: PowerInteractor,
|
||||
private val featureFlags: FeatureFlags,
|
||||
) : ViewModel() {
|
||||
|
||||
private val isNewImpl: Boolean
|
||||
get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
|
||||
|
||||
/** On-device users. */
|
||||
val users: Flow<List<UserViewModel>> =
|
||||
userInteractor.users.map { models -> models.map { user -> toViewModel(user) } }
|
||||
@@ -47,9 +55,6 @@ private constructor(
|
||||
val maximumUserColumns: Flow<Int> =
|
||||
users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) }
|
||||
|
||||
/** Whether the button to open the user action menu is visible. */
|
||||
val isOpenMenuButtonVisible: Flow<Boolean> = userInteractor.actions.map { it.isNotEmpty() }
|
||||
|
||||
private val _isMenuVisible = MutableStateFlow(false)
|
||||
/**
|
||||
* Whether the user action menu should be shown. Once the action menu is dismissed/closed, the
|
||||
@@ -58,9 +63,23 @@ private constructor(
|
||||
val isMenuVisible: Flow<Boolean> = _isMenuVisible
|
||||
/** The user action menu. */
|
||||
val menu: Flow<List<UserActionViewModel>> =
|
||||
userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } }
|
||||
userInteractor.actions.map { actions ->
|
||||
if (isNewImpl && actions.isNotEmpty()) {
|
||||
// If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user
|
||||
// switcher specific action that is not known to the our data source or other
|
||||
// features.
|
||||
actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
|
||||
} else {
|
||||
actions
|
||||
}
|
||||
.map { action -> toViewModel(action) }
|
||||
}
|
||||
|
||||
/** Whether the button to open the user action menu is visible. */
|
||||
val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() }
|
||||
|
||||
private val hasCancelButtonBeenClicked = MutableStateFlow(false)
|
||||
private val isFinishRequiredDueToExecutedAction = MutableStateFlow(false)
|
||||
|
||||
/**
|
||||
* Whether the observer should finish the experience. Once consumed, [onFinished] must be called
|
||||
@@ -81,6 +100,7 @@ private constructor(
|
||||
*/
|
||||
fun onFinished() {
|
||||
hasCancelButtonBeenClicked.value = false
|
||||
isFinishRequiredDueToExecutedAction.value = false
|
||||
}
|
||||
|
||||
/** Notifies that the user has clicked the "open menu" button. */
|
||||
@@ -120,8 +140,10 @@ private constructor(
|
||||
},
|
||||
// When the cancel button is clicked, we should finish.
|
||||
hasCancelButtonBeenClicked,
|
||||
) { selectedUserChanged, screenTurnedOff, cancelButtonClicked ->
|
||||
selectedUserChanged || screenTurnedOff || cancelButtonClicked
|
||||
// If an executed action told us to finish, we should finish,
|
||||
isFinishRequiredDueToExecutedAction,
|
||||
) { selectedUserChanged, screenTurnedOff, cancelButtonClicked, executedActionFinish ->
|
||||
selectedUserChanged || screenTurnedOff || cancelButtonClicked || executedActionFinish
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,13 +186,25 @@ private constructor(
|
||||
} else {
|
||||
LegacyUserUiHelper.getUserSwitcherActionTextResourceId(
|
||||
isGuest = model == UserActionModel.ENTER_GUEST_MODE,
|
||||
isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
|
||||
isGuestUserResetting = userInteractor.isGuestUserResetting,
|
||||
isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated,
|
||||
isGuestUserResetting = guestUserInteractor.isGuestUserResetting,
|
||||
isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
|
||||
isAddUser = model == UserActionModel.ADD_USER,
|
||||
)
|
||||
},
|
||||
onClicked = { userInteractor.executeAction(action = model) },
|
||||
onClicked = {
|
||||
userInteractor.executeAction(action = model)
|
||||
// We don't finish because we want to show a dialog over the full-screen UI and
|
||||
// that dialog can be dismissed in case the user changes their mind and decides not
|
||||
// to add a user.
|
||||
//
|
||||
// We finish for all other actions because they navigate us away from the
|
||||
// full-screen experience or are destructive (like changing to the guest user).
|
||||
val shouldFinish = model != UserActionModel.ADD_USER
|
||||
if (shouldFinish) {
|
||||
isFinishRequiredDueToExecutedAction.value = true
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -186,13 +220,17 @@ private constructor(
|
||||
@Inject
|
||||
constructor(
|
||||
private val userInteractor: UserInteractor,
|
||||
private val guestUserInteractor: GuestUserInteractor,
|
||||
private val powerInteractor: PowerInteractor,
|
||||
private val featureFlags: FeatureFlags,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return UserSwitcherViewModel(
|
||||
userInteractor = userInteractor,
|
||||
guestUserInteractor = guestUserInteractor,
|
||||
powerInteractor = powerInteractor,
|
||||
featureFlags = featureFlags,
|
||||
)
|
||||
as T
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -411,7 +411,7 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase {
|
||||
0 /* flags */);
|
||||
users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */,
|
||||
false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */,
|
||||
false /* isAddSupervisedUser */));
|
||||
false /* isAddSupervisedUser */, null /* enforcedAdmin */));
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
|
||||
val job = underTest.isKeyguardShowing.onEach { latest = it }.launchIn(this)
|
||||
|
||||
assertThat(latest).isFalse()
|
||||
assertThat(underTest.isKeyguardShowing()).isFalse()
|
||||
|
||||
val captor = argumentCaptor<KeyguardStateController.Callback>()
|
||||
verify(keyguardStateController).addCallback(captor.capture())
|
||||
@@ -123,10 +124,12 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
|
||||
whenever(keyguardStateController.isShowing).thenReturn(true)
|
||||
captor.value.onKeyguardShowingChanged()
|
||||
assertThat(latest).isTrue()
|
||||
assertThat(underTest.isKeyguardShowing()).isTrue()
|
||||
|
||||
whenever(keyguardStateController.isShowing).thenReturn(false)
|
||||
captor.value.onKeyguardShowingChanged()
|
||||
assertThat(latest).isFalse()
|
||||
assertThat(underTest.isKeyguardShowing()).isFalse()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -17,201 +17,54 @@
|
||||
|
||||
package com.android.systemui.user.data.repository
|
||||
|
||||
import android.content.pm.UserInfo
|
||||
import android.os.UserManager
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.flags.FakeFeatureFlags
|
||||
import com.android.systemui.flags.Flags
|
||||
import com.android.systemui.settings.FakeUserTracker
|
||||
import com.android.systemui.statusbar.policy.UserSwitcherController
|
||||
import com.android.systemui.user.data.source.UserRecord
|
||||
import com.android.systemui.user.shared.model.UserActionModel
|
||||
import com.android.systemui.user.shared.model.UserModel
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.capture
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.android.systemui.util.settings.FakeSettings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Captor
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@RunWith(JUnit4::class)
|
||||
class UserRepositoryImplTest : SysuiTestCase() {
|
||||
abstract class UserRepositoryImplTest : SysuiTestCase() {
|
||||
|
||||
@Mock private lateinit var manager: UserManager
|
||||
@Mock private lateinit var controller: UserSwitcherController
|
||||
@Captor
|
||||
private lateinit var userSwitchCallbackCaptor:
|
||||
ArgumentCaptor<UserSwitcherController.UserSwitchCallback>
|
||||
@Mock protected lateinit var manager: UserManager
|
||||
@Mock protected lateinit var controller: UserSwitcherController
|
||||
|
||||
private lateinit var underTest: UserRepositoryImpl
|
||||
protected lateinit var underTest: UserRepositoryImpl
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
protected lateinit var globalSettings: FakeSettings
|
||||
protected lateinit var tracker: FakeUserTracker
|
||||
protected lateinit var featureFlags: FakeFeatureFlags
|
||||
|
||||
protected fun setUp(isRefactored: Boolean) {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false))
|
||||
whenever(controller.isGuestUserAutoCreated).thenReturn(false)
|
||||
whenever(controller.isGuestUserResetting).thenReturn(false)
|
||||
|
||||
underTest =
|
||||
UserRepositoryImpl(
|
||||
appContext = context,
|
||||
manager = manager,
|
||||
controller = controller,
|
||||
)
|
||||
globalSettings = FakeSettings()
|
||||
tracker = FakeUserTracker()
|
||||
featureFlags = FakeFeatureFlags()
|
||||
featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored)
|
||||
}
|
||||
|
||||
@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 `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,
|
||||
protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl {
|
||||
return UserRepositoryImpl(
|
||||
appContext = context,
|
||||
manager = manager,
|
||||
controller = controller,
|
||||
applicationScope = scope,
|
||||
mainDispatcher = IMMEDIATE,
|
||||
backgroundDispatcher = IMMEDIATE,
|
||||
globalSettings = globalSettings,
|
||||
tracker = tracker,
|
||||
featureFlags = featureFlags,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val IMMEDIATE = Dispatchers.Main.immediate
|
||||
@JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -17,51 +17,61 @@
|
||||
|
||||
package com.android.systemui.user.domain.interactor
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import android.app.ActivityManager
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.os.UserManager
|
||||
import com.android.internal.logging.UiEventLogger
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.flags.FakeFeatureFlags
|
||||
import com.android.systemui.flags.Flags
|
||||
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
|
||||
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
|
||||
import com.android.systemui.plugins.ActivityStarter
|
||||
import com.android.systemui.statusbar.policy.DeviceProvisionedController
|
||||
import com.android.systemui.statusbar.policy.UserSwitcherController
|
||||
import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
|
||||
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
|
||||
import com.android.systemui.user.data.repository.FakeUserRepository
|
||||
import com.android.systemui.user.shared.model.UserActionModel
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import com.android.systemui.util.mockito.nullable
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.anyBoolean
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@RunWith(JUnit4::class)
|
||||
class UserInteractorTest : SysuiTestCase() {
|
||||
abstract class UserInteractorTest : SysuiTestCase() {
|
||||
|
||||
@Mock private lateinit var controller: UserSwitcherController
|
||||
@Mock private lateinit var activityStarter: ActivityStarter
|
||||
@Mock protected lateinit var controller: UserSwitcherController
|
||||
@Mock protected lateinit var activityStarter: ActivityStarter
|
||||
@Mock protected lateinit var manager: UserManager
|
||||
@Mock protected lateinit var activityManager: ActivityManager
|
||||
@Mock protected lateinit var deviceProvisionedController: DeviceProvisionedController
|
||||
@Mock protected lateinit var devicePolicyManager: DevicePolicyManager
|
||||
@Mock protected lateinit var uiEventLogger: UiEventLogger
|
||||
|
||||
private lateinit var underTest: UserInteractor
|
||||
protected lateinit var underTest: UserInteractor
|
||||
|
||||
private lateinit var userRepository: FakeUserRepository
|
||||
private lateinit var keyguardRepository: FakeKeyguardRepository
|
||||
protected lateinit var testCoroutineScope: TestCoroutineScope
|
||||
protected lateinit var userRepository: FakeUserRepository
|
||||
protected lateinit var keyguardRepository: FakeKeyguardRepository
|
||||
protected lateinit var telephonyRepository: FakeTelephonyRepository
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
abstract fun isRefactored(): Boolean
|
||||
|
||||
open fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
|
||||
userRepository = FakeUserRepository()
|
||||
keyguardRepository = FakeKeyguardRepository()
|
||||
telephonyRepository = FakeTelephonyRepository()
|
||||
testCoroutineScope = TestCoroutineScope()
|
||||
val refreshUsersScheduler =
|
||||
RefreshUsersScheduler(
|
||||
applicationScope = testCoroutineScope,
|
||||
mainDispatcher = IMMEDIATE,
|
||||
repository = userRepository,
|
||||
)
|
||||
underTest =
|
||||
UserInteractor(
|
||||
applicationContext = context,
|
||||
repository = userRepository,
|
||||
controller = controller,
|
||||
activityStarter = activityStarter,
|
||||
@@ -69,142 +79,34 @@ class UserInteractorTest : SysuiTestCase() {
|
||||
KeyguardInteractor(
|
||||
repository = keyguardRepository,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `actions - not actionable when locked and locked - no actions`() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
userRepository.setActions(UserActionModel.values().toList())
|
||||
userRepository.setActionableWhenLocked(false)
|
||||
keyguardRepository.setKeyguardShowing(true)
|
||||
|
||||
var actions: List<UserActionModel>? = null
|
||||
val job = underTest.actions.onEach { actions = it }.launchIn(this)
|
||||
|
||||
assertThat(actions).isEmpty()
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `actions - not actionable when locked and not locked`() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
userRepository.setActions(
|
||||
listOf(
|
||||
UserActionModel.ENTER_GUEST_MODE,
|
||||
UserActionModel.ADD_USER,
|
||||
UserActionModel.ADD_SUPERVISED_USER,
|
||||
)
|
||||
)
|
||||
userRepository.setActionableWhenLocked(false)
|
||||
keyguardRepository.setKeyguardShowing(false)
|
||||
|
||||
var actions: List<UserActionModel>? = null
|
||||
val job = underTest.actions.onEach { actions = it }.launchIn(this)
|
||||
|
||||
assertThat(actions)
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
UserActionModel.ENTER_GUEST_MODE,
|
||||
UserActionModel.ADD_USER,
|
||||
UserActionModel.ADD_SUPERVISED_USER,
|
||||
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
|
||||
featureFlags =
|
||||
FakeFeatureFlags().apply {
|
||||
set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored())
|
||||
},
|
||||
manager = manager,
|
||||
applicationScope = testCoroutineScope,
|
||||
telephonyInteractor =
|
||||
TelephonyInteractor(
|
||||
repository = telephonyRepository,
|
||||
),
|
||||
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||
backgroundDispatcher = IMMEDIATE,
|
||||
activityManager = activityManager,
|
||||
refreshUsersScheduler = refreshUsersScheduler,
|
||||
guestUserInteractor =
|
||||
GuestUserInteractor(
|
||||
applicationContext = context,
|
||||
applicationScope = testCoroutineScope,
|
||||
mainDispatcher = IMMEDIATE,
|
||||
backgroundDispatcher = IMMEDIATE,
|
||||
manager = manager,
|
||||
repository = userRepository,
|
||||
deviceProvisionedController = deviceProvisionedController,
|
||||
devicePolicyManager = devicePolicyManager,
|
||||
refreshUsersScheduler = refreshUsersScheduler,
|
||||
uiEventLogger = uiEventLogger,
|
||||
)
|
||||
)
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `actions - actionable when locked and 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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -17,17 +17,28 @@
|
||||
|
||||
package com.android.systemui.user.ui.viewmodel
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.UserManager
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.internal.logging.UiEventLogger
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.common.shared.model.Text
|
||||
import com.android.systemui.flags.FakeFeatureFlags
|
||||
import com.android.systemui.flags.Flags
|
||||
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
|
||||
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
|
||||
import com.android.systemui.plugins.ActivityStarter
|
||||
import com.android.systemui.power.data.repository.FakePowerRepository
|
||||
import com.android.systemui.power.domain.interactor.PowerInteractor
|
||||
import com.android.systemui.statusbar.policy.DeviceProvisionedController
|
||||
import com.android.systemui.statusbar.policy.UserSwitcherController
|
||||
import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
|
||||
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
|
||||
import com.android.systemui.user.data.repository.FakeUserRepository
|
||||
import com.android.systemui.user.domain.interactor.GuestUserInteractor
|
||||
import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
|
||||
import com.android.systemui.user.domain.interactor.UserInteractor
|
||||
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
|
||||
import com.android.systemui.user.shared.model.UserActionModel
|
||||
@@ -38,6 +49,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
import kotlinx.coroutines.yield
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -52,6 +64,11 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
|
||||
|
||||
@Mock private lateinit var controller: UserSwitcherController
|
||||
@Mock private lateinit var activityStarter: ActivityStarter
|
||||
@Mock private lateinit var activityManager: ActivityManager
|
||||
@Mock private lateinit var manager: UserManager
|
||||
@Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
|
||||
@Mock private lateinit var devicePolicyManager: DevicePolicyManager
|
||||
@Mock private lateinit var uiEventLogger: UiEventLogger
|
||||
|
||||
private lateinit var underTest: UserSwitcherViewModel
|
||||
|
||||
@@ -66,22 +83,60 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
|
||||
userRepository = FakeUserRepository()
|
||||
keyguardRepository = FakeKeyguardRepository()
|
||||
powerRepository = FakePowerRepository()
|
||||
val featureFlags = FakeFeatureFlags()
|
||||
featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, true)
|
||||
val scope = TestCoroutineScope()
|
||||
val refreshUsersScheduler =
|
||||
RefreshUsersScheduler(
|
||||
applicationScope = scope,
|
||||
mainDispatcher = IMMEDIATE,
|
||||
repository = userRepository,
|
||||
)
|
||||
val guestUserInteractor =
|
||||
GuestUserInteractor(
|
||||
applicationContext = context,
|
||||
applicationScope = scope,
|
||||
mainDispatcher = IMMEDIATE,
|
||||
backgroundDispatcher = IMMEDIATE,
|
||||
manager = manager,
|
||||
repository = userRepository,
|
||||
deviceProvisionedController = deviceProvisionedController,
|
||||
devicePolicyManager = devicePolicyManager,
|
||||
refreshUsersScheduler = refreshUsersScheduler,
|
||||
uiEventLogger = uiEventLogger,
|
||||
)
|
||||
|
||||
underTest =
|
||||
UserSwitcherViewModel.Factory(
|
||||
userInteractor =
|
||||
UserInteractor(
|
||||
applicationContext = context,
|
||||
repository = userRepository,
|
||||
controller = controller,
|
||||
activityStarter = activityStarter,
|
||||
keyguardInteractor =
|
||||
KeyguardInteractor(
|
||||
repository = keyguardRepository,
|
||||
)
|
||||
),
|
||||
featureFlags = featureFlags,
|
||||
manager = manager,
|
||||
applicationScope = scope,
|
||||
telephonyInteractor =
|
||||
TelephonyInteractor(
|
||||
repository = FakeTelephonyRepository(),
|
||||
),
|
||||
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||
backgroundDispatcher = IMMEDIATE,
|
||||
activityManager = activityManager,
|
||||
refreshUsersScheduler = refreshUsersScheduler,
|
||||
guestUserInteractor = guestUserInteractor,
|
||||
),
|
||||
powerInteractor =
|
||||
PowerInteractor(
|
||||
repository = powerRepository,
|
||||
),
|
||||
featureFlags = featureFlags,
|
||||
guestUserInteractor = guestUserInteractor,
|
||||
)
|
||||
.create(UserSwitcherViewModel::class.java)
|
||||
}
|
||||
@@ -97,6 +152,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
|
||||
image = USER_IMAGE,
|
||||
isSelected = true,
|
||||
isSelectable = true,
|
||||
isGuest = false,
|
||||
),
|
||||
UserModel(
|
||||
id = 1,
|
||||
@@ -104,6 +160,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
|
||||
image = USER_IMAGE,
|
||||
isSelected = false,
|
||||
isSelectable = true,
|
||||
isGuest = false,
|
||||
),
|
||||
UserModel(
|
||||
id = 2,
|
||||
@@ -111,6 +168,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
|
||||
image = USER_IMAGE,
|
||||
isSelected = false,
|
||||
isSelectable = false,
|
||||
isGuest = false,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -260,7 +318,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
private fun setUsers(count: Int) {
|
||||
private suspend fun setUsers(count: Int) {
|
||||
userRepository.setUsers(
|
||||
(0 until count).map { index ->
|
||||
UserModel(
|
||||
@@ -269,6 +327,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
|
||||
image = USER_IMAGE,
|
||||
isSelected = index == 0,
|
||||
isSelectable = true,
|
||||
isGuest = false,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -37,10 +37,18 @@ class FakeBroadcastDispatcher(
|
||||
dumpManager: DumpManager,
|
||||
logger: BroadcastDispatcherLogger,
|
||||
userTracker: UserTracker
|
||||
) : BroadcastDispatcher(
|
||||
context, looper, executor, dumpManager, logger, userTracker, PendingRemovalStore(logger)) {
|
||||
) :
|
||||
BroadcastDispatcher(
|
||||
context,
|
||||
looper,
|
||||
executor,
|
||||
dumpManager,
|
||||
logger,
|
||||
userTracker,
|
||||
PendingRemovalStore(logger)
|
||||
) {
|
||||
|
||||
private val registeredReceivers = ArraySet<BroadcastReceiver>()
|
||||
val registeredReceivers = ArraySet<BroadcastReceiver>()
|
||||
|
||||
override fun registerReceiverWithHandler(
|
||||
receiver: BroadcastReceiver,
|
||||
@@ -78,4 +86,4 @@ class FakeBroadcastDispatcher(
|
||||
}
|
||||
registeredReceivers.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,10 @@ class FakeKeyguardRepository : KeyguardRepository {
|
||||
private val _dozeAmount = MutableStateFlow(0f)
|
||||
override val dozeAmount: Flow<Float> = _dozeAmount
|
||||
|
||||
override fun isKeyguardShowing(): Boolean {
|
||||
return _isKeyguardShowing.value
|
||||
}
|
||||
|
||||
override fun setAnimateDozingTransitions(animate: Boolean) {
|
||||
_animateBottomAreaDozingTransitions.tryEmit(animate)
|
||||
}
|
||||
|
||||
@@ -26,20 +26,24 @@ import java.util.concurrent.Executor
|
||||
|
||||
/** A fake [UserTracker] to be used in tests. */
|
||||
class FakeUserTracker(
|
||||
userId: Int = 0,
|
||||
userHandle: UserHandle = UserHandle.of(userId),
|
||||
userInfo: UserInfo = mock(),
|
||||
userProfiles: List<UserInfo> = emptyList(),
|
||||
private var _userId: Int = 0,
|
||||
private var _userHandle: UserHandle = UserHandle.of(_userId),
|
||||
private var _userInfo: UserInfo = mock(),
|
||||
private var _userProfiles: List<UserInfo> = emptyList(),
|
||||
userContentResolver: ContentResolver = MockContentResolver(),
|
||||
userContext: Context = mock(),
|
||||
private val onCreateCurrentUserContext: (Context) -> Context = { mock() },
|
||||
) : UserTracker {
|
||||
val callbacks = mutableListOf<UserTracker.Callback>()
|
||||
|
||||
override val userId: Int = userId
|
||||
override val userHandle: UserHandle = userHandle
|
||||
override val userInfo: UserInfo = userInfo
|
||||
override val userProfiles: List<UserInfo> = userProfiles
|
||||
override val userId: Int
|
||||
get() = _userId
|
||||
override val userHandle: UserHandle
|
||||
get() = _userHandle
|
||||
override val userInfo: UserInfo
|
||||
get() = _userInfo
|
||||
override val userProfiles: List<UserInfo>
|
||||
get() = _userProfiles
|
||||
|
||||
override val userContentResolver: ContentResolver = userContentResolver
|
||||
override val userContext: Context = userContext
|
||||
@@ -55,4 +59,13 @@ class FakeUserTracker(
|
||||
override fun createCurrentUserContext(context: Context): Context {
|
||||
return onCreateCurrentUserContext(context)
|
||||
}
|
||||
|
||||
fun set(userInfos: List<UserInfo>, selectedUserIndex: Int) {
|
||||
_userProfiles = userInfos
|
||||
_userInfo = userInfos[selectedUserIndex]
|
||||
_userId = _userInfo.id
|
||||
_userHandle = UserHandle.of(_userId)
|
||||
|
||||
callbacks.forEach { it.onUserChanged(_userId, userContext) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,18 @@
|
||||
|
||||
package com.android.systemui.user.data.repository
|
||||
|
||||
import android.content.pm.UserInfo
|
||||
import android.os.UserHandle
|
||||
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
|
||||
import com.android.systemui.user.shared.model.UserActionModel
|
||||
import com.android.systemui.user.shared.model.UserModel
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.yield
|
||||
|
||||
class FakeUserRepository : UserRepository {
|
||||
|
||||
@@ -34,21 +40,71 @@ class FakeUserRepository : UserRepository {
|
||||
private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList())
|
||||
override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow()
|
||||
|
||||
private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel())
|
||||
override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
|
||||
_userSwitcherSettings.asStateFlow()
|
||||
|
||||
private val _userInfos = MutableStateFlow<List<UserInfo>>(emptyList())
|
||||
override val userInfos: Flow<List<UserInfo>> = _userInfos.asStateFlow()
|
||||
|
||||
private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
|
||||
override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
|
||||
|
||||
override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
|
||||
|
||||
private val _isActionableWhenLocked = MutableStateFlow(false)
|
||||
override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow()
|
||||
|
||||
private var _isGuestUserAutoCreated: Boolean = false
|
||||
override val isGuestUserAutoCreated: Boolean
|
||||
get() = _isGuestUserAutoCreated
|
||||
private var _isGuestUserResetting: Boolean = false
|
||||
override val isGuestUserResetting: Boolean
|
||||
get() = _isGuestUserResetting
|
||||
|
||||
override var isGuestUserResetting: Boolean = false
|
||||
|
||||
override val isGuestUserCreationScheduled = AtomicBoolean()
|
||||
|
||||
override var secondaryUserId: Int = UserHandle.USER_NULL
|
||||
|
||||
override var isRefreshUsersPaused: Boolean = false
|
||||
|
||||
var refreshUsersCallCount: Int = 0
|
||||
private set
|
||||
|
||||
override fun refreshUsers() {
|
||||
refreshUsersCallCount++
|
||||
}
|
||||
|
||||
override fun getSelectedUserInfo(): UserInfo {
|
||||
return checkNotNull(_selectedUserInfo.value)
|
||||
}
|
||||
|
||||
override fun isSimpleUserSwitcher(): Boolean {
|
||||
return _userSwitcherSettings.value.isSimpleUserSwitcher
|
||||
}
|
||||
|
||||
fun setUserInfos(infos: List<UserInfo>) {
|
||||
_userInfos.value = infos
|
||||
}
|
||||
|
||||
suspend fun setSelectedUserInfo(userInfo: UserInfo) {
|
||||
check(_userInfos.value.contains(userInfo)) {
|
||||
"Cannot select the following user, it is not in the list of user infos: $userInfo!"
|
||||
}
|
||||
|
||||
_selectedUserInfo.value = userInfo
|
||||
yield()
|
||||
}
|
||||
|
||||
suspend fun setSettings(settings: UserSwitcherSettingsModel) {
|
||||
_userSwitcherSettings.value = settings
|
||||
yield()
|
||||
}
|
||||
|
||||
fun setUsers(models: List<UserModel>) {
|
||||
_users.value = models
|
||||
}
|
||||
|
||||
fun setSelectedUser(userId: Int) {
|
||||
suspend fun setSelectedUser(userId: Int) {
|
||||
check(_users.value.find { it.id == userId } != null) {
|
||||
"Cannot select a user with ID $userId - no user with that ID found!"
|
||||
}
|
||||
@@ -62,6 +118,7 @@ class FakeUserRepository : UserRepository {
|
||||
}
|
||||
}
|
||||
)
|
||||
yield()
|
||||
}
|
||||
|
||||
fun setActions(models: List<UserActionModel>) {
|
||||
@@ -75,8 +132,4 @@ class FakeUserRepository : UserRepository {
|
||||
fun setGuestUserAutoCreated(value: Boolean) {
|
||||
_isGuestUserAutoCreated = value
|
||||
}
|
||||
|
||||
fun setGuestUserResetting(value: Boolean) {
|
||||
_isGuestUserResetting = value
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user