Merge changes from topics "usc-less-domain", "usc-less-split-flag", "usc-less-ui" into tm-qpr-dev am: 3cd2429e55
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20064881 Change-Id: I0dc00354e9a0cffe33a10f96ac26b191eb18f111 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
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