From 948dee1b6feba7a79bda5e4a025b4321adf910d9 Mon Sep 17 00:00:00 2001 From: Kedar Chitnis Date: Tue, 28 Dec 2021 13:30:04 +0000 Subject: [PATCH] Guest mode UX flow updates to user settings - Add option in user settings to enable/disable ephemeral mode for guest user - Update user settings to show exit guest and reset guest preferences - Update user settings to show guest related preferences grouped together Bug: 214031645, 175795666 Screenshots: go/ephemeral-guest-b-214031645-ux Test: Manual test on sunfish, atest SystemUITests, atest SettingsRoboTests Change-Id: I7cf205883f8250178ac24c67c74b9142ceb6c1f5 --- color-check-baseline.xml | 102 ++-- res/drawable/ic_account_circle.xml | 25 + res/drawable/ic_guest_exit.xml | 25 + res/drawable/ic_guest_reset.xml | 25 + res/values/strings.xml | 16 + res/xml/user_settings.xml | 55 ++- ...RemoveGuestOnExitPreferenceController.java | 246 +++++++++ .../settings/users/UserCapabilities.java | 2 + .../settings/users/UserDetailsSettings.java | 20 + .../android/settings/users/UserDialogs.java | 2 +- .../android/settings/users/UserSettings.java | 465 +++++++++++++++--- .../testutils/shadow/ShadowUserManager.java | 28 ++ .../settings/users/UserSettingsTest.java | 22 +- 13 files changed, 915 insertions(+), 118 deletions(-) create mode 100644 res/drawable/ic_account_circle.xml create mode 100644 res/drawable/ic_guest_exit.xml create mode 100644 res/drawable/ic_guest_reset.xml create mode 100644 src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java diff --git a/color-check-baseline.xml b/color-check-baseline.xml index 96f0210b36d..99b524efd60 100644 --- a/color-check-baseline.xml +++ b/color-check-baseline.xml @@ -1,18 +1,6 @@ - - - - + + + + + + + + + + + + - - - - @@ -4553,7 +4573,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -4569,7 +4589,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -4585,10 +4605,26 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + + + + diff --git a/res/drawable/ic_account_circle.xml b/res/drawable/ic_account_circle.xml new file mode 100644 index 00000000000..2c4d5a6a188 --- /dev/null +++ b/res/drawable/ic_account_circle.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_guest_exit.xml b/res/drawable/ic_guest_exit.xml new file mode 100644 index 00000000000..2f7ca09e4d2 --- /dev/null +++ b/res/drawable/ic_guest_exit.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_guest_reset.xml b/res/drawable/ic_guest_reset.xml new file mode 100644 index 00000000000..243a322acc1 --- /dev/null +++ b/res/drawable/ic_guest_reset.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 26ff034d6ca..4b2843ee39d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7716,6 +7716,22 @@ All apps and data in this session will be deleted. Remove + + Guest (You) + + Users + + Other users + + Delete guest activity + + Delete all guest apps and data + when exiting guest mode + + Delete guest activity? + + Apps and data from this guest session will be + deleted now, and all future guest activity will be deleted when you exit guest mode Turn on phone calls diff --git a/res/xml/user_settings.xml b/res/xml/user_settings.xml index ab54989128b..c73849b0eb4 100644 --- a/res/xml/user_settings.xml +++ b/res/xml/user_settings.xml @@ -26,27 +26,64 @@ settings:controller="com.android.settings.users.MultiUserTopIntroPreferenceController"/> - + android:key="guest_category" + android:title="@string/guest_category_title" + android:order="2" + settings:searchable="false"/> - + + + + + + + + + + + + + resetGuest()); + case DIALOG_CONFIRM_RESET_GUEST_AND_SWITCH_USER: + return UserDialogs.createResetGuestDialog(getActivity(), + (dialog, which) -> switchUser()); } throw new IllegalArgumentException("Unsupported dialogId " + dialogId); } @@ -356,6 +366,16 @@ public class UserDetailsSettings extends SettingsPreferenceFragment if (mUserInfo.isGuest()) { mMetricsFeatureProvider.action(getActivity(), SettingsEnums.ACTION_SWITCH_TO_GUEST); } + if (mUserCaps.mIsGuest && mUserCaps.mIsEphemeral) { + int guestUserId = UserHandle.myUserId(); + // Using markGuestForDeletion allows us to create a new guest before this one is + // fully removed. + boolean marked = mUserManager.markGuestForDeletion(guestUserId); + if (!marked) { + Log.w(TAG, "Couldn't mark the guest for deletion for user " + guestUserId); + return; + } + } ActivityManager.getService().switchUser(mUserInfo.id); } catch (RemoteException re) { Log.e(TAG, "Error while switching to other user."); diff --git a/src/com/android/settings/users/UserDialogs.java b/src/com/android/settings/users/UserDialogs.java index 4dcec4d2b56..67b7572ee4b 100644 --- a/src/com/android/settings/users/UserDialogs.java +++ b/src/com/android/settings/users/UserDialogs.java @@ -187,7 +187,7 @@ public final class UserDialogs { DialogInterface.OnClickListener onConfirmListener) { return new AlertDialog.Builder(context) .setTitle(com.android.settingslib.R.string.guest_reset_guest_dialog_title) - .setMessage(R.string.user_exit_guest_confirm_message) + .setMessage(com.android.settingslib.R.string.guest_exit_dialog_message) .setPositiveButton( com.android.settingslib.R.string.guest_reset_guest_confirm_button, onConfirmListener) diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java index 0f2eb66bcea..0d02748569b 100644 --- a/src/com/android/settings/users/UserSettings.java +++ b/src/com/android/settings/users/UserSettings.java @@ -47,13 +47,16 @@ import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; import android.provider.ContactsContract; +import android.provider.Settings; import android.text.TextUtils; +import android.util.FeatureFlagUtils; import android.util.Log; import android.util.SparseArray; import android.view.Gravity; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.WindowManagerGlobal; import android.widget.SimpleAdapter; import android.widget.Toast; @@ -95,6 +98,7 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; /** * Screen that manages the list of users on the device. @@ -123,6 +127,14 @@ public class UserSettings extends SettingsPreferenceFragment private static final String KEY_ADD_USER_WHEN_LOCKED = "user_settings_add_users_when_locked"; private static final String KEY_MULTIUSER_TOP_INTRO = "multiuser_top_intro"; private static final String KEY_TIMEOUT_TO_USER_ZERO = "timeout_to_user_zero_preference"; + private static final String KEY_GUEST_CATEGORY = "guest_category"; + private static final String KEY_GUEST_RESET = "guest_reset"; + private static final String KEY_GUEST_EXIT = "guest_exit"; + private static final String KEY_GUEST_INFO = "guest_info"; + private static final String KEY_REMOVE_GUEST_ON_EXIT = "remove_guest_on_exit"; + private static final String KEY_GUEST_USER_CATEGORY = "guest_user_category"; + + private static final String SETTING_GUEST_HAS_LOGGED_IN = "systemui.guest_has_logged_in"; private static final int MENU_REMOVE_USER = Menu.FIRST; @@ -134,14 +146,18 @@ public class UserSettings extends SettingsPreferenceFragment private static final int DIALOG_USER_CANNOT_MANAGE = 5; private static final int DIALOG_CHOOSE_USER_TYPE = 6; private static final int DIALOG_NEED_LOCKSCREEN = 7; - private static final int DIALOG_CONFIRM_EXIT_GUEST = 8; + private static final int DIALOG_CONFIRM_REMOVE_GUEST = 8; private static final int DIALOG_USER_PROFILE_EDITOR = 9; private static final int DIALOG_USER_PROFILE_EDITOR_ADD_USER = 10; private static final int DIALOG_USER_PROFILE_EDITOR_ADD_RESTRICTED_PROFILE = 11; - private static final int DIALOG_CONFIRM_RESET_GUEST = 12; + private static final int DIALOG_CONFIRM_REMOVE_GUEST_WITH_AUTO_CREATE = 12; + private static final int DIALOG_CONFIRM_RESET_AND_RESTART_GUEST = 13; + private static final int DIALOG_CONFIRM_EXIT_GUEST_EPHEMERAL = 14; + private static final int DIALOG_CONFIRM_EXIT_GUEST_NON_EPHEMERAL = 15; private static final int MESSAGE_UPDATE_LIST = 1; private static final int MESSAGE_USER_CREATED = 2; + static final int MESSAGE_REMOVE_GUEST_ON_EXIT_CONTROLLER_GUEST_REMOVED = 3; private static final int USER_TYPE_USER = 1; private static final int USER_TYPE_RESTRICTED_PROFILE = 2; @@ -165,6 +181,16 @@ public class UserSettings extends SettingsPreferenceFragment @VisibleForTesting PreferenceGroup mUserListCategory; @VisibleForTesting + PreferenceGroup mGuestUserCategory; + @VisibleForTesting + PreferenceGroup mGuestCategory; + @VisibleForTesting + Preference mGuestResetPreference; + @VisibleForTesting + Preference mGuestExitPreference; + @VisibleForTesting + Preference mGuestInfoPreference; + @VisibleForTesting UserPreference mMePreference; @VisibleForTesting RestrictedPreference mAddGuest; @@ -189,6 +215,7 @@ public class UserSettings extends SettingsPreferenceFragment private EditUserInfoController mEditUserInfoController = new EditUserInfoController(Utils.FILE_PROVIDER_AUTHORITY); private AddUserWhenLockedPreferenceController mAddUserWhenLockedPreferenceController; + private RemoveGuestOnExitPreferenceController mRemoveGuestOnExitPreferenceController; private MultiUserTopIntroPreferenceController mMultiUserTopIntroPreferenceController; private TimeoutToUserZeroPreferenceController mTimeoutToUserZeroPreferenceController; private UserCreatingDialog mUserCreatingDialog; @@ -213,6 +240,12 @@ public class UserSettings extends SettingsPreferenceFragment case MESSAGE_USER_CREATED: onUserCreated(msg.arg1); break; + case MESSAGE_REMOVE_GUEST_ON_EXIT_CONTROLLER_GUEST_REMOVED: + updateUserList(); + if (mGuestUserAutoCreated) { + scheduleGuestCreation(); + } + break; } } }; @@ -245,7 +278,11 @@ public class UserSettings extends SettingsPreferenceFragment final SettingsActivity activity = (SettingsActivity) getActivity(); final SettingsMainSwitchBar switchBar = activity.getSwitchBar(); switchBar.setTitle(getContext().getString(R.string.multiple_users_main_switch_title)); - switchBar.show(); + if (mUserCaps.mIsAdmin) { + switchBar.show(); + } else { + switchBar.hide(); + } mSwitchBarController = new MultiUserSwitchBarController(activity, new MainSwitchBarController(switchBar), this /* listener */); getSettingsLifecycle().addObserver(mSwitchBarController); @@ -267,6 +304,9 @@ public class UserSettings extends SettingsPreferenceFragment mAddUserWhenLockedPreferenceController = new AddUserWhenLockedPreferenceController( activity, KEY_ADD_USER_WHEN_LOCKED); + mRemoveGuestOnExitPreferenceController = new RemoveGuestOnExitPreferenceController( + activity, KEY_REMOVE_GUEST_ON_EXIT, this, mHandler); + mMultiUserTopIntroPreferenceController = new MultiUserTopIntroPreferenceController(activity, KEY_MULTIUSER_TOP_INTRO); @@ -275,12 +315,16 @@ public class UserSettings extends SettingsPreferenceFragment final PreferenceScreen screen = getPreferenceScreen(); mAddUserWhenLockedPreferenceController.displayPreference(screen); + mRemoveGuestOnExitPreferenceController.displayPreference(screen); mMultiUserTopIntroPreferenceController.displayPreference(screen); mTimeoutToUserZeroPreferenceController.displayPreference(screen); screen.findPreference(mAddUserWhenLockedPreferenceController.getPreferenceKey()) .setOnPreferenceChangeListener(mAddUserWhenLockedPreferenceController); + screen.findPreference(mRemoveGuestOnExitPreferenceController.getPreferenceKey()) + .setOnPreferenceChangeListener(mRemoveGuestOnExitPreferenceController); + if (icicle != null) { if (icicle.containsKey(SAVE_REMOVING_USER)) { mRemovingUserId = icicle.getInt(SAVE_REMOVING_USER); @@ -304,6 +348,18 @@ public class UserSettings extends SettingsPreferenceFragment mMePreference.setSummary(R.string.user_admin); } + mGuestCategory = findPreference(KEY_GUEST_CATEGORY); + + mGuestResetPreference = findPreference(KEY_GUEST_RESET); + mGuestResetPreference.setOnPreferenceClickListener(this); + + mGuestExitPreference = findPreference(KEY_GUEST_EXIT); + mGuestExitPreference.setOnPreferenceClickListener(this); + + mGuestInfoPreference = findPreference(KEY_GUEST_INFO); + + mGuestUserCategory = findPreference(KEY_GUEST_USER_CATEGORY); + mAddGuest = findPreference(KEY_ADD_GUEST); mAddGuest.setOnPreferenceClickListener(this); @@ -339,7 +395,8 @@ public class UserSettings extends SettingsPreferenceFragment mAddUserWhenLockedPreferenceController.getPreferenceKey())); mTimeoutToUserZeroPreferenceController.updateState(screen.findPreference( mTimeoutToUserZeroPreferenceController.getPreferenceKey())); - + mRemoveGuestOnExitPreferenceController.updateState(screen.findPreference( + mRemoveGuestOnExitPreferenceController.getPreferenceKey())); if (mShouldUpdateUserList) { updateUI(); } @@ -418,6 +475,11 @@ public class UserSettings extends SettingsPreferenceFragment updateUserList(); } + private boolean isEnableGuestModeUxChanges() { + return FeatureFlagUtils.isEnabled(getContext(), + FeatureFlagUtils.SETTINGS_GUEST_MODE_UX_CHANGES); + } + /** * Loads profile information for the current user. */ @@ -702,7 +764,7 @@ public class UserSettings extends SettingsPreferenceFragment .create(); return dlg; } - case DIALOG_CONFIRM_EXIT_GUEST: { + case DIALOG_CONFIRM_REMOVE_GUEST: { Dialog dlg = new AlertDialog.Builder(context) .setTitle(R.string.user_exit_guest_confirm_title) .setMessage(R.string.user_exit_guest_confirm_message) @@ -710,13 +772,56 @@ public class UserSettings extends SettingsPreferenceFragment new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - exitGuest(); + clearAndExitGuest(); } }) .setNegativeButton(android.R.string.cancel, null) .create(); return dlg; } + case DIALOG_CONFIRM_EXIT_GUEST_EPHEMERAL: { + Dialog dlg = new AlertDialog.Builder(context) + .setTitle(com.android.settingslib.R.string.guest_exit_dialog_title) + .setMessage(com.android.settingslib.R.string.guest_exit_dialog_message) + .setPositiveButton( + com.android.settingslib.R.string.guest_exit_dialog_button, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + clearAndExitGuest(); + } + }) + .setNeutralButton(android.R.string.cancel, null) + .create(); + return dlg; + } + case DIALOG_CONFIRM_EXIT_GUEST_NON_EPHEMERAL: { + Dialog dlg = new AlertDialog.Builder(context) + .setTitle( + com.android.settingslib.R.string.guest_exit_dialog_title_non_ephemeral) + .setMessage( + com.android.settingslib + .R.string.guest_exit_dialog_message_non_ephemeral) + .setPositiveButton( + com.android.settingslib.R.string.guest_exit_save_data_button, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + exitGuest(); + } + }) + .setNegativeButton( + com.android.settingslib.R.string.guest_exit_clear_data_button, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + clearAndExitGuest(); + } + }) + .setNeutralButton(android.R.string.cancel, null) + .create(); + return dlg; + } case DIALOG_USER_PROFILE_EDITOR: { return buildEditCurrentUserDialog(); } @@ -736,9 +841,27 @@ public class UserSettings extends SettingsPreferenceFragment } return buildAddUserDialog(USER_TYPE_RESTRICTED_PROFILE); } - case DIALOG_CONFIRM_RESET_GUEST: { + case DIALOG_CONFIRM_REMOVE_GUEST_WITH_AUTO_CREATE: { return UserDialogs.createResetGuestDialog(getActivity(), - (dialog, which) -> resetGuest()); + (dialog, which) -> clearAndExitGuest()); + } + case DIALOG_CONFIRM_RESET_AND_RESTART_GUEST: { + Dialog dlg = new AlertDialog.Builder(context) + .setTitle( + com.android.settingslib.R.string.guest_reset_and_restart_dialog_title) + .setMessage( + com.android.settingslib.R.string.guest_reset_and_restart_dialog_message) + .setPositiveButton( + com.android.settingslib.R.string.guest_reset_guest_confirm_button, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + resetAndRestartGuest(); + } + }) + .setNeutralButton(android.R.string.cancel, null) + .create(); + return dlg; } default: return null; @@ -816,8 +939,11 @@ public class UserSettings extends SettingsPreferenceFragment return SettingsEnums.DIALOG_USER_CHOOSE_TYPE; case DIALOG_NEED_LOCKSCREEN: return SettingsEnums.DIALOG_USER_NEED_LOCKSCREEN; - case DIALOG_CONFIRM_EXIT_GUEST: - case DIALOG_CONFIRM_RESET_GUEST: + case DIALOG_CONFIRM_REMOVE_GUEST: + case DIALOG_CONFIRM_REMOVE_GUEST_WITH_AUTO_CREATE: + case DIALOG_CONFIRM_EXIT_GUEST_EPHEMERAL: + case DIALOG_CONFIRM_EXIT_GUEST_NON_EPHEMERAL: + case DIALOG_CONFIRM_RESET_AND_RESTART_GUEST: return SettingsEnums.DIALOG_USER_CONFIRM_EXIT_GUEST; case DIALOG_USER_PROFILE_EDITOR: case DIALOG_USER_PROFILE_EDITOR_ADD_USER: @@ -859,6 +985,18 @@ public class UserSettings extends SettingsPreferenceFragment } } + private void switchToUserId(int userId) { + if (!canSwitchUserNow()) { + Log.w(TAG, "Cannot switch current user when switching is disabled"); + return; + } + try { + ActivityManager.getService().switchUser(userId); + } catch (RemoteException re) { + Log.e(TAG, "Unable to switch user"); + } + } + private void addUserNow(final int userType) { Trace.beginAsyncSection("UserSettings.addUserNow", 0); synchronized (mUserLock) { @@ -940,36 +1078,91 @@ public class UserSettings extends SettingsPreferenceFragment * Erase the current user (guest) and switch to another user. */ @VisibleForTesting - void exitGuest() { + void clearAndExitGuest() { // Just to be safe if (!isCurrentUserGuest()) { return; } mMetricsFeatureProvider.action(getActivity(), SettingsEnums.ACTION_USER_GUEST_EXIT_CONFIRMED); - removeThisUser(); - } - /** - * Erase the current user (assuming it is a guest user), and create a new one in the background - */ - @VisibleForTesting - void resetGuest() { - // Just to be safe - if (!isCurrentUserGuest()) { - return; - } int guestUserId = UserHandle.myUserId(); // Using markGuestForDeletion allows us to create a new guest before this one is - // fully removed. This could happen if someone calls scheduleGuestCreation() - // immediately after calling this method. + // fully removed. boolean marked = mUserManager.markGuestForDeletion(guestUserId); if (!marked) { Log.w(TAG, "Couldn't mark the guest for deletion for user " + guestUserId); return; } - exitGuest(); - scheduleGuestCreation(); + + removeThisUser(); + if (mGuestUserAutoCreated) { + scheduleGuestCreation(); + } + } + + /** + * Switch to another user. + */ + private void exitGuest() { + // Just to be safe + if (!isCurrentUserGuest()) { + return; + } + mMetricsFeatureProvider.action(getActivity(), + SettingsEnums.ACTION_USER_GUEST_EXIT_CONFIRMED); + switchToUserId(UserHandle.USER_SYSTEM); + } + + private int createGuest() { + UserInfo guest; + Context context = getPrefContext(); + try { + guest = mUserManager.createGuest(context); + } catch (UserManager.UserOperationException e) { + Log.e(TAG, "Couldn't create guest user", e); + return UserHandle.USER_NULL; + } + if (guest == null) { + Log.e(TAG, "Couldn't create guest, most likely because there already exists one"); + return UserHandle.USER_NULL; + } + return guest.id; + } + + /** + * Remove current guest and start a new guest session + */ + private void resetAndRestartGuest() { + // Just to be safe + if (!isCurrentUserGuest()) { + return; + } + int oldGuestUserId = UserHandle.myUserId(); + // Using markGuestForDeletion allows us to create a new guest before this one is + // fully removed. + boolean marked = mUserManager.markGuestForDeletion(oldGuestUserId); + if (!marked) { + Log.w(TAG, "Couldn't mark the guest for deletion for user " + oldGuestUserId); + return; + } + + try { + // Create a new guest in the foreground, and then immediately switch to it + int newGuestUserId = createGuest(); + if (newGuestUserId == UserHandle.USER_NULL) { + Log.e(TAG, "Could not create new guest, switching back to system user"); + switchToUserId(UserHandle.USER_SYSTEM); + mUserManager.removeUser(oldGuestUserId); + WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null); + return; + } + switchToUserId(newGuestUserId); + mUserManager.removeUser(oldGuestUserId); + } catch (RemoteException e) { + Log.e(TAG, "Couldn't remove guest because ActivityManager or WindowManager is dead"); + return; + } } /** @@ -1004,18 +1197,28 @@ public class UserSettings extends SettingsPreferenceFragment if (context == null) { return; } - final List users = mUserManager.getAliveUsers(); - + final List users = mUserManager.getAliveUsers() + // Only users that can be switched to should show up here. + // e.g. Managed profiles appear under Accounts Settings instead + .stream().filter(UserInfo::supportsSwitchToByUser) + .collect(Collectors.toList()); final ArrayList missingIcons = new ArrayList<>(); final ArrayList userPreferences = new ArrayList<>(); - userPreferences.add(mMePreference); + + // mMePreference shows a icon for current user. However when current user is a guest, we + // don't show the guest user icon, instead we show two preferences for guest user to + // exit and reset itself. Hence we don't add mMePreference, i.e. guest user to the + // list of users visible in the UI. + if (!mUserCaps.mIsGuest) { + userPreferences.add(mMePreference); + } boolean canOpenUserDetails = mUserCaps.mIsAdmin || (canSwitchUserNow() && !mUserCaps.mDisallowSwitchUser); for (UserInfo user : users) { - if (!user.supportsSwitchToByUser()) { - // Only users that can be switched to should show up here. - // e.g. Managed profiles appear under Accounts Settings instead + if (user.isGuest()) { + // Guest user is added to guest category via updateGuestCategory + // and not to user list so skip guest here continue; } UserPreference pref; @@ -1028,21 +1231,9 @@ public class UserSettings extends SettingsPreferenceFragment pref.setOnPreferenceClickListener(this); pref.setEnabled(canOpenUserDetails); pref.setSelectable(true); - - if (user.isGuest()) { - pref.setIcon(getEncircledDefaultIcon()); - pref.setKey(KEY_USER_GUEST); - if (mUserCaps.mDisallowSwitchUser) { - pref.setDisabledByAdmin( - RestrictedLockUtilsInternal.getDeviceOwner(context)); - } else { - pref.setDisabledByAdmin(null); - } - } else { - pref.setKey("id=" + user.id); - if (user.isAdmin()) { - pref.setSummary(R.string.user_admin); - } + pref.setKey("id=" + user.id); + if (user.isAdmin()) { + pref.setSummary(R.string.user_admin); } } if (pref == null) { @@ -1097,12 +1288,13 @@ public class UserSettings extends SettingsPreferenceFragment loadIconsAsync(missingIcons); } - // If profiles are supported, mUserListCategory will have a special title + // If restricted profiles are supported, mUserListCategory will have a special title if (mUserCaps.mCanAddRestrictedProfile) { mUserListCategory.setTitle(R.string.user_list_title); + } else if (isCurrentUserGuest()) { + mUserListCategory.setTitle(R.string.other_user_category_title); } else { - mUserListCategory.setTitle(null); - mUserListCategory.setLayoutResource(R.layout.empty_view); + mUserListCategory.setTitle(R.string.user_category_title); } // Remove everything from mUserListCategory and add new users. @@ -1117,8 +1309,8 @@ public class UserSettings extends SettingsPreferenceFragment mMultiUserTopIntroPreferenceController.getPreferenceKey()); mMultiUserTopIntroPreferenceController.updateState(multiUserTopIntroPrefence); mUserListCategory.setVisible(mUserCaps.mUserSwitcherEnabled); - - updateAddGuest(context, users.stream().anyMatch(UserInfo::isGuest)); + updateGuestPreferences(); + updateGuestCategory(context, users); updateAddUser(context); updateAddSupervisedUser(context); @@ -1147,14 +1339,127 @@ public class UserSettings extends SettingsPreferenceFragment return mUserManager.getUserSwitchability() == UserManager.SWITCHABILITY_STATUS_OK; } - private void updateAddGuest(Context context, boolean isGuestAlreadyCreated) { + private void updateGuestPreferences() { + // reset guest and exit guest preferences are shown only in guest mode. + // For all other users these are not visible. + mGuestCategory.setVisible(false); + mGuestResetPreference.setVisible(false); + mGuestExitPreference.setVisible(false); + mGuestInfoPreference.setVisible(false); + if (!isCurrentUserGuest()) { + return; + } + mGuestCategory.setVisible(true); + mGuestExitPreference.setVisible(true); + if (isEnableGuestModeUxChanges()) { + mGuestResetPreference.setVisible(true); + mGuestInfoPreference.setVisible(true); + + boolean isGuestFirstLogin = Settings.Secure.getIntForUser( + getContext().getContentResolver(), + SETTING_GUEST_HAS_LOGGED_IN, + 0, + UserHandle.myUserId()) <= 1; + String guestInfoText; + if (mUserCaps.mIsEphemeral) { + guestInfoText = getContext().getString( + R.string.guest_notification_ephemeral); + } else if (isGuestFirstLogin) { + guestInfoText = getContext().getString( + R.string.guest_notification_non_ephemeral); + } else { + guestInfoText = getContext().getString( + R.string.guest_notification_non_ephemeral_non_first_login); + } + mGuestInfoPreference.setSummary(guestInfoText); + } else { + mGuestExitPreference.setIcon(getEncircledDefaultIcon()); + mGuestExitPreference.setTitle( + mGuestUserAutoCreated + ? com.android.settingslib.R.string.guest_reset_guest + : R.string.user_exit_guest_title); + } + } + + private void updateGuestCategory(Context context, List users) { + // show guest category title and related guest preferences + // - if guest is created, then show guest user preference + // - if guest is not created and its allowed to create guest, + // then show "add guest" preference + // - if allowed, show "reset guest on exit" preference + // - if there is nothing to show, then make the guest category as not visible + // - guest category is not visible for guest user. + UserPreference pref = null; + boolean isGuestAlreadyCreated = false; + boolean canOpenUserDetails = + mUserCaps.mIsAdmin || (canSwitchUserNow() && !mUserCaps.mDisallowSwitchUser); + + mGuestUserCategory.removeAll(); + mGuestUserCategory.setVisible(false); + for (UserInfo user : users) { + if (!user.isGuest() || !user.isEnabled()) { + // Only look at enabled, guest users + continue; + } + final Context prefContext = getPrefContext(); + pref = new UserPreference(prefContext, null, user.id); + pref.setTitle(user.name); + pref.setOnPreferenceClickListener(this); + pref.setEnabled(canOpenUserDetails); + pref.setSelectable(true); + if (isEnableGuestModeUxChanges()) { + pref.setIcon(getContext().getDrawable(R.drawable.ic_account_circle)); + } else { + pref.setIcon(getEncircledDefaultIcon()); + } + pref.setKey(KEY_USER_GUEST); + pref.setOrder(Preference.DEFAULT_ORDER); + if (mUserCaps.mDisallowSwitchUser) { + pref.setDisabledByAdmin( + RestrictedLockUtilsInternal.getDeviceOwner(context)); + } else { + pref.setDisabledByAdmin(null); + } + if (mUserCaps.mUserSwitcherEnabled) { + mGuestUserCategory.addPreference(pref); + // guest user preference is shown hence also make guest category visible + mGuestUserCategory.setVisible(true); + } + isGuestAlreadyCreated = true; + } + boolean isVisible = updateAddGuestPreference(context, isGuestAlreadyCreated); + if (isVisible) { + // "add guest" preference is shown hence also make guest category visible + mGuestUserCategory.setVisible(true); + } + final Preference removeGuestOnExit = getPreferenceScreen().findPreference( + mRemoveGuestOnExitPreferenceController.getPreferenceKey()); + mRemoveGuestOnExitPreferenceController.updateState(removeGuestOnExit); + if (mRemoveGuestOnExitPreferenceController.isAvailable()) { + // "reset guest on exit" preference is shown hence also make guest category visible + mGuestUserCategory.setVisible(true); + } + if (mUserCaps.mIsGuest) { + // guest category is not visible for guest user. + mGuestUserCategory.setVisible(false); + } + } + + private boolean updateAddGuestPreference(Context context, boolean isGuestAlreadyCreated) { + boolean isVisible = false; if (!isGuestAlreadyCreated && mUserCaps.mCanAddGuest && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_GUEST) && WizardManagerHelper.isDeviceProvisioned(context) && mUserCaps.mUserSwitcherEnabled) { + isVisible = true; mAddGuest.setVisible(true); - Drawable icon = context.getDrawable(R.drawable.ic_account_circle); - mAddGuest.setIcon(centerAndTint(icon)); + // when isEnableGuestModeUxChanges() is true, the icon is set via the layout xml + // In com.android.settings.users.UserSettingsTest + // we disable the check for setIcon being called + if (!isEnableGuestModeUxChanges()) { + Drawable icon = context.getDrawable(R.drawable.ic_account_circle); + mAddGuest.setIcon(centerAndTint(icon)); + } mAddGuest.setSelectable(true); if (mGuestUserAutoCreated && mGuestCreationScheduled.get()) { mAddGuest.setTitle(com.android.settingslib.R.string.user_guest); @@ -1167,19 +1472,26 @@ public class UserSettings extends SettingsPreferenceFragment } else { mAddGuest.setVisible(false); } + return isVisible; } private void updateAddUser(Context context) { updateAddUserCommon(context, mAddUser, mUserCaps.mCanAddRestrictedProfile); - Drawable icon = context.getDrawable(R.drawable.ic_account_circle_filled); - mAddUser.setIcon(centerAndTint(icon)); + // when isEnableGuestModeUxChanges() is true, the icon is set via the layout xml + if (!isEnableGuestModeUxChanges()) { + Drawable icon = context.getDrawable(R.drawable.ic_account_circle_filled); + mAddUser.setIcon(centerAndTint(icon)); + } } private void updateAddSupervisedUser(Context context) { if (!TextUtils.isEmpty(mConfigSupervisedUserCreationPackage)) { updateAddUserCommon(context, mAddSupervisedUser, false); - Drawable icon = context.getDrawable(R.drawable.ic_add_supervised_user); - mAddSupervisedUser.setIcon(centerAndTint(icon)); + // when isEnableGuestModeUxChanges() is true, the icon is set via the layout xml + if (!isEnableGuestModeUxChanges()) { + Drawable icon = context.getDrawable(R.drawable.ic_add_supervised_user); + mAddSupervisedUser.setIcon(centerAndTint(icon)); + } } else { mAddSupervisedUser.setVisible(false); } @@ -1276,17 +1588,36 @@ public class UserSettings extends SettingsPreferenceFragment @Override public boolean onPreferenceClick(Preference pref) { - if (pref == mMePreference) { - if (isCurrentUserGuest()) { - if (mGuestUserAutoCreated) { - showDialog(DIALOG_CONFIRM_RESET_GUEST); - } else { - showDialog(DIALOG_CONFIRM_EXIT_GUEST); + if (isCurrentUserGuest()) { + if (isEnableGuestModeUxChanges()) { + if (mGuestResetPreference != null && pref == mGuestResetPreference) { + showDialog(DIALOG_CONFIRM_RESET_AND_RESTART_GUEST); + return true; + } + if (mGuestExitPreference != null && pref == mGuestExitPreference) { + if (mUserCaps.mIsEphemeral) { + showDialog(DIALOG_CONFIRM_EXIT_GUEST_EPHEMERAL); + } else { + showDialog(DIALOG_CONFIRM_EXIT_GUEST_NON_EPHEMERAL); + } + return true; } } else { - showDialog(DIALOG_USER_PROFILE_EDITOR); + if (mGuestExitPreference != null && pref == mGuestExitPreference) { + if (mGuestUserAutoCreated) { + showDialog(DIALOG_CONFIRM_REMOVE_GUEST); + } else { + showDialog(DIALOG_CONFIRM_REMOVE_GUEST_WITH_AUTO_CREATE); + } + return true; + } + } + } + if (pref == mMePreference) { + if (!isCurrentUserGuest()) { + showDialog(DIALOG_USER_PROFILE_EDITOR); + return true; } - return true; } else if (pref instanceof UserPreference) { UserInfo userInfo = mUserManager.getUserInfo(((UserPreference) pref).getUserId()); openUserDetails(userInfo, false); diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java index ea51370c471..b865ea6ad15 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java @@ -226,4 +226,32 @@ public class ShadowUserManager extends org.robolectric.shadows.ShadowUserManager return new UserInfo(PRIMARY_USER_ID, null, null, UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_ADMIN | UserInfo.FLAG_PRIMARY); } + + protected boolean setUserEphemeral(@UserIdInt int userId, boolean enableEphemeral) { + UserInfo userInfo = mUserProfileInfos.stream() + .filter(user -> user.id == userId) + .findFirst() + .orElse(super.getUserInfo(userId)); + + boolean isSuccess = false; + boolean isEphemeralUser = + (userInfo.flags & UserInfo.FLAG_EPHEMERAL) != 0; + boolean isEphemeralOnCreateUser = + (userInfo.flags & UserInfo.FLAG_EPHEMERAL_ON_CREATE) + != 0; + // when user is created in ephemeral mode via FLAG_EPHEMERAL + // its state cannot be changed. + // FLAG_EPHEMERAL_ON_CREATE is used to keep track of this state + if (!isEphemeralOnCreateUser) { + isSuccess = true; + if (isEphemeralUser != enableEphemeral) { + if (enableEphemeral) { + userInfo.flags |= UserInfo.FLAG_EPHEMERAL; + } else { + userInfo.flags &= ~UserInfo.FLAG_EPHEMERAL; + } + } + } + return isSuccess; + } } diff --git a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java index 1376712d75f..107519eaf11 100644 --- a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java +++ b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java @@ -56,6 +56,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import androidx.fragment.app.FragmentActivity; +import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; @@ -152,6 +153,8 @@ public class UserSettingsTest { ReflectionHelpers.setField(mFragment, "mDefaultIconDrawable", mDefaultIconDrawable); ReflectionHelpers.setField(mFragment, "mAddingUser", false); ReflectionHelpers.setField(mFragment, "mMetricsFeatureProvider", mMetricsFeatureProvider); + ReflectionHelpers.setField(mFragment, "mRemoveGuestOnExitPreferenceController", + mock(RemoveGuestOnExitPreferenceController.class)); doReturn(mUserManager).when(mActivity).getSystemService(UserManager.class); doReturn(mPackageManager).when(mActivity).getPackageManager(); @@ -178,6 +181,11 @@ public class UserSettingsTest { mFragment.mAddSupervisedUser = mAddSupervisedUserPreference; mFragment.mAddGuest = mAddGuestPreference; mFragment.mUserListCategory = mock(PreferenceCategory.class); + mFragment.mGuestUserCategory = mock(PreferenceCategory.class); + mFragment.mGuestCategory = mock(PreferenceCategory.class); + mFragment.mGuestResetPreference = mock(Preference.class); + mFragment.mGuestExitPreference = mock(Preference.class); + mFragment.mGuestInfoPreference = mock(Preference.class); } @After @@ -219,7 +227,7 @@ public class UserSettingsTest { @Test public void testExitGuest_ShouldLogAction() { mUserCapabilities.mIsGuest = true; - mFragment.exitGuest(); + mFragment.clearAndExitGuest(); verify(mMetricsFeatureProvider).action(any(), eq(SettingsEnums.ACTION_USER_GUEST_EXIT_CONFIRMED)); } @@ -227,7 +235,7 @@ public class UserSettingsTest { @Test public void testExitGuestWhenNotGuest_ShouldNotLogAction() { mUserCapabilities.mIsGuest = false; - mFragment.exitGuest(); + mFragment.clearAndExitGuest(); verify(mMetricsFeatureProvider, never()).action(any(), eq(SettingsEnums.ACTION_USER_GUEST_EXIT_CONFIRMED)); } @@ -323,7 +331,6 @@ public class UserSettingsTest { verify(mAddGuestPreference).setVisible(true); verify(mAddGuestPreference).setEnabled(true); - verify(mAddGuestPreference).setIcon(any(Drawable.class)); verify(mAddGuestPreference).setSelectable(true); } @@ -370,7 +377,6 @@ public class UserSettingsTest { verify(mAddGuestPreference).setVisible(true); verify(mAddGuestPreference).setEnabled(false); - verify(mAddGuestPreference).setIcon(any(Drawable.class)); verify(mAddGuestPreference).setSelectable(true); } @@ -472,9 +478,9 @@ public class UserSettingsTest { mFragment.updateUserList(); ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); - verify(mFragment.mUserListCategory, times(2)) + verify(mFragment.mGuestUserCategory, times(1)) .addPreference(captor.capture()); - UserPreference guestPref = captor.getAllValues().get(1); + UserPreference guestPref = captor.getAllValues().get(0); assertThat(guestPref.getUserId()).isEqualTo(INACTIVE_GUEST_USER_ID); assertThat(guestPref.getTitle()).isEqualTo("Guest"); assertThat(guestPref.getIcon()).isNotNull(); @@ -594,9 +600,9 @@ public class UserSettingsTest { mFragment.updateUserList(); ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); - verify(mFragment.mUserListCategory, times(2)) + verify(mFragment.mGuestUserCategory, times(1)) .addPreference(captor.capture()); - UserPreference userPref = captor.getAllValues().get(1); + UserPreference userPref = captor.getAllValues().get(0); assertThat(userPref.getUserId()).isEqualTo(INACTIVE_GUEST_USER_ID); assertThat(userPref.getSummary()).isNull(); }