diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java index a5bcb538cd8c1..62bda2c76e9fe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -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 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 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. + * + *

This will trigger the right user journeys to create a guest user, switch users, and/or + * navigate to the correct destination. + * + *

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 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}. * - *

If {@code targetUserId} is {@link UserHandle.USER_NULL}, then create a new guest user in + *

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 { *

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); } diff --git a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt index 6ab6d7d7891ad..67999f381a0ed 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt @@ -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) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt new file mode 100644 index 0000000000000..18369d9a71d22 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt @@ -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 +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt index 5db3b9caa007b..da52a9b1a3c29 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt @@ -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, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherAdapterTest.kt index c3805ad365334..8290dab19bdfa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherAdapterTest.kt @@ -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,