Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ee3de9c2 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

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
parent 5c240329
Loading
Loading
Loading
Loading
+90 −66
Original line number Diff line number Diff line
@@ -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);
        }

+19 −9
Original line number Diff line number Diff line
@@ -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)
        }
    }
}
+130 −0
Original line number Diff line number Diff line
/*
 * 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
}
+1 −2
Original line number Diff line number Diff line
@@ -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,
+1 −3
Original line number Diff line number Diff line
@@ -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,