Prepares UserSwitcherController.

UserSwitcherActivity refactor: CL 1/7

- Extracts some logic out of UserSwitcherController and into LegacyUserUiHelper. This logic will later be used by classes in the modern architecture refactor of UserSwitcherActivity. The code is being moved to a common place to avoid code duplication.
- Exposes some new APIs in UserSwitcherController to facilitate user
  selection, expose guest-specific and lock-screen specific
  configuration, and trigger dialog-based user journeys such as adding a
  user

Bug: 243844359
Test: Existing unit tests still pass. Manually verified in CL 7/7.
Change-Id: Iab77acb20781b291289a59389db7b542fdfe0a47
This commit is contained in:
Alejandro Nijamkin
2022-08-31 15:38:30 -07:00
parent 5c2403298f
commit ee3de9c2a4
5 changed files with 241 additions and 80 deletions

View File

@@ -86,6 +86,7 @@ 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.ui.LegacyUserUiHelper;
import com.android.systemui.util.settings.GlobalSettings;
import com.android.systemui.util.settings.SecureSettings;
@@ -100,14 +101,20 @@ import java.util.function.Consumer;
import javax.inject.Inject;
import kotlinx.coroutines.flow.Flow;
import kotlinx.coroutines.flow.MutableStateFlow;
import kotlinx.coroutines.flow.StateFlowKt;
/**
* Keeps a list of all users on the device for user switching.
*/
@SysUISingleton
public class UserSwitcherController implements Dumpable {
public static final float USER_SWITCH_ENABLED_ALPHA = 1.0f;
public static final float USER_SWITCH_DISABLED_ALPHA = 0.38f;
public static final float USER_SWITCH_ENABLED_ALPHA =
LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA;
public static final float USER_SWITCH_DISABLED_ALPHA =
LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA;
private static final String TAG = "UserSwitcherController";
private static final boolean DEBUG = false;
@@ -155,7 +162,8 @@ public class UserSwitcherController implements Dumpable {
private boolean mSimpleUserSwitcher;
// When false, there won't be any visual affordance to add a new user from the keyguard even if
// the user is unlocked
private boolean mAddUsersFromLockScreen;
private final MutableStateFlow<Boolean> mAddUsersFromLockScreen =
StateFlowKt.MutableStateFlow(false);
private boolean mUserSwitcherEnabled;
@VisibleForTesting
boolean mPauseRefreshUsers;
@@ -258,8 +266,11 @@ public class UserSwitcherController implements Dumpable {
@Override
public void onChange(boolean selfChange) {
mSimpleUserSwitcher = shouldUseSimpleUserSwitcher();
mAddUsersFromLockScreen = mGlobalSettings.getIntForUser(
Settings.Global.ADD_USERS_WHEN_LOCKED, 0, UserHandle.USER_SYSTEM) != 0;
mAddUsersFromLockScreen.setValue(
mGlobalSettings.getIntForUser(
Settings.Global.ADD_USERS_WHEN_LOCKED,
0,
UserHandle.USER_SYSTEM) != 0);
mUserSwitcherEnabled = mGlobalSettings.getIntForUser(
Settings.Global.USER_SWITCHER_ENABLED, 0, UserHandle.USER_SYSTEM) != 0;
refreshUsers(UserHandle.USER_NULL);
@@ -323,7 +334,6 @@ public class UserSwitcherController implements Dumpable {
}
mForcePictureLoadForUserId.clear();
final boolean addUsersWhenLocked = mAddUsersFromLockScreen;
mBgExecutor.execute(() -> {
List<UserInfo> infos = mUserManager.getAliveUsers();
if (infos == null) {
@@ -434,7 +444,7 @@ public class UserSwitcherController implements Dumpable {
}
boolean anyoneCanCreateUsers() {
return systemCanCreateUsers() && mAddUsersFromLockScreen;
return systemCanCreateUsers() && mAddUsersFromLockScreen.getValue();
}
boolean canCreateGuest(boolean hasExistingGuest) {
@@ -450,7 +460,7 @@ public class UserSwitcherController implements Dumpable {
}
boolean createIsRestricted() {
return !mAddUsersFromLockScreen;
return !mAddUsersFromLockScreen.getValue();
}
boolean canCreateSupervisedUser() {
@@ -516,17 +526,48 @@ public class UserSwitcherController implements Dumpable {
return null;
}
/**
* Notifies that a user has been selected.
*
* <p>This will trigger the right user journeys to create a guest user, switch users, and/or
* navigate to the correct destination.
*
* <p>If a user with the given ID is not found, this method is a no-op.
*
* @param userId The ID of the user to switch to.
* @param dialogShower An optional {@link DialogShower} in case we need to show dialogs.
*/
public void onUserSelected(int userId, @Nullable DialogShower dialogShower) {
UserRecord userRecord = mUsers.stream()
.filter(x -> x.resolveId() == userId)
.findFirst()
.orElse(null);
if (userRecord == null) {
return;
}
onUserListItemClicked(userRecord, dialogShower);
}
/** Whether it is allowed to add users while the device is locked. */
public Flow<Boolean> getAddUsersFromLockScreen() {
return mAddUsersFromLockScreen;
}
/** Returns {@code true} if the guest user is configured to always be present on the device. */
public boolean isGuestUserAutoCreated() {
return mGuestUserAutoCreated;
}
/** Returns {@code true} if the guest user is currently being reset. */
public boolean isGuestUserResetting() {
return mGuestIsResetting.get();
}
@VisibleForTesting
void onUserListItemClicked(UserRecord record, DialogShower dialogShower) {
if (record.isGuest && record.info == null) {
// No guest user. Create one.
createGuestAsync(guestId -> {
// guestId may be USER_NULL if we haven't reloaded the user list yet.
if (guestId != UserHandle.USER_NULL) {
mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD);
onUserListItemClicked(guestId, record, dialogShower);
}
});
createAndSwitchToGuestUser(dialogShower);
} else if (record.isAddUser) {
showAddUserDialog(dialogShower);
} else if (record.isAddSupervisedUser) {
@@ -604,7 +645,23 @@ public class UserSwitcherController implements Dumpable {
}
}
private void showAddUserDialog(DialogShower dialogShower) {
/**
* Creates and switches to the guest user.
*/
public void createAndSwitchToGuestUser(@Nullable DialogShower dialogShower) {
createGuestAsync(guestId -> {
// guestId may be USER_NULL if we haven't reloaded the user list yet.
if (guestId != UserHandle.USER_NULL) {
mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD);
onUserListItemClicked(guestId, UserRecord.createForGuest(), dialogShower);
}
});
}
/**
* Shows the add user dialog.
*/
public void showAddUserDialog(@Nullable DialogShower dialogShower) {
if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
mAddUserDialog.cancel();
}
@@ -620,7 +677,10 @@ public class UserSwitcherController implements Dumpable {
}
}
private void startSupervisedUserActivity() {
/**
* Starts an activity to add a supervised user to the device.
*/
public void startSupervisedUserActivity() {
final Intent intent = new Intent()
.setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
.setPackage(mCreateSupervisedUserPackage)
@@ -772,7 +832,7 @@ public class UserSwitcherController implements Dumpable {
* Removes guest user and switches to target user. The guest must be the current user and its id
* must be {@code guestUserId}.
*
* <p>If {@code targetUserId} is {@link UserHandle.USER_NULL}, then create a new guest user in
* <p>If {@code targetUserId} is {@link UserHandle#USER_NULL}, then create a new guest user in
* the foreground, and immediately switch to it. This is used for wiping the current guest and
* replacing it with a new one.
*
@@ -782,11 +842,11 @@ public class UserSwitcherController implements Dumpable {
* <p>If device is configured with {@link
* com.android.internal.R.bool.config_guestUserAutoCreated}, then after guest user is removed, a
* new one is created in the background. This has no effect if {@code targetUserId} is {@link
* UserHandle.USER_NULL}.
* UserHandle#USER_NULL}.
*
* @param guestUserId id of the guest user to remove
* @param targetUserId id of the user to switch to after guest is removed. If {@link
* UserHandle.USER_NULL}, then switch immediately to the newly created guest user.
* UserHandle#USER_NULL}, then switch immediately to the newly created guest user.
*/
public void removeGuestUser(@UserIdInt int guestUserId, @UserIdInt int targetUserId) {
UserInfo currentUser = mUserTracker.getUserInfo();
@@ -839,7 +899,7 @@ public class UserSwitcherController implements Dumpable {
* user.
*
* @param guestUserId user id of the guest user to exit
* @param targetUserId user id of the guest user to exit, set to UserHandle.USER_NULL when
* @param targetUserId user id of the guest user to exit, set to UserHandle#USER_NULL when
* target user id is not known
* @param forceRemoveGuestOnExit true: remove guest before switching user,
* false: remove guest only if its ephemeral, else keep guest
@@ -952,7 +1012,7 @@ public class UserSwitcherController implements Dumpable {
* {@link UserManager} to create a new one.
*
* @return The multi-user user ID of the newly created guest user, or
* {@link UserHandle.USER_NULL} if the guest couldn't be created.
* {@link UserHandle#USER_NULL} if the guest couldn't be created.
*/
public @UserIdInt int createGuest() {
UserInfo guest;
@@ -1062,38 +1122,11 @@ public class UserSwitcherController implements Dumpable {
}
public String getName(Context context, UserRecord item) {
if (item.isGuest) {
if (item.isCurrent) {
return context.getString(
com.android.settingslib.R.string.guest_exit_quick_settings_button);
} else {
if (item.info != null) {
return context.getString(com.android.internal.R.string.guest_name);
} else {
if (mController.mGuestUserAutoCreated) {
// If mGuestIsResetting=true, we expect the guest user to be created
// shortly, so display a "Resetting guest..." as an indicator that we
// are busy. Otherwise, if mGuestIsResetting=false, we probably failed
// to create a guest at some point. In this case, always show guest
// nickname instead of "Add guest" to make it seem as though the device
// always has a guest ready for use.
return context.getString(
mController.mGuestIsResetting.get()
? com.android.settingslib.R.string.guest_resetting
: com.android.internal.R.string.guest_name);
} else {
// we always show "guest" as string, instead of "add guest"
return context.getString(com.android.internal.R.string.guest_name);
}
}
}
} else if (item.isAddUser) {
return context.getString(com.android.settingslib.R.string.user_add_user);
} else if (item.isAddSupervisedUser) {
return context.getString(R.string.add_user_supervised);
} else {
return item.info.name;
}
return LegacyUserUiHelper.getUserRecordName(
context,
item,
mController.isGuestUserAutoCreated(),
mController.isGuestUserResetting());
}
protected static ColorFilter getDisabledUserAvatarColorFilter() {
@@ -1103,17 +1136,8 @@ public class UserSwitcherController implements Dumpable {
}
protected static Drawable getIconDrawable(Context context, UserRecord item) {
int iconRes;
if (item.isAddUser) {
iconRes = R.drawable.ic_add;
} else if (item.isGuest) {
iconRes = R.drawable.ic_account_circle;
} else if (item.isAddSupervisedUser) {
iconRes = R.drawable.ic_add_supervised_user;
} else {
iconRes = R.drawable.ic_avatar_user;
}
int iconRes = LegacyUserUiHelper.getUserSwitcherActionIconResourceId(
item.isAddUser, item.isGuest, item.isAddSupervisedUser);
return context.getDrawable(iconRes);
}

View File

@@ -26,28 +26,31 @@ import android.os.UserHandle
data class UserRecord(
/** Relevant user information. If `null`, this record is not a user but an option item. */
@JvmField
val info: UserInfo?,
val info: UserInfo? = null,
/** An image representing the user. */
@JvmField
val picture: Bitmap?,
val picture: Bitmap? = null,
/** Whether this record represents an option to switch to a guest user. */
@JvmField
val isGuest: Boolean,
val isGuest: Boolean = false,
/** Whether this record represents the currently-selected user. */
@JvmField
val isCurrent: Boolean,
val isCurrent: Boolean = false,
/** Whether this record represents an option to add another user to the device. */
@JvmField
val isAddUser: Boolean,
/** If true, the record is only visible to the owner and only when unlocked. */
val isAddUser: Boolean = false,
/**
* If true, the record is only available if unlocked or if the user has granted permission to
* access this user action whilst on the device is locked.
*/
@JvmField
val isRestricted: Boolean,
val isRestricted: Boolean = false,
/** Whether it is possible to switch to this user. */
@JvmField
val isSwitchToEnabled: Boolean,
val isSwitchToEnabled: Boolean = false,
/** Whether this record represents an option to add another supervised user to the device. */
@JvmField
val isAddSupervisedUser: Boolean,
val isAddSupervisedUser: Boolean = false,
) {
/**
* Returns a new instance of [UserRecord] with its [isCurrent] set to the given value.
@@ -67,4 +70,11 @@ data class UserRecord(
info.id
}
}
companion object {
@JvmStatic
fun createForGuest(): UserRecord {
return UserRecord(isGuest = true)
}
}
}

View File

@@ -0,0 +1,130 @@
/*
* 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.ui
import android.content.Context
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.android.systemui.R
import com.android.systemui.user.data.source.UserRecord
import kotlin.math.ceil
/**
* Defines utility functions for helping with legacy UI 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 LegacyUserUiHelper {
/** Returns the maximum number of columns for user items in the user switcher. */
fun getMaxUserSwitcherItemColumns(userCount: Int): Int {
// TODO(b/243844097): remove this once we remove the old user switcher implementation.
return if (userCount < 5) {
4
} else {
ceil(userCount / 2.0).toInt()
}
}
@JvmStatic
@DrawableRes
fun getUserSwitcherActionIconResourceId(
isAddUser: Boolean,
isGuest: Boolean,
isAddSupervisedUser: Boolean,
): Int {
return if (isAddUser) {
R.drawable.ic_add
} else if (isGuest) {
R.drawable.ic_account_circle
} else if (isAddSupervisedUser) {
R.drawable.ic_add_supervised_user
} else {
R.drawable.ic_avatar_user
}
}
@JvmStatic
fun getUserRecordName(
context: Context,
record: UserRecord,
isGuestUserAutoCreated: Boolean,
isGuestUserResetting: Boolean,
): String {
val resourceId: Int? = getGuestUserRecordNameResourceId(record)
return when {
resourceId != null -> context.getString(resourceId)
record.info != null -> record.info.name
else ->
context.getString(
getUserSwitcherActionTextResourceId(
isGuest = record.isGuest,
isGuestUserAutoCreated = isGuestUserAutoCreated,
isGuestUserResetting = isGuestUserResetting,
isAddUser = record.isAddUser,
isAddSupervisedUser = record.isAddSupervisedUser,
)
)
}
}
/**
* Returns the resource ID for a string for the name of the guest user.
*
* If the given record is not the guest user, returns `null`.
*/
@StringRes
fun getGuestUserRecordNameResourceId(record: UserRecord): Int? {
return when {
record.isGuest && record.isCurrent ->
com.android.settingslib.R.string.guest_exit_quick_settings_button
record.isGuest && record.info != null -> com.android.internal.R.string.guest_name
else -> null
}
}
@JvmStatic
@StringRes
fun getUserSwitcherActionTextResourceId(
isGuest: Boolean,
isGuestUserAutoCreated: Boolean,
isGuestUserResetting: Boolean,
isAddUser: Boolean,
isAddSupervisedUser: Boolean,
): Int {
check(isGuest || isAddUser || isAddSupervisedUser)
return when {
isGuest && isGuestUserAutoCreated && isGuestUserResetting ->
com.android.settingslib.R.string.guest_resetting
isGuest && isGuestUserAutoCreated -> com.android.internal.R.string.guest_name
isGuest -> com.android.internal.R.string.guest_name
isAddUser -> com.android.settingslib.R.string.user_add_user
isAddSupervisedUser -> R.string.add_user_supervised
else -> error("This should never happen!")
}
}
/** Alpha value to apply to a user view in the user switcher when it's selectable. */
const val USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA = 1.0f
/** Alpha value to apply to a user view in the user switcher when it's not selectable. */
const val USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA = 0.38f
}

View File

@@ -53,7 +53,6 @@ class UserDetailViewAdapterTest : SysuiTestCase() {
@Mock private lateinit var mUserDetailItemView: UserDetailItemView
@Mock private lateinit var mOtherView: View
@Mock private lateinit var mInflatedUserDetailItemView: UserDetailItemView
@Mock private lateinit var mUserInfo: UserInfo
@Mock private lateinit var mLayoutInflater: LayoutInflater
private var falsingManagerFake: FalsingManagerFake = FalsingManagerFake()
private lateinit var adapter: UserDetailView.Adapter
@@ -142,7 +141,7 @@ class UserDetailViewAdapterTest : SysuiTestCase() {
private fun createUserRecord(current: Boolean, guest: Boolean) =
UserRecord(
mUserInfo,
UserInfo(0 /* id */, "name", 0 /* flags */),
mPicture,
guest,
current,

View File

@@ -57,8 +57,6 @@ class KeyguardUserSwitcherAdapterTest : SysuiTestCase() {
@Mock
private lateinit var inflatedUserDetailItemView: KeyguardUserDetailItemView
@Mock
private lateinit var userInfo: UserInfo
@Mock
private lateinit var layoutInflater: LayoutInflater
@Mock
private lateinit var keyguardUserSwitcherController: KeyguardUserSwitcherController
@@ -188,7 +186,7 @@ class KeyguardUserSwitcherAdapterTest : SysuiTestCase() {
private fun createUserRecord(isCurrentUser: Boolean, isGuestUser: Boolean) =
UserRecord(
userInfo,
UserInfo(0 /* id */, "name", 0 /* flags */),
picture,
isGuestUser,
isCurrentUser,