From 4d7d4effa5d98a0a84bf0c7539cd60bd05571c7b Mon Sep 17 00:00:00 2001 From: Andras Kloczl Date: Tue, 21 Apr 2020 11:32:12 +0100 Subject: [PATCH] Improve multi user settings screen - Added switch and user delete functionality to details screen. - Added robo tests. Screenshots: http://shortn/_S6fbIMhAYO Bug: 142798722 Test: Run robo tests with this command: make -j64 RunSettingsRoboTests ROBOTEST_FILTER="com.android.settings.users.*SettingsTest" Change-Id: Ied67290e8fed87feb0a60a3f2c40eb91cc57988e --- res/drawable/ic_add_40dp.xml | 36 ++ res/drawable/ic_phone.xml | 29 + res/drawable/ic_swap.xml | 25 + ...stricted_preference_user_delete_widget.xml | 60 -- res/layout/user_info_header.xml | 39 +- res/xml/user_details_settings.xml | 7 +- res/xml/user_settings.xml | 7 +- .../users/AppRestrictionsFragment.java | 8 + .../users/RestrictedProfileSettings.java | 33 +- .../settings/users/UserDetailsSettings.java | 276 +++++--- .../settings/users/UserPreference.java | 79 +-- .../android/settings/users/UserSettings.java | 256 ++++---- .../testutils/shadow/ShadowUserManager.java | 39 +- .../users/UserDetailsSettingsTest.java | 467 ++++++++++++++ .../settings/users/UserPreferenceTest.java | 24 +- .../settings/users/UserSettingsTest.java | 607 ++++++++++++++---- 16 files changed, 1469 insertions(+), 523 deletions(-) create mode 100644 res/drawable/ic_add_40dp.xml create mode 100644 res/drawable/ic_phone.xml create mode 100644 res/drawable/ic_swap.xml delete mode 100644 res/layout/restricted_preference_user_delete_widget.xml create mode 100644 tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java diff --git a/res/drawable/ic_add_40dp.xml b/res/drawable/ic_add_40dp.xml new file mode 100644 index 00000000000..7245823749b --- /dev/null +++ b/res/drawable/ic_add_40dp.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/res/drawable/ic_phone.xml b/res/drawable/ic_phone.xml new file mode 100644 index 00000000000..28f47fd3486 --- /dev/null +++ b/res/drawable/ic_phone.xml @@ -0,0 +1,29 @@ + + + + \ No newline at end of file diff --git a/res/drawable/ic_swap.xml b/res/drawable/ic_swap.xml new file mode 100644 index 00000000000..1c43d974428 --- /dev/null +++ b/res/drawable/ic_swap.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/layout/restricted_preference_user_delete_widget.xml b/res/layout/restricted_preference_user_delete_widget.xml deleted file mode 100644 index 71f1dd7c8bf..00000000000 --- a/res/layout/restricted_preference_user_delete_widget.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout/user_info_header.xml b/res/layout/user_info_header.xml index 5135e0e2d27..bfdf3fc7324 100644 --- a/res/layout/user_info_header.xml +++ b/res/layout/user_info_header.xml @@ -15,6 +15,7 @@ --> + android:layout_marginEnd="@*android:dimen/preference_item_padding_inner"/> @@ -88,6 +89,40 @@ android:layout_gravity="center" android:background="?android:attr/selectableItemBackground" /> + + + + + + + + + diff --git a/res/xml/user_details_settings.xml b/res/xml/user_details_settings.xml index 09154c483d7..d336395d02a 100644 --- a/res/xml/user_details_settings.xml +++ b/res/xml/user_details_settings.xml @@ -17,12 +17,17 @@ + + diff --git a/res/xml/user_settings.xml b/res/xml/user_settings.xml index eb8803b73ee..7726a18ea94 100644 --- a/res/xml/user_settings.xml +++ b/res/xml/user_settings.xml @@ -27,10 +27,15 @@ settings:searchable="false"> + + switchUser()); + // This is going to bind the preferences. super.onActivityCreated(savedInstanceState); } @@ -80,7 +92,6 @@ public class RestrictedProfileSettings extends AppRestrictionsFragment @Override public void onResume() { super.onResume(); - // Check if user still exists UserInfo info = Utils.getExistingUser(mUserManager, mUser); if (info == null) { @@ -89,6 +100,16 @@ public class RestrictedProfileSettings extends AppRestrictionsFragment ((TextView) mHeaderView.findViewById(android.R.id.title)).setText(info.name); ((ImageView) mHeaderView.findViewById(android.R.id.icon)).setImageDrawable( com.android.settingslib.Utils.getUserIcon(getActivity(), mUserManager, info)); + + boolean canSwitchUser = + mUserManager.getUserSwitchability() == UserManager.SWITCHABILITY_STATUS_OK; + if (mShowSwitchUser && canSwitchUser) { + mSwitchUserView.setVisibility(View.VISIBLE); + mSwitchTitle.setText(getString(com.android.settingslib.R.string.user_switch_to_user, + info.name)); + } else { + mSwitchUserView.setVisibility(View.GONE); + } } } @@ -158,6 +179,16 @@ public class RestrictedProfileSettings extends AppRestrictionsFragment }); } + private void switchUser() { + try { + ActivityManager.getService().switchUser(mUser.getIdentifier()); + } catch (RemoteException re) { + Log.e(TAG, "Error while switching to other user."); + } finally { + finishFragment(); + } + } + @Override public void onPhotoChanged(UserHandle user, Drawable photo) { mUserIconView.setImageDrawable(photo); diff --git a/src/com/android/settings/users/UserDetailsSettings.java b/src/com/android/settings/users/UserDetailsSettings.java index 371c152e157..2696ddc1319 100644 --- a/src/com/android/settings/users/UserDetailsSettings.java +++ b/src/com/android/settings/users/UserDetailsSettings.java @@ -16,55 +16,63 @@ package com.android.settings.users; +import static android.os.UserHandle.USER_NULL; + +import android.app.ActivityManager; import android.app.Dialog; import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.DialogInterface; import android.content.pm.UserInfo; import android.os.Bundle; +import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.util.Log; +import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.SwitchPreference; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.Utils; +import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtilsInternal; import java.util.List; /** - * Settings screen for configuring a specific user. It can contain user restrictions - * and deletion controls. It is shown when you tap on the settings icon in the - * user management (UserSettings) screen. + * Settings screen for configuring, deleting or switching to a specific user. + * It is shown when you tap on a user in the user management (UserSettings) screen. * * Arguments to this fragment must include the userId of the user (in EXTRA_USER_ID) for whom - * to display controls, or should contain the EXTRA_USER_GUEST = true. + * to display controls. */ public class UserDetailsSettings extends SettingsPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener { private static final String TAG = UserDetailsSettings.class.getSimpleName(); + private static final String KEY_SWITCH_USER = "switch_user"; private static final String KEY_ENABLE_TELEPHONY = "enable_calling"; private static final String KEY_REMOVE_USER = "remove_user"; /** Integer extra containing the userId to manage */ static final String EXTRA_USER_ID = "user_id"; - /** Boolean extra to indicate guest preferences */ - static final String EXTRA_USER_GUEST = "guest_user"; private static final int DIALOG_CONFIRM_REMOVE = 1; private static final int DIALOG_CONFIRM_ENABLE_CALLING = 2; private static final int DIALOG_CONFIRM_ENABLE_CALLING_AND_SMS = 3; private UserManager mUserManager; + @VisibleForTesting + Preference mSwitchUserPref; private SwitchPreference mPhonePref; - private Preference mRemoveUserPref; + @VisibleForTesting + Preference mRemoveUserPref; - private UserInfo mUserInfo; - private boolean mGuestUser; + @VisibleForTesting + UserInfo mUserInfo; private Bundle mDefaultGuestRestrictions; @Override @@ -78,46 +86,28 @@ public class UserDetailsSettings extends SettingsPreferenceFragment final Context context = getActivity(); mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); - addPreferencesFromResource(R.xml.user_details_settings); - mPhonePref = (SwitchPreference) findPreference(KEY_ENABLE_TELEPHONY); - mRemoveUserPref = findPreference(KEY_REMOVE_USER); - mGuestUser = getArguments().getBoolean(EXTRA_USER_GUEST, false); + initialize(context, getArguments()); + } - if (!mGuestUser) { - // Regular user. Get the user id from the caller. - final int userId = getArguments().getInt(EXTRA_USER_ID, -1); - if (userId == -1) { - throw new RuntimeException("Arguments to this fragment must contain the user id"); - } - mUserInfo = mUserManager.getUserInfo(userId); - mPhonePref.setChecked(!mUserManager.hasUserRestriction( - UserManager.DISALLOW_OUTGOING_CALLS, new UserHandle(userId))); - mRemoveUserPref.setOnPreferenceClickListener(this); - } else { - // These are not for an existing user, just general Guest settings. - removePreference(KEY_REMOVE_USER); - // Default title is for calling and SMS. Change to calling-only here - mPhonePref.setTitle(R.string.user_enable_calling); - mDefaultGuestRestrictions = mUserManager.getDefaultGuestRestrictions(); - mPhonePref.setChecked( - !mDefaultGuestRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS)); - } - if (RestrictedLockUtilsInternal.hasBaseUserRestriction(context, - UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId())) { - removePreference(KEY_REMOVE_USER); - } - mPhonePref.setOnPreferenceChangeListener(this); + @Override + public void onResume() { + super.onResume(); + mSwitchUserPref.setEnabled(canSwitchUserNow()); } @Override public boolean onPreferenceClick(Preference preference) { if (preference == mRemoveUserPref) { - if (!mUserManager.isAdminUser()) { - throw new RuntimeException("Only admins can remove a user"); + if (canDeleteUser()) { + showDialog(DIALOG_CONFIRM_REMOVE); + } + return true; + } else if (preference == mSwitchUserPref) { + if (canSwitchUserNow()) { + switchUser(); } - showDialog(DIALOG_CONFIRM_REMOVE); return true; } return false; @@ -126,7 +116,7 @@ public class UserDetailsSettings extends SettingsPreferenceFragment @Override public boolean onPreferenceChange(Preference preference, Object newValue) { if (Boolean.TRUE.equals(newValue)) { - showDialog(mGuestUser ? DIALOG_CONFIRM_ENABLE_CALLING + showDialog(mUserInfo.isGuest() ? DIALOG_CONFIRM_ENABLE_CALLING : DIALOG_CONFIRM_ENABLE_CALLING_AND_SMS); return false; } @@ -134,65 +124,6 @@ public class UserDetailsSettings extends SettingsPreferenceFragment return true; } - void enableCallsAndSms(boolean enabled) { - mPhonePref.setChecked(enabled); - if (mGuestUser) { - mDefaultGuestRestrictions.putBoolean(UserManager.DISALLOW_OUTGOING_CALLS, !enabled); - // SMS is always disabled for guest - mDefaultGuestRestrictions.putBoolean(UserManager.DISALLOW_SMS, true); - mUserManager.setDefaultGuestRestrictions(mDefaultGuestRestrictions); - - // Update the guest's restrictions, if there is a guest - // TODO: Maybe setDefaultGuestRestrictions() can internally just set the restrictions - // on any existing guest rather than do it here with multiple Binder calls. - List users = mUserManager.getUsers(true); - for (UserInfo user: users) { - if (user.isGuest()) { - UserHandle userHandle = UserHandle.of(user.id); - for (String key : mDefaultGuestRestrictions.keySet()) { - mUserManager.setUserRestriction( - key, mDefaultGuestRestrictions.getBoolean(key), userHandle); - } - } - } - } else { - UserHandle userHandle = UserHandle.of(mUserInfo.id); - mUserManager.setUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS, !enabled, - userHandle); - mUserManager.setUserRestriction(UserManager.DISALLOW_SMS, !enabled, userHandle); - } - } - - @Override - public Dialog onCreateDialog(int dialogId) { - Context context = getActivity(); - if (context == null) return null; - switch (dialogId) { - case DIALOG_CONFIRM_REMOVE: - return UserDialogs.createRemoveDialog(getActivity(), mUserInfo.id, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - removeUser(); - } - }); - case DIALOG_CONFIRM_ENABLE_CALLING: - return UserDialogs.createEnablePhoneCallsDialog(getActivity(), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - enableCallsAndSms(true); - } - }); - case DIALOG_CONFIRM_ENABLE_CALLING_AND_SMS: - return UserDialogs.createEnablePhoneCallsAndSmsDialog(getActivity(), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - enableCallsAndSms(true); - } - }); - } - throw new IllegalArgumentException("Unsupported dialogId " + dialogId); - } - @Override public int getDialogMetricsCategory(int dialogId) { switch (dialogId) { @@ -207,7 +138,148 @@ public class UserDetailsSettings extends SettingsPreferenceFragment } } - void removeUser() { + @Override + public Dialog onCreateDialog(int dialogId) { + Context context = getActivity(); + if (context == null) { + return null; + } + switch (dialogId) { + case DIALOG_CONFIRM_REMOVE: + return UserDialogs.createRemoveDialog(getActivity(), mUserInfo.id, + (dialog, which) -> removeUser()); + case DIALOG_CONFIRM_ENABLE_CALLING: + return UserDialogs.createEnablePhoneCallsDialog(getActivity(), + (dialog, which) -> enableCallsAndSms(true)); + case DIALOG_CONFIRM_ENABLE_CALLING_AND_SMS: + return UserDialogs.createEnablePhoneCallsAndSmsDialog(getActivity(), + (dialog, which) -> enableCallsAndSms(true)); + } + throw new IllegalArgumentException("Unsupported dialogId " + dialogId); + } + + @VisibleForTesting + @Override + protected void showDialog(int dialogId) { + super.showDialog(dialogId); + } + + @VisibleForTesting + void initialize(Context context, Bundle arguments) { + int userId = arguments != null ? arguments.getInt(EXTRA_USER_ID, USER_NULL) : USER_NULL; + if (userId == USER_NULL) { + throw new IllegalStateException("Arguments to this fragment must contain the user id"); + } + mUserInfo = mUserManager.getUserInfo(userId); + + mSwitchUserPref = findPreference(KEY_SWITCH_USER); + mPhonePref = findPreference(KEY_ENABLE_TELEPHONY); + mRemoveUserPref = findPreference(KEY_REMOVE_USER); + + mSwitchUserPref.setTitle( + context.getString(com.android.settingslib.R.string.user_switch_to_user, + mUserInfo.name)); + mSwitchUserPref.setOnPreferenceClickListener(this); + + if (!mUserManager.isAdminUser()) { // non admin users can't remove users and allow calls + removePreference(KEY_ENABLE_TELEPHONY); + removePreference(KEY_REMOVE_USER); + } else { + if (!Utils.isVoiceCapable(context)) { // no telephony + removePreference(KEY_ENABLE_TELEPHONY); + } + + if (!mUserInfo.isGuest()) { + mPhonePref.setChecked(!mUserManager.hasUserRestriction( + UserManager.DISALLOW_OUTGOING_CALLS, new UserHandle(userId))); + mRemoveUserPref.setTitle(R.string.user_remove_user); + } else { + // These are not for an existing user, just general Guest settings. + // Default title is for calling and SMS. Change to calling-only here + mPhonePref.setTitle(R.string.user_enable_calling); + mDefaultGuestRestrictions = mUserManager.getDefaultGuestRestrictions(); + mPhonePref.setChecked( + !mDefaultGuestRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS)); + mRemoveUserPref.setTitle(R.string.user_exit_guest_title); + } + if (RestrictedLockUtilsInternal.hasBaseUserRestriction(context, + UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId())) { + removePreference(KEY_REMOVE_USER); + } + + mRemoveUserPref.setOnPreferenceClickListener(this); + mPhonePref.setOnPreferenceChangeListener(this); + } + } + + @VisibleForTesting + boolean canDeleteUser() { + if (!mUserManager.isAdminUser()) { + return false; + } + + Context context = getActivity(); + if (context == null) { + return false; + } + + final RestrictedLockUtils.EnforcedAdmin removeDisallowedAdmin = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context, + UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId()); + if (removeDisallowedAdmin != null) { + RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, + removeDisallowedAdmin); + return false; + } + return true; + } + + @VisibleForTesting + boolean canSwitchUserNow() { + return mUserManager.getUserSwitchability() == UserManager.SWITCHABILITY_STATUS_OK; + } + + @VisibleForTesting + void switchUser() { + try { + ActivityManager.getService().switchUser(mUserInfo.id); + } catch (RemoteException re) { + Log.e(TAG, "Error while switching to other user."); + } finally { + finishFragment(); + } + } + + private void enableCallsAndSms(boolean enabled) { + mPhonePref.setChecked(enabled); + if (mUserInfo.isGuest()) { + mDefaultGuestRestrictions.putBoolean(UserManager.DISALLOW_OUTGOING_CALLS, !enabled); + // SMS is always disabled for guest + mDefaultGuestRestrictions.putBoolean(UserManager.DISALLOW_SMS, true); + mUserManager.setDefaultGuestRestrictions(mDefaultGuestRestrictions); + + // Update the guest's restrictions, if there is a guest + // TODO: Maybe setDefaultGuestRestrictions() can internally just set the restrictions + // on any existing guest rather than do it here with multiple Binder calls. + List users = mUserManager.getUsers(true); + for (UserInfo user : users) { + if (user.isGuest()) { + UserHandle userHandle = UserHandle.of(user.id); + for (String key : mDefaultGuestRestrictions.keySet()) { + mUserManager.setUserRestriction( + key, mDefaultGuestRestrictions.getBoolean(key), userHandle); + } + } + } + } else { + UserHandle userHandle = UserHandle.of(mUserInfo.id); + mUserManager.setUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS, !enabled, + userHandle); + mUserManager.setUserRestriction(UserManager.DISALLOW_SMS, !enabled, userHandle); + } + } + + private void removeUser() { mUserManager.removeUser(mUserInfo.id); finishFragment(); } diff --git a/src/com/android/settings/users/UserPreference.java b/src/com/android/settings/users/UserPreference.java index 3603d44ea09..0b78d787608 100644 --- a/src/com/android/settings/users/UserPreference.java +++ b/src/com/android/settings/users/UserPreference.java @@ -21,18 +21,16 @@ import android.graphics.drawable.Drawable; import android.os.UserHandle; import android.os.UserManager; import android.util.AttributeSet; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.ImageView; import androidx.preference.PreferenceViewHolder; -import com.android.settings.R; -import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; import java.util.Comparator; +/** + * Preference for a user that appear on {@link UserSettings} screen. + */ public class UserPreference extends RestrictedPreference { private static final int ALPHA_ENABLED = 255; private static final int ALPHA_DISABLED = 102; @@ -44,8 +42,7 @@ public class UserPreference extends RestrictedPreference { if (p1 == null) { return -1; - } - else if (p2 == null) { + } else if (p2 == null) { return 1; } int sn1 = p1.getSerialNumber(); @@ -58,26 +55,15 @@ public class UserPreference extends RestrictedPreference { return 0; }; - private OnClickListener mDeleteClickListener; - private OnClickListener mSettingsClickListener; private int mSerialNumber = -1; private int mUserId = USERID_UNKNOWN; - static final int SETTINGS_ID = R.id.manage_user; - static final int DELETE_ID = R.id.trash_user; public UserPreference(Context context, AttributeSet attrs) { - this(context, attrs, USERID_UNKNOWN, null, null); + this(context, attrs, USERID_UNKNOWN); } - UserPreference(Context context, AttributeSet attrs, int userId, - OnClickListener settingsListener, - OnClickListener deleteListener) { + UserPreference(Context context, AttributeSet attrs, int userId) { super(context, attrs); - if (deleteListener != null || settingsListener != null) { - setWidgetLayoutResource(R.layout.restricted_preference_user_delete_widget); - } - mDeleteClickListener = deleteListener; - mSettingsClickListener = settingsListener; mUserId = userId; useAdminDisabledSummary(true); } @@ -92,62 +78,13 @@ public class UserPreference extends RestrictedPreference { @Override protected boolean shouldHideSecondTarget() { - if (isDisabledByAdmin()) { - // Disabled by admin, show no secondary target. - return true; - } - if (canDeleteUser()) { - // Need to show delete user target so don't hide. - return false; - } - // Hide if don't have advanced setting listener. - return mSettingsClickListener == null; + return true; } @Override public void onBindViewHolder(PreferenceViewHolder view) { super.onBindViewHolder(view); - final boolean disabledByAdmin = isDisabledByAdmin(); - dimIcon(disabledByAdmin); - View userDeleteWidget = view.findViewById(R.id.user_delete_widget); - if (userDeleteWidget != null) { - userDeleteWidget.setVisibility(disabledByAdmin ? View.GONE : View.VISIBLE); - } - if (!disabledByAdmin) { - View deleteDividerView = view.findViewById(R.id.divider_delete); - View manageDividerView = view.findViewById(R.id.divider_manage); - View deleteView = view.findViewById(R.id.trash_user); - if (deleteView != null) { - if (canDeleteUser()) { - deleteView.setVisibility(View.VISIBLE); - deleteDividerView.setVisibility(View.VISIBLE); - deleteView.setOnClickListener(mDeleteClickListener); - deleteView.setTag(this); - } else { - deleteView.setVisibility(View.GONE); - deleteDividerView.setVisibility(View.GONE); - } - } - ImageView manageView = (ImageView) view.findViewById(R.id.manage_user); - if (manageView != null) { - if (mSettingsClickListener != null) { - manageView.setVisibility(View.VISIBLE); - manageDividerView.setVisibility(mDeleteClickListener == null - ? View.VISIBLE : View.GONE); - manageView.setOnClickListener(mSettingsClickListener); - manageView.setTag(this); - } else { - manageView.setVisibility(View.GONE); - manageDividerView.setVisibility(View.GONE); - } - } - } - } - - private boolean canDeleteUser() { - return mDeleteClickListener != null - && !RestrictedLockUtilsInternal.hasBaseUserRestriction(getContext(), - UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId()); + dimIcon(isDisabledByAdmin()); } private int getSerialNumber() { diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java index 38ef199c5cb..7d4ab5d5312 100644 --- a/src/com/android/settings/users/UserSettings.java +++ b/src/com/android/settings/users/UserSettings.java @@ -48,7 +48,6 @@ import android.util.SparseArray; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.View; import android.widget.SimpleAdapter; import androidx.annotation.VisibleForTesting; @@ -69,7 +68,6 @@ import com.android.settings.password.ChooseLockGeneric; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.widget.SwitchBar; import com.android.settings.widget.SwitchBarController; -import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; @@ -89,15 +87,14 @@ import java.util.Random; /** * Screen that manages the list of users on the device. - * Guest user is an always visible entry, even if the guest is not currently - * active/created. It is meant for controlling properties of a guest user. + * Secondary users and a guest user can be created if there is no restriction. * - * The first one is always the current user. + * The first user in the list is always the current user. * Owner is the primary user. */ @SearchIndexable public class UserSettings extends SettingsPreferenceFragment - implements Preference.OnPreferenceClickListener, View.OnClickListener, + implements Preference.OnPreferenceClickListener, MultiUserSwitchBarController.OnMultiUserSwitchChangedListener, DialogInterface.OnDismissListener { @@ -111,6 +108,7 @@ public class UserSettings extends SettingsPreferenceFragment private static final String KEY_USER_LIST = "user_list"; private static final String KEY_USER_ME = "user_me"; private static final String KEY_USER_GUEST = "user_guest"; + private static final String KEY_ADD_GUEST = "guest_add"; private static final String KEY_ADD_USER = "user_add"; private static final String KEY_ADD_USER_WHEN_LOCKED = "user_settings_add_users_when_locked"; private static final String KEY_MULTIUSER_FOOTER = "multiuser_footer"; @@ -156,7 +154,11 @@ public class UserSettings extends SettingsPreferenceFragment @VisibleForTesting UserPreference mMePreference; @VisibleForTesting + RestrictedPreference mAddGuest; + @VisibleForTesting RestrictedPreference mAddUser; + @VisibleForTesting + SparseArray mUserIcons = new SparseArray<>(); private int mRemovingUserId = -1; private int mAddedUserId = 0; private boolean mAddingUser; @@ -165,7 +167,6 @@ public class UserSettings extends SettingsPreferenceFragment private boolean mShouldUpdateUserList = true; private final Object mUserLock = new Object(); private UserManager mUserManager; - private SparseArray mUserIcons = new SparseArray<>(); private static SparseArray sDarkDefaultUserBitmapCache = new SparseArray<>(); private MultiUserSwitchBarController mSwitchBarController; @@ -271,15 +272,17 @@ public class UserSettings extends SettingsPreferenceFragment final int myUserId = UserHandle.myUserId(); mUserListCategory = (PreferenceGroup) findPreference(KEY_USER_LIST); - mMePreference = new UserPreference(getPrefContext(), null /* attrs */, myUserId, - null /* settings icon handler */, - null /* delete icon handler */); + mMePreference = new UserPreference(getPrefContext(), null /* attrs */, myUserId); mMePreference.setKey(KEY_USER_ME); mMePreference.setOnPreferenceClickListener(this); if (mUserCaps.mIsAdmin) { mMePreference.setSummary(R.string.user_admin); } - mAddUser = (RestrictedPreference) findPreference(KEY_ADD_USER); + + mAddGuest = findPreference(KEY_ADD_GUEST); + mAddGuest.setOnPreferenceClickListener(this); + + mAddUser = findPreference(KEY_ADD_USER); if (!mUserCaps.mCanAddRestrictedProfile) { // Label should only mention adding a "user", not a "profile" mAddUser.setTitle(R.string.user_add_user_menu); @@ -344,8 +347,7 @@ public class UserSettings extends SettingsPreferenceFragment @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { int pos = 0; - final boolean canSwitchUsers = mUserManager.canSwitchUsers(); - if (!mUserCaps.mIsAdmin && canSwitchUsers) { + if (!mUserCaps.mIsAdmin && canSwitchUserNow()) { String nickname = mUserManager.getUserName(); MenuItem removeThisUser = menu.add(0, MENU_REMOVE_USER, pos++, getResources().getString(R.string.user_remove_user_menu, nickname)); @@ -386,10 +388,13 @@ public class UserSettings extends SettingsPreferenceFragment * Loads profile information for the current user. */ private void loadProfile() { - if (mUserCaps.mIsGuest) { + if (isCurrentUserGuest()) { // No need to load profile information mMePreference.setIcon(getEncircledDefaultIcon()); mMePreference.setTitle(R.string.user_exit_guest_title); + mMePreference.setSelectable(true); + // removing a guest will result in switching back to the admin user + mMePreference.setEnabled(canSwitchUserNow()); return; } @@ -412,7 +417,9 @@ public class UserSettings extends SettingsPreferenceFragment } private void finishLoadProfile(String profileName) { - if (getActivity() == null) return; + if (getActivity() == null) { + return; + } mMePreference.setTitle(getString(R.string.user_you, profileName)); int myUserId = UserHandle.myUserId(); Bitmap b = mUserManager.getUserIcon(myUserId); @@ -477,38 +484,28 @@ public class UserSettings extends SettingsPreferenceFragment private void onManageUserClicked(int userId, boolean newUser) { mAddingUser = false; - if (userId == UserPreference.USERID_GUEST_DEFAULTS) { - Bundle extras = new Bundle(); - extras.putBoolean(UserDetailsSettings.EXTRA_USER_GUEST, true); - new SubSettingLauncher(getContext()) - .setDestination(UserDetailsSettings.class.getName()) - .setArguments(extras) - .setTitleRes(R.string.user_guest) - .setSourceMetricsCategory(getMetricsCategory()) - .launch(); - return; - } - UserInfo info = mUserManager.getUserInfo(userId); - if (info.isRestricted() && mUserCaps.mIsAdmin) { + UserInfo userInfo = mUserManager.getUserInfo(userId); + if (userInfo.isRestricted() && mUserCaps.mIsAdmin) { Bundle extras = new Bundle(); extras.putInt(RestrictedProfileSettings.EXTRA_USER_ID, userId); extras.putBoolean(RestrictedProfileSettings.EXTRA_NEW_USER, newUser); + extras.putBoolean(RestrictedProfileSettings.EXTRA_SHOW_SWITCH_USER, canSwitchUserNow()); new SubSettingLauncher(getContext()) .setDestination(RestrictedProfileSettings.class.getName()) .setArguments(extras) .setTitleRes(R.string.user_restrictions_title) .setSourceMetricsCategory(getMetricsCategory()) .launch(); - } else if (info.id == UserHandle.myUserId()) { + } else if (userId == UserHandle.myUserId()) { // Jump to owner info panel OwnerInfoSettings.show(this); - } else if (mUserCaps.mIsAdmin) { - final Bundle extras = new Bundle(); + } else { + Bundle extras = new Bundle(); extras.putInt(UserDetailsSettings.EXTRA_USER_ID, userId); new SubSettingLauncher(getContext()) .setDestination(UserDetailsSettings.class.getName()) .setArguments(extras) - .setTitleText(info.name) + .setTitleText(userInfo.name) .setSourceMetricsCategory(getMetricsCategory()) .launch(); } @@ -538,7 +535,9 @@ public class UserSettings extends SettingsPreferenceFragment @Override public Dialog onCreateDialog(int dialogId) { Context context = getActivity(); - if (context == null) return null; + if (context == null) { + return null; + } switch (dialogId) { case DIALOG_CONFIRM_REMOVE: { Dialog dlg = @@ -811,7 +810,7 @@ public class UserSettings extends SettingsPreferenceFragment } private void removeThisUser() { - if (!mUserManager.canSwitchUsers()) { + if (!canSwitchUserNow()) { Log.w(TAG, "Cannot remove current user when switching is disabled"); return; } @@ -882,10 +881,14 @@ public class UserSettings extends SettingsPreferenceFragment } private void switchUserNow(int userId) { + if (!canSwitchUserNow()) { + return; + } + try { ActivityManager.getService().switchUser(userId); } catch (RemoteException re) { - // Nothing to do + Log.e(TAG, "Error while switching to other user."); } } @@ -894,7 +897,7 @@ public class UserSettings extends SettingsPreferenceFragment */ private void exitGuest() { // Just to be safe - if (!mUserCaps.mIsGuest) { + if (!isCurrentUserGuest()) { return; } removeThisUser(); @@ -908,12 +911,12 @@ public class UserSettings extends SettingsPreferenceFragment } final List users = mUserManager.getUsers(true); - final boolean voiceCapable = Utils.isVoiceCapable(context); final ArrayList missingIcons = new ArrayList<>(); final ArrayList userPreferences = new ArrayList<>(); - int guestId = UserPreference.USERID_GUEST_DEFAULTS; 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. @@ -924,37 +927,38 @@ public class UserSettings extends SettingsPreferenceFragment if (user.id == UserHandle.myUserId()) { pref = mMePreference; } else if (user.isGuest()) { - // Skip over Guest. We add generic Guest settings after this loop - guestId = user.id; - continue; + pref = new UserPreference(getPrefContext(), null, user.id); + pref.setTitle(R.string.user_guest); + pref.setIcon(getEncircledDefaultIcon()); + pref.setKey(KEY_USER_GUEST); + userPreferences.add(pref); + pref.setEnabled(canOpenUserDetails); + pref.setSelectable(true); + + if (mUserCaps.mDisallowSwitchUser) { + pref.setDisabledByAdmin(RestrictedLockUtilsInternal.getDeviceOwner(context)); + } else { + pref.setDisabledByAdmin(null); + } + pref.setOnPreferenceClickListener(this); } else { - // With Telephony: - // Secondary user: Settings - // Guest: Settings - // Restricted Profile: There is no Restricted Profile - // Without Telephony: - // Secondary user: Delete - // Guest: Nothing - // Restricted Profile: Settings - final boolean showSettings = mUserCaps.mIsAdmin - && (voiceCapable || user.isRestricted()); - final boolean showDelete = mUserCaps.mIsAdmin - && (!voiceCapable && !user.isRestricted() && !user.isGuest()); - pref = new UserPreference(getPrefContext(), null, user.id, - showSettings ? this : null, - showDelete ? this : null); + pref = new UserPreference(getPrefContext(), null, user.id); pref.setKey("id=" + user.id); userPreferences.add(pref); if (user.isAdmin()) { pref.setSummary(R.string.user_admin); } pref.setTitle(user.name); - pref.setSelectable(false); + pref.setOnPreferenceClickListener(this); + pref.setEnabled(canOpenUserDetails); + pref.setSelectable(true); } if (pref == null) { continue; } - if (!isInitialized(user)) { + if (user.id != UserHandle.myUserId() && !user.isGuest() && !user.isInitialized()) { + // sometimes after creating a guest the initialized flag isn't immediately set + // and we don't want to show "Not set up" summary for them if (user.isRestricted()) { pref.setSummary(R.string.user_summary_restricted_not_set_up); } else { @@ -962,10 +966,7 @@ public class UserSettings extends SettingsPreferenceFragment } // Disallow setting up user which results in user switching when the restriction is // set. - if (!mUserCaps.mDisallowSwitchUser) { - pref.setOnPreferenceClickListener(this); - pref.setSelectable(mUserManager.canSwitchUsers()); - } + pref.setEnabled(!mUserCaps.mDisallowSwitchUser && canSwitchUserNow()); } else if (user.isRestricted()) { pref.setSummary(R.string.user_summary_restricted_profile); } @@ -986,53 +987,13 @@ public class UserSettings extends SettingsPreferenceFragment // Add a temporary entry for the user being created if (mAddingUser) { UserPreference pref = new UserPreference(getPrefContext(), null, - UserPreference.USERID_UNKNOWN, null, null); + UserPreference.USERID_UNKNOWN); pref.setEnabled(false); pref.setTitle(mAddingUserName); pref.setIcon(getEncircledDefaultIcon()); userPreferences.add(pref); } - // Check if Guest tile should be added. - if (!mUserCaps.mIsGuest && (mUserCaps.mCanAddGuest || - mUserCaps.mDisallowAddUserSetByAdmin)) { - // Add a virtual Guest user for guest defaults - UserPreference pref = new UserPreference(getPrefContext(), null, - UserPreference.USERID_GUEST_DEFAULTS, - mUserCaps.mIsAdmin && voiceCapable ? this : null /* settings icon handler */, - null /* delete icon handler */); - pref.setTitle(R.string.user_guest); - pref.setIcon(getEncircledDefaultIcon()); - pref.setKey(KEY_USER_GUEST); - userPreferences.add(pref); - if (mUserCaps.mDisallowAddUser) { - pref.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); - } else if (mUserCaps.mDisallowSwitchUser) { - pref.setDisabledByAdmin(RestrictedLockUtilsInternal.getDeviceOwner(context)); - } else { - pref.setDisabledByAdmin(null); - } - if (!mUserManager.canSwitchUsers()) { - pref.setSelectable(false); - } - int finalGuestId = guestId; - pref.setOnPreferenceClickListener(preference -> { - int id = finalGuestId; - if (id == UserPreference.USERID_GUEST_DEFAULTS) { - UserInfo guest = mUserManager.createGuest( - getContext(), preference.getTitle().toString()); - if (guest != null) { - id = guest.id; - } - } - try { - ActivityManager.getService().switchUser(id); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } - return true; - }); - } // Sort list of users by serialNum Collections.sort(userPreferences, UserPreference.SERIAL_NUMBER_COMPARATOR); @@ -1064,6 +1025,7 @@ public class UserSettings extends SettingsPreferenceFragment mMultiUserFooterPreferenceController.updateState(multiUserFooterPrefence); mUserListCategory.setVisible(mUserCaps.mUserSwitcherEnabled); + updateAddGuest(context, users.stream().anyMatch(UserInfo::isGuest)); updateAddUser(context); if (!mUserCaps.mUserSwitcherEnabled) { @@ -1077,15 +1039,38 @@ public class UserSettings extends SettingsPreferenceFragment } + private boolean isCurrentUserGuest() { + return mUserCaps.mIsGuest; + } + + private boolean canSwitchUserNow() { + return mUserManager.getUserSwitchability() == UserManager.SWITCHABILITY_STATUS_OK; + } + + private void updateAddGuest(Context context, boolean isGuestAlreadyCreated) { + if (!isGuestAlreadyCreated && mUserCaps.mCanAddGuest + && WizardManagerHelper.isDeviceProvisioned(context) + && mUserCaps.mUserSwitcherEnabled) { + mAddGuest.setVisible(true); + mAddGuest.setIcon(getEncircledDefaultIcon()); + mAddGuest.setEnabled(canSwitchUserNow()); + mAddGuest.setSelectable(true); + } else { + mAddGuest.setVisible(false); + } + } + private void updateAddUser(Context context) { if ((mUserCaps.mCanAddUser || mUserCaps.mDisallowAddUserSetByAdmin) && WizardManagerHelper.isDeviceProvisioned(context) && mUserCaps.mUserSwitcherEnabled) { mAddUser.setVisible(true); - final boolean moreUsers = mUserManager.canAddMoreUsers(); - mAddUser.setEnabled(moreUsers && !mAddingUser && mUserManager.canSwitchUsers()); - if (!moreUsers) { - mAddUser.setSummary(getString(R.string.user_add_max_count, getMaxRealUsers())); + mAddUser.setSelectable(true); + final boolean canAddMoreUsers = mUserManager.canAddMoreUsers(); + mAddUser.setEnabled(canAddMoreUsers && !mAddingUser && canSwitchUserNow()); + if (!canAddMoreUsers) { + mAddUser.setSummary( + getString(R.string.user_add_max_count, getRealUsersCount())); } else { mAddUser.setSummary(null); } @@ -1098,18 +1083,15 @@ public class UserSettings extends SettingsPreferenceFragment } } - private int getMaxRealUsers() { - // guest is not counted against getMaxSupportedUsers() number - final int maxUsersAndGuest = UserManager.getMaxSupportedUsers() + 1; - final List users = mUserManager.getUsers(); - // managed profiles are counted against getMaxSupportedUsers() - int managedProfiles = 0; - for (UserInfo user : users) { - if (user.isManagedProfile()) { - managedProfiles++; - } - } - return maxUsersAndGuest - managedProfiles; + /** + * @return number of non-guest non-managed users + */ + @VisibleForTesting + int getRealUsersCount() { + return (int) mUserManager.getUsers() + .stream() + .filter(user -> !user.isGuest() && !user.isProfile()) + .count(); } private void loadIconsAsync(List missingIcons) { @@ -1151,12 +1133,12 @@ public class UserSettings extends SettingsPreferenceFragment @Override public boolean onPreferenceClick(Preference pref) { if (pref == mMePreference) { - if (mUserCaps.mIsGuest) { + if (isCurrentUserGuest()) { showDialog(DIALOG_CONFIRM_EXIT_GUEST); return true; } // If this is a limited user, launch the user info settings instead of profile editor - if (mUserManager.isLinkedUser()) { + if (mUserManager.isRestrictedProfile()) { onManageUserClicked(UserHandle.myUserId(), false); } else { showDialog(DIALOG_USER_PROFILE_EDITOR); @@ -1165,9 +1147,11 @@ public class UserSettings extends SettingsPreferenceFragment int userId = ((UserPreference) pref).getUserId(); // Get the latest status of the user UserInfo user = mUserManager.getUserInfo(userId); - if (!isInitialized(user)) { + if (!user.isInitialized()) { mHandler.sendMessage(mHandler.obtainMessage( MESSAGE_SETUP_USER, user.id, user.serialNumber)); + } else { + onManageUserClicked(userId, false); } } else if (pref == mAddUser) { // If we allow both types, show a picker, otherwise directly go to @@ -1177,39 +1161,19 @@ public class UserSettings extends SettingsPreferenceFragment } else { onAddUserClicked(USER_TYPE_USER); } + } else if (pref == mAddGuest) { + UserInfo guest = mUserManager.createGuest( + getContext(), getString(com.android.settingslib.R.string.user_guest)); + switchUserNow(guest.id); } return false; } - private boolean isInitialized(UserInfo user) { - return (user.flags & UserInfo.FLAG_INITIALIZED) != 0; - } - private Drawable encircle(Bitmap icon) { Drawable circled = CircleFramedDrawable.getInstance(getActivity(), icon); return circled; } - @Override - public void onClick(View v) { - if (v.getTag() instanceof UserPreference) { - int userId = ((UserPreference) v.getTag()).getUserId(); - if (v.getId() == UserPreference.DELETE_ID) { - final EnforcedAdmin removeDisallowedAdmin = - RestrictedLockUtilsInternal.checkIfRestrictionEnforced(getContext(), - UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId()); - if (removeDisallowedAdmin != null) { - RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), - removeDisallowedAdmin); - } else { - onRemoveUserClicked(userId); - } - } else if (v.getId() == UserPreference.SETTINGS_ID) { - onManageUserClicked(userId, false); - } - } - } - @Override public void onDismiss(DialogInterface dialog) { synchronized (mUserLock) { 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 659c5de770c..1d4f2019fb8 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java @@ -18,6 +18,7 @@ package com.android.settings.testutils.shadow; import android.annotation.UserIdInt; import android.content.pm.UserInfo; +import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; import android.os.UserManager.EnforcingUser; @@ -43,13 +44,17 @@ public class ShadowUserManager extends org.robolectric.shadows.ShadowUserManager private static boolean sIsSupportsMultipleUsers; - private final List mRestrictions = new ArrayList<>(); + private final List mBaseRestrictions = new ArrayList<>(); + private final List mGuestRestrictions = new ArrayList<>(); private final Map> mRestrictionSources = new HashMap<>(); private final List mUserProfileInfos = new ArrayList<>(); private final Set mManagedProfiles = new HashSet<>(); private boolean mIsQuietModeEnabled = false; private int[] profileIdsForUser = new int[0]; private boolean mUserSwitchEnabled; + + private @UserManager.UserSwitchabilityResult int mSwitchabilityStatus = + UserManager.SWITCHABILITY_STATUS_OK; private final Map mSameProfileGroupIds = Maps.newHashMap(); public void addProfile(UserInfo userInfo) { @@ -82,11 +87,22 @@ public class ShadowUserManager extends org.robolectric.shadows.ShadowUserManager @Implementation protected boolean hasBaseUserRestriction(String restrictionKey, UserHandle userHandle) { - return mRestrictions.contains(restrictionKey); + return mBaseRestrictions.contains(restrictionKey); } public void addBaseUserRestriction(String restriction) { - mRestrictions.add(restriction); + mBaseRestrictions.add(restriction); + } + + @Implementation + protected Bundle getDefaultGuestRestrictions() { + Bundle bundle = new Bundle(); + mGuestRestrictions.forEach(restriction -> bundle.putBoolean(restriction, true)); + return bundle; + } + + public void addGuestUserRestriction(String restriction) { + mGuestRestrictions.add(restriction); } public static ShadowUserManager getShadow() { @@ -166,4 +182,21 @@ public class ShadowUserManager extends org.robolectric.shadows.ShadowUserManager public void setSupportsMultipleUsers(boolean supports) { sIsSupportsMultipleUsers = supports; } + + @Implementation + protected UserInfo getUserInfo(@UserIdInt int userId) { + return mUserProfileInfos.stream() + .filter(userInfo -> userInfo.id == userId) + .findFirst() + .orElse(super.getUserInfo(userId)); + } + + @Implementation + protected @UserManager.UserSwitchabilityResult int getUserSwitchability() { + return mSwitchabilityStatus; + } + + public void setSwitchabilityStatus(@UserManager.UserSwitchabilityResult int newStatus) { + mSwitchabilityStatus = newStatus; + } } diff --git a/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java new file mode 100644 index 00000000000..6c5478262aa --- /dev/null +++ b/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java @@ -0,0 +1,467 @@ +/* + * Copyright (C) 2020 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.settings.users; + +import static android.os.UserManager.SWITCHABILITY_STATUS_OK; +import static android.os.UserManager.SWITCHABILITY_STATUS_USER_IN_CALL; +import static android.os.UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.UserInfo; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.telephony.TelephonyManager; + +import androidx.fragment.app.FragmentActivity; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.settings.R; +import com.android.settings.testutils.shadow.ShadowDevicePolicyManager; +import com.android.settings.testutils.shadow.ShadowUserManager; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.android.controller.ActivityController; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.util.ReflectionHelpers; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = { + ShadowUserManager.class, + ShadowDevicePolicyManager.class +}) +public class UserDetailsSettingsTest { + + private static final String KEY_SWITCH_USER = "switch_user"; + private static final String KEY_ENABLE_TELEPHONY = "enable_calling"; + private static final String KEY_REMOVE_USER = "remove_user"; + + private static final int DIALOG_CONFIRM_REMOVE = 1; + + @Mock + private TelephonyManager mTelephonyManager; + + private ShadowUserManager mUserManager; + + @Mock + private Preference mSwitchUserPref; + @Mock + private SwitchPreference mPhonePref; + @Mock + private Preference mRemoveUserPref; + + private FragmentActivity mActivity; + private Context mContext; + private UserDetailsSettings mFragment; + private Bundle mArguments; + private UserInfo mUserInfo; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mActivity = spy(ActivityController.of(new FragmentActivity()).get()); + mContext = spy(RuntimeEnvironment.application); + mFragment = spy(new UserDetailsSettings()); + mArguments = new Bundle(); + + UserManager userManager = (UserManager) mContext.getSystemService( + Context.USER_SERVICE); + mUserManager = Shadow.extract(userManager); + + doReturn(mTelephonyManager).when(mActivity).getSystemService(Context.TELEPHONY_SERVICE); + + ReflectionHelpers.setField(mFragment, "mUserManager", userManager); + doReturn(mActivity).when(mFragment).getActivity(); + doReturn(mContext).when(mFragment).getContext(); + + doReturn(mock(PreferenceScreen.class)).when(mFragment).getPreferenceScreen(); + doReturn("").when(mActivity).getString(anyInt(), anyString()); + + doReturn(mSwitchUserPref).when(mFragment).findPreference(KEY_SWITCH_USER); + doReturn(mPhonePref).when(mFragment).findPreference(KEY_ENABLE_TELEPHONY); + doReturn(mRemoveUserPref).when(mFragment).findPreference(KEY_REMOVE_USER); + } + + @After + public void tearDown() { + ShadowUserManager.reset(); + } + + @Test(expected = IllegalStateException.class) + public void initialize_nullArguments_shouldThrowException() { + mFragment.initialize(mActivity, null); + } + + @Test(expected = IllegalStateException.class) + public void initialize_emptyArguments_shouldThrowException() { + mFragment.initialize(mActivity, new Bundle()); + } + + @Test + public void initialize_userSelected_shouldSetupSwitchPref() { + setupSelectedUser(); + doReturn("Switch to " + mUserInfo.name) + .when(mActivity).getString(anyInt(), anyString()); + + mFragment.initialize(mActivity, mArguments); + + verify(mActivity).getString(com.android.settingslib.R.string.user_switch_to_user, + mUserInfo.name); + verify(mSwitchUserPref).setTitle("Switch to " + mUserInfo.name); + verify(mSwitchUserPref).setOnPreferenceClickListener(mFragment); + verify(mFragment, never()).removePreference(KEY_SWITCH_USER); + } + + @Test + public void initialize_guestSelected_shouldSetupSwitchPref() { + setupSelectedGuest(); + doReturn("Switch to " + mUserInfo.name) + .when(mActivity).getString(anyInt(), anyString()); + + mFragment.initialize(mActivity, mArguments); + + verify(mActivity).getString(com.android.settingslib.R.string.user_switch_to_user, + mUserInfo.name); + verify(mSwitchUserPref).setTitle("Switch to " + mUserInfo.name); + verify(mSwitchUserPref).setOnPreferenceClickListener(mFragment); + verify(mFragment, never()).removePreference(KEY_SWITCH_USER); + } + + @Test + public void onResume_canSwitch_shouldEnableSwitchPref() { + mUserManager.setSwitchabilityStatus(SWITCHABILITY_STATUS_OK); + mFragment.mSwitchUserPref = mSwitchUserPref; + mFragment.onAttach(mContext); + + mFragment.onResume(); + + verify(mSwitchUserPref).setEnabled(true); + } + + @Test + public void onResume_userInCall_shouldDisableSwitchPref() { + mUserManager.setSwitchabilityStatus(SWITCHABILITY_STATUS_USER_IN_CALL); + mFragment.mSwitchUserPref = mSwitchUserPref; + mFragment.onAttach(mContext); + + mFragment.onResume(); + + verify(mSwitchUserPref).setEnabled(false); + } + + @Test + public void onResume_switchDisallowed_shouldDisableSwitchPref() { + mUserManager.setSwitchabilityStatus(SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED); + mFragment.mSwitchUserPref = mSwitchUserPref; + mFragment.onAttach(mContext); + + mFragment.onResume(); + + verify(mSwitchUserPref).setEnabled(false); + } + + @Test + public void onResume_systemUserLocked_shouldDisableSwitchPref() { + mUserManager.setSwitchabilityStatus(UserManager.SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED); + mFragment.mSwitchUserPref = mSwitchUserPref; + mFragment.onAttach(mContext); + + mFragment.onResume(); + + verify(mSwitchUserPref).setEnabled(false); + } + + @Test + public void initialize_adminWithTelephony_shouldShowPhonePreference() { + setupSelectedUser(); + doReturn(true).when(mTelephonyManager).isVoiceCapable(); + mUserManager.setIsAdminUser(true); + + mFragment.initialize(mActivity, mArguments); + + verify(mFragment, never()).removePreference(KEY_ENABLE_TELEPHONY); + verify(mPhonePref).setOnPreferenceChangeListener(mFragment); + } + + @Test + public void initialize_adminNoTelephony_shouldNotShowPhonePreference() { + setupSelectedUser(); + doReturn(false).when(mTelephonyManager).isVoiceCapable(); + mUserManager.setIsAdminUser(true); + doReturn(null).when(mActivity).getSystemService(Context.TELEPHONY_SERVICE); + + mFragment.initialize(mActivity, mArguments); + + verify(mFragment).removePreference(KEY_ENABLE_TELEPHONY); + } + + @Test + public void initialize_nonAdminWithTelephony_shouldNotShowPhonePreference() { + setupSelectedUser(); + doReturn(true).when(mTelephonyManager).isVoiceCapable(); + mUserManager.setIsAdminUser(false); + + mFragment.initialize(mActivity, mArguments); + + verify(mFragment).removePreference(KEY_ENABLE_TELEPHONY); + } + + @Test + public void initialize_adminSelectsSecondaryUser_shouldShowRemovePreference() { + setupSelectedUser(); + mUserManager.setIsAdminUser(true); + + mFragment.initialize(mActivity, mArguments); + + verify(mRemoveUserPref).setOnPreferenceClickListener(mFragment); + verify(mRemoveUserPref).setTitle(R.string.user_remove_user); + verify(mFragment, never()).removePreference(KEY_REMOVE_USER); + } + + @Test + public void initialize_adminSelectsGuest_shouldShowRemovePreference() { + setupSelectedGuest(); + mUserManager.setIsAdminUser(true); + + mFragment.initialize(mActivity, mArguments); + + verify(mRemoveUserPref).setOnPreferenceClickListener(mFragment); + verify(mRemoveUserPref).setTitle(R.string.user_exit_guest_title); + verify(mFragment, never()).removePreference(KEY_REMOVE_USER); + } + + @Test + public void initialize_nonAdmin_shouldNotShowRemovePreference() { + setupSelectedUser(); + mUserManager.setIsAdminUser(false); + + mFragment.initialize(mActivity, mArguments); + + verify(mFragment).removePreference(KEY_REMOVE_USER); + } + + @Test + public void initialize_disallowRemoveUserRestriction_shouldNotShowRemovePreference() { + setupSelectedUser(); + mUserManager.setIsAdminUser(true); + mUserManager.addBaseUserRestriction(UserManager.DISALLOW_REMOVE_USER); + + mFragment.initialize(mActivity, mArguments); + + verify(mFragment).removePreference(KEY_REMOVE_USER); + } + + @Test + public void initialize_userHasCallRestriction_shouldSetPhoneSwitchUnChecked() { + setupSelectedUser(); + mUserManager.setIsAdminUser(true); + mUserManager.setUserRestriction(mUserInfo.getUserHandle(), + UserManager.DISALLOW_OUTGOING_CALLS, true); + + mFragment.initialize(mActivity, mArguments); + + verify(mPhonePref).setChecked(false); + } + + @Test + public void initialize_noCallRestriction_shouldSetPhoneSwitchChecked() { + setupSelectedUser(); + mUserManager.setIsAdminUser(true); + + mFragment.initialize(mActivity, mArguments); + + verify(mPhonePref).setChecked(true); + } + + @Test + public void initialize_guestSelected_noCallRestriction_shouldSetPhonePreference() { + setupSelectedGuest(); + mUserManager.setIsAdminUser(true); + + mFragment.initialize(mActivity, mArguments); + + verify(mPhonePref).setTitle(R.string.user_enable_calling); + verify(mPhonePref).setChecked(true); + } + + @Test + public void initialize_guestSelected_callRestriction_shouldSetPhonePreference() { + setupSelectedGuest(); + mUserManager.setIsAdminUser(true); + mUserManager.addGuestUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS); + + mFragment.initialize(mActivity, mArguments); + + verify(mPhonePref).setTitle(R.string.user_enable_calling); + verify(mPhonePref).setChecked(false); + } + + @Test + public void onPreferenceClick_switchClicked_canSwitch_shouldSwitch() { + setupSelectedUser(); + mUserManager.setSwitchabilityStatus(SWITCHABILITY_STATUS_OK); + mFragment.mSwitchUserPref = mSwitchUserPref; + mFragment.mRemoveUserPref = mRemoveUserPref; + mFragment.mUserInfo = mUserInfo; + + mFragment.onPreferenceClick(mSwitchUserPref); + + verify(mFragment).switchUser(); + } + + @Test + public void onPreferenceClick_switchClicked_canNotSwitch_doNothing() { + setupSelectedUser(); + mUserManager.setSwitchabilityStatus(SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED); + mFragment.mSwitchUserPref = mSwitchUserPref; + mFragment.mRemoveUserPref = mRemoveUserPref; + mFragment.mUserInfo = mUserInfo; + + mFragment.onPreferenceClick(mSwitchUserPref); + + verify(mFragment, never()).switchUser(); + } + + @Test + public void onPreferenceClick_removeClicked_canDelete_shouldShowDialog() { + setupSelectedUser(); + mFragment.mUserInfo = mUserInfo; + mUserManager.setIsAdminUser(true); + mFragment.mSwitchUserPref = mSwitchUserPref; + mFragment.mRemoveUserPref = mRemoveUserPref; + doNothing().when(mFragment).showDialog(anyInt()); + + mFragment.onPreferenceClick(mRemoveUserPref); + + verify(mFragment).canDeleteUser(); + verify(mFragment).showDialog(DIALOG_CONFIRM_REMOVE); + } + + @Test + public void onPreferenceClick_removeClicked_canNotDelete_doNothing() { + setupSelectedUser(); + mFragment.mUserInfo = mUserInfo; + mUserManager.setIsAdminUser(false); + mFragment.mSwitchUserPref = mSwitchUserPref; + mFragment.mRemoveUserPref = mRemoveUserPref; + doNothing().when(mFragment).showDialog(anyInt()); + + mFragment.onPreferenceClick(mRemoveUserPref); + + verify(mFragment).canDeleteUser(); + verify(mFragment, never()).showDialog(DIALOG_CONFIRM_REMOVE); + } + + @Test + public void onPreferenceClick_unknownPreferenceClicked_doNothing() { + setupSelectedUser(); + mFragment.mUserInfo = mUserInfo; + mFragment.mSwitchUserPref = mSwitchUserPref; + mFragment.mRemoveUserPref = mRemoveUserPref; + + mFragment.onPreferenceClick(mock(UserPreference.class)); + + verify(mFragment).onPreferenceClick(any()); + verifyNoMoreInteractions(mFragment); + } + + @Test + public void canDeleteUser_nonAdminUser_shouldReturnFalse() { + mUserManager.setIsAdminUser(false); + + boolean result = mFragment.canDeleteUser(); + + assertThat(result).isFalse(); + } + + @Test + public void canDeleteUser_adminSelectsUser_noRestrictions_shouldReturnTrue() { + setupSelectedUser(); + mUserManager.setIsAdminUser(true); + + boolean result = mFragment.canDeleteUser(); + + assertThat(result).isTrue(); + } + + @Test + public void canDeleteUser_adminSelectsUser_hasRemoveRestriction_shouldReturnFalse() { + setupSelectedUser(); + mUserManager.setIsAdminUser(true); + ComponentName componentName = new ComponentName("test", "test"); + ShadowDevicePolicyManager.getShadow().setDeviceOwnerComponentOnAnyUser(componentName); + ShadowDevicePolicyManager.getShadow().setDeviceOwnerUserId(UserHandle.myUserId()); + List enforcingUsers = new ArrayList<>(); + enforcingUsers.add(new UserManager.EnforcingUser(UserHandle.myUserId(), + UserManager.RESTRICTION_SOURCE_DEVICE_OWNER)); + mUserManager.setUserRestrictionSources( + UserManager.DISALLOW_REMOVE_USER, + UserHandle.of(UserHandle.myUserId()), + enforcingUsers + ); + + boolean result = mFragment.canDeleteUser(); + + assertThat(result).isFalse(); + } + + private void setupSelectedUser() { + mArguments.putInt("user_id", 1); + mUserInfo = new UserInfo(1, "Tom", null, + UserInfo.FLAG_FULL | UserInfo.FLAG_INITIALIZED, + UserManager.USER_TYPE_FULL_SECONDARY); + + mUserManager.addProfile(mUserInfo); + } + + private void setupSelectedGuest() { + mArguments.putInt("user_id", 23); + mUserInfo = new UserInfo(23, "Guest", null, + UserInfo.FLAG_FULL | UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_GUEST, + UserManager.USER_TYPE_FULL_GUEST); + + mUserManager.addProfile(mUserInfo); + } +} diff --git a/tests/robotests/src/com/android/settings/users/UserPreferenceTest.java b/tests/robotests/src/com/android/settings/users/UserPreferenceTest.java index 345784aa51e..28f415e362b 100644 --- a/tests/robotests/src/com/android/settings/users/UserPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/users/UserPreferenceTest.java @@ -18,12 +18,8 @@ package com.android.settings.users; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import android.content.Context; import android.os.UserHandle; -import android.view.View; import com.android.settingslib.RestrictedPreferenceHelper; @@ -48,28 +44,12 @@ public class UserPreferenceTest { public void setUp() { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; - mUserPreference = new UserPreference(mContext, null /* attrs */, UserHandle.USER_CURRENT, - null /* settingsListener */, null /* deleteListener */); + mUserPreference = new UserPreference(mContext, null /* attrs */, UserHandle.USER_CURRENT); ReflectionHelpers.setField(mUserPreference, "mHelper", mRestrictedPreferenceHelper); } @Test - public void testShouldHideSecondTarget_noListener_shouldHide() { + public void testShouldHideSecondTarget_shouldHide() { assertThat(mUserPreference.shouldHideSecondTarget()).isTrue(); } - - @Test - public void testShouldHideSecondTarget_disabledByAdmin_shouldHide() { - when(mRestrictedPreferenceHelper.isDisabledByAdmin()).thenReturn(true); - - assertThat(mUserPreference.shouldHideSecondTarget()).isTrue(); - } - - @Test - public void testShouldHideSecondTarget_hasSettingListener_shouldNotHide() { - ReflectionHelpers.setField(mUserPreference, "mSettingsClickListener", - mock(View.OnClickListener.class)); - - assertThat(mUserPreference.shouldHideSecondTarget()).isFalse(); - } } diff --git a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java index 5853308c601..3104c3c2749 100644 --- a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java +++ b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java @@ -16,9 +16,14 @@ package com.android.settings.users; +import static android.os.UserManager.SWITCHABILITY_STATUS_OK; +import static android.os.UserManager.SWITCHABILITY_STATUS_USER_IN_CALL; +import static android.os.UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.notNull; @@ -26,12 +31,14 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.Context; import android.content.SharedPreferences; +import android.content.pm.UserInfo; +import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.os.UserHandle; import android.os.UserManager; @@ -42,14 +49,13 @@ import android.view.MenuInflater; import android.view.MenuItem; import androidx.fragment.app.FragmentActivity; -import androidx.preference.Preference; import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; import com.android.settings.testutils.shadow.ShadowDevicePolicyManager; import com.android.settings.testutils.shadow.ShadowUserManager; +import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedPreference; import org.junit.After; @@ -57,6 +63,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.AdditionalMatchers; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @@ -65,6 +72,7 @@ import org.robolectric.android.controller.ActivityController; import org.robolectric.annotation.Config; import org.robolectric.util.ReflectionHelpers; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -73,7 +81,18 @@ import java.util.List; public class UserSettingsTest { private static final String KEY_USER_GUEST = "user_guest"; - private int mProvisioned; + private static final int ACTIVE_USER_ID = 0; + private static final int INACTIVE_ADMIN_USER_ID = 1; + private static final int INACTIVE_SECONDARY_USER_ID = 14; + private static final int INACTIVE_RESTRICTED_USER_ID = 21; + private static final int INACTIVE_GUEST_USER_ID = 23; + private static final int MANAGED_USER_ID = 11; + private static final String ADMIN_USER_NAME = "Owner"; + private static final String SECONDARY_USER_NAME = "Tom"; + private static final String RESTRICTED_USER_NAME = "Bob"; + private static final String GUEST_USER_NAME = "Guest"; + private static final String MANAGED_USER_NAME = "Work profile"; + private int mProvisionedBackupValue; @Mock private Drawable mDefaultIconDrawable; @@ -82,6 +101,10 @@ public class UserSettingsTest { @Mock private UserPreference mMePreference; @Mock + private RestrictedPreference mAddUserPreference; + @Mock + private RestrictedPreference mAddGuestPreference; + @Mock private UserManager mUserManager; private FragmentActivity mActivity; @@ -95,6 +118,7 @@ public class UserSettingsTest { mActivity = spy(ActivityController.of(new FragmentActivity()).get()); mContext = spy(RuntimeEnvironment.application); mUserCapabilities = UserCapabilities.create(mContext); + mUserCapabilities.mUserSwitcherEnabled = true; mFragment = spy(new UserSettings()); ReflectionHelpers.setField(mFragment, "mAddUserWhenLockedPreferenceController", @@ -105,100 +129,41 @@ public class UserSettingsTest { ReflectionHelpers.setField(mFragment, "mUserCaps", mUserCapabilities); ReflectionHelpers.setField(mFragment, "mDefaultIconDrawable", mDefaultIconDrawable); ReflectionHelpers.setField(mFragment, "mAddingUser", false); - mFragment.mMePreference = mMePreference; - when((Object) mActivity.getSystemService(UserManager.class)).thenReturn(mUserManager); + doReturn(mUserManager).when(mActivity).getSystemService(UserManager.class); + doReturn(mActivity).when(mFragment).getActivity(); doReturn(mContext).when(mFragment).getContext(); doReturn(mMockPreferenceManager).when(mFragment).getPreferenceManager(); doReturn(mUserManager).when(mContext).getSystemService(UserManager.class); - mProvisioned = Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.DEVICE_PROVISIONED, 0); - final SharedPreferences prefs = mock(SharedPreferences .class); - when(mMockPreferenceManager.getSharedPreferences()).thenReturn(prefs); - when(mMockPreferenceManager.getContext()).thenReturn(mContext); + + mProvisionedBackupValue = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 0); + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 1); //default state + + final SharedPreferences prefs = mock(SharedPreferences.class); + + doReturn(prefs).when(mMockPreferenceManager).getSharedPreferences(); + doReturn(mContext).when(mMockPreferenceManager).getContext(); + doReturn(mock(PreferenceScreen.class)).when(mFragment).getPreferenceScreen(); + + mFragment.mMePreference = mMePreference; + mFragment.mAddUser = mAddUserPreference; + mFragment.mAddGuest = mAddGuestPreference; + mFragment.mUserListCategory = mock(PreferenceCategory.class); } @After public void tearDown() { Settings.Global.putInt(mContext.getContentResolver(), - Settings.Global.DEVICE_PROVISIONED, mProvisioned); + Settings.Global.DEVICE_PROVISIONED, mProvisionedBackupValue); } @Test public void testAssignDefaultPhoto_ContextNull_ReturnFalseAndNotCrash() { // Should not crash here - assertThat(UserSettings.assignDefaultPhoto(null, 0)).isFalse(); - } - - @Test - public void updateUserList_cannotSwitchUser_shouldNotBeSelectableForGuest() { - final RestrictedPreference addUser = spy(new RestrictedPreference(mContext)); - final PreferenceGroup userListCategory = spy(new PreferenceCategory(mContext)); - - mUserCapabilities.mIsGuest = false; - mUserCapabilities.mCanAddGuest = true; - mUserCapabilities.mDisallowAddUser = false; - mUserCapabilities.mDisallowSwitchUser = false; - mUserCapabilities.mUserSwitcherEnabled = true; - - mFragment.mUserListCategory = userListCategory; - mFragment.mAddUser = addUser; - - when(mUserManager.canSwitchUsers()).thenReturn(false); - doReturn(mMockPreferenceManager).when(mFragment).getPreferenceManager(); - doReturn(mock(PreferenceScreen.class)).when(mFragment).getPreferenceScreen(); - doReturn(mMockPreferenceManager).when(userListCategory).getPreferenceManager(); - - mFragment.updateUserList(); - - final Preference guest = userListCategory.findPreference(KEY_USER_GUEST); - assertThat(guest.isSelectable()).isFalse(); - } - - @Test - public void updateUserList_cannotSwitchUser_shouldDisableAddUser() { - Settings.Global.putInt(mContext.getContentResolver(), - Settings.Global.DEVICE_PROVISIONED, 1); - final RestrictedPreference addUser = spy(new RestrictedPreference(mContext)); - final PreferenceGroup userListCategory = spy(new PreferenceCategory(mContext)); - - mUserCapabilities.mCanAddUser = true; - mUserCapabilities.mDisallowAddUser = false; - mUserCapabilities.mUserSwitcherEnabled = true; - - mFragment.mUserListCategory = userListCategory; - mFragment.mAddUser = addUser; - - when(mUserManager.canSwitchUsers()).thenReturn(false); - when(mUserManager.canAddMoreUsers()).thenReturn(true); - doReturn(mMockPreferenceManager).when(mFragment).getPreferenceManager(); - doReturn(mock(PreferenceScreen.class)).when(mFragment).getPreferenceScreen(); - doReturn(mMockPreferenceManager).when(userListCategory).getPreferenceManager(); - - mFragment.updateUserList(); - - assertThat(addUser.isEnabled()).isFalse(); - } - - @Test - public void updateUserList_cannotAddUserButCanSwitchUser_shouldNotShowAddUser() { - Settings.Global.putInt(mContext.getContentResolver(), - Settings.Global.DEVICE_PROVISIONED, 1); - final RestrictedPreference addUser = mock(RestrictedPreference.class); - - mUserCapabilities.mCanAddUser = false; - mUserCapabilities.mDisallowAddUser = true; - mUserCapabilities.mUserSwitcherEnabled = true; - - mFragment.mUserListCategory = mock(PreferenceCategory.class); - mFragment.mAddUser = addUser; - - doReturn(mock(PreferenceScreen.class)).when(mFragment).getPreferenceScreen(); - - mFragment.updateUserList(); - - verify(addUser, never()).setVisible(true); + assertThat(UserSettings.assignDefaultPhoto(null, ACTIVE_USER_ID)).isFalse(); } @Test @@ -218,7 +183,7 @@ public class UserSettingsTest { ShadowDevicePolicyManager.getShadow().setDeviceOwnerComponentOnAnyUser( new ComponentName("test", "test")); - doReturn(true).when(mUserManager).canSwitchUsers(); + doReturn(SWITCHABILITY_STATUS_OK).when(mUserManager).getUserSwitchability(); mUserCapabilities.mIsAdmin = false; Menu menu = mock(Menu.class); @@ -243,7 +208,7 @@ public class UserSettingsTest { @Test public void withoutDisallowRemoveUser_ShouldNotDisableRemoveUser() { // Arrange - doReturn(true).when(mUserManager).canSwitchUsers(); + doReturn(SWITCHABILITY_STATUS_OK).when(mUserManager).getUserSwitchability(); mUserCapabilities.mIsAdmin = false; Menu menu = mock(Menu.class); @@ -266,44 +231,458 @@ public class UserSettingsTest { @Test public void updateUserList_canAddUserAndSwitchUser_shouldShowAddUser() { - Settings.Global.putInt(mContext.getContentResolver(), - Settings.Global.DEVICE_PROVISIONED, 1); - final RestrictedPreference addUser = mock(RestrictedPreference.class); - mUserCapabilities.mCanAddUser = true; - mUserCapabilities.mDisallowAddUser = false; - mUserCapabilities.mUserSwitcherEnabled = true; - - mFragment.mAddUser = addUser; - mFragment.mUserListCategory = mock(PreferenceCategory.class); - - doReturn(mock(PreferenceScreen.class)).when(mFragment).getPreferenceScreen(); - doReturn("Test summary").when(mFragment).getString(anyInt(), anyInt()); + doReturn(true).when(mUserManager).canAddMoreUsers(); + doReturn(true).when(mAddUserPreference).isEnabled(); + doReturn(SWITCHABILITY_STATUS_OK).when(mUserManager).getUserSwitchability(); mFragment.updateUserList(); - verify(addUser).setVisible(true); + verify(mAddUserPreference).setVisible(true); + verify(mAddUserPreference).setSummary(null); + verify(mAddUserPreference).setEnabled(true); + verify(mAddUserPreference).setDisabledByAdmin(null); + verify(mAddUserPreference).setSelectable(true); } @Test - public void updateUserList_addUserDisallowedByAdmin_shouldShowAddUserDisabled() { - Settings.Global.putInt(mContext.getContentResolver(), - Settings.Global.DEVICE_PROVISIONED, 1); - final RestrictedPreference addUser = mock(RestrictedPreference.class); - - mUserCapabilities.mCanAddUser = false; - mUserCapabilities.mDisallowAddUser = true; - mUserCapabilities.mDisallowAddUserSetByAdmin = true; - mUserCapabilities.mUserSwitcherEnabled = true; - - mFragment.mUserListCategory = mock(PreferenceCategory.class); - mFragment.mAddUser = addUser; - - doReturn(mock(PreferenceScreen.class)).when(mFragment).getPreferenceScreen(); + public void updateUserList_canAddGuestAndSwitchUser_shouldShowAddGuest() { + mUserCapabilities.mCanAddGuest = true; + doReturn(true).when(mUserManager).canAddMoreUsers(); + doReturn(SWITCHABILITY_STATUS_OK).when(mUserManager).getUserSwitchability(); mFragment.updateUserList(); - verify(addUser).setVisible(true); - assertThat(addUser.isEnabled()).isFalse(); + verify(mAddGuestPreference).setVisible(true); + verify(mAddGuestPreference).setEnabled(true); + verify(mAddGuestPreference).setIcon(any(Drawable.class)); + verify(mAddGuestPreference).setSelectable(true); } + + @Test + public void updateUserList_cannotSwitchUser_shouldDisableAddUser() { + mUserCapabilities.mCanAddUser = true; + doReturn(true).when(mUserManager).canAddMoreUsers(); + doReturn(true).when(mAddUserPreference).isEnabled(); + doReturn(SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED) + .when(mUserManager).getUserSwitchability(); + + mFragment.updateUserList(); + + verify(mAddUserPreference).setVisible(true); + verify(mAddUserPreference).setSummary(null); + verify(mAddUserPreference).setEnabled(false); + verify(mAddUserPreference).setSelectable(true); + } + + @Test + public void updateUserList_canNotAddMoreUsers_shouldDisableAddUserWithSummary() { + mUserCapabilities.mCanAddUser = true; + doReturn(false).when(mUserManager).canAddMoreUsers(); + doReturn(false).when(mAddUserPreference).isEnabled(); + doReturn(SWITCHABILITY_STATUS_OK).when(mUserManager).getUserSwitchability(); + doReturn(4).when(mFragment).getRealUsersCount(); + + mFragment.updateUserList(); + + verify(mAddUserPreference).setVisible(true); + verify(mAddUserPreference).setSummary("You can add up to 4 users"); + verify(mAddUserPreference).setEnabled(false); + verify(mAddUserPreference).setSelectable(true); + } + + @Test + public void updateUserList_cannotSwitchUser_shouldDisableAddGuest() { + mUserCapabilities.mCanAddGuest = true; + doReturn(true).when(mUserManager).canAddMoreUsers(); + doReturn(SWITCHABILITY_STATUS_USER_IN_CALL).when(mUserManager).getUserSwitchability(); + + mFragment.updateUserList(); + + verify(mAddGuestPreference).setVisible(true); + verify(mAddGuestPreference).setEnabled(false); + verify(mAddGuestPreference).setIcon(any(Drawable.class)); + verify(mAddGuestPreference).setSelectable(true); + } + + @Test + public void updateUserList_addUserDisallowedByAdmin_shouldShowDisabledAddUser() { + RestrictedLockUtils.EnforcedAdmin enforcedAdmin = mock( + RestrictedLockUtils.EnforcedAdmin.class); + mUserCapabilities.mEnforcedAdmin = enforcedAdmin; + mUserCapabilities.mCanAddUser = false; + mUserCapabilities.mDisallowAddUser = true; + mUserCapabilities.mDisallowAddUserSetByAdmin = true; + doReturn(true).when(mAddUserPreference).isEnabled(); + + mFragment.updateUserList(); + + verify(mAddUserPreference).setVisible(true); + ArgumentCaptor captor = ArgumentCaptor.forClass( + RestrictedLockUtils.EnforcedAdmin.class); + verify(mAddUserPreference).setDisabledByAdmin(captor.capture()); + assertThat(captor.getValue()).isEqualTo(enforcedAdmin); + } + + @Test + public void updateUserList_cannotAddUserButCanSwitchUser_shouldNotShowAddUser() { + mUserCapabilities.mCanAddUser = false; + + mFragment.updateUserList(); + + verify(mAddUserPreference).setVisible(false); + } + + @Test + public void updateUserList_canNotAddGuest_shouldNotShowAddGuest() { + mUserCapabilities.mCanAddGuest = false; + + mFragment.updateUserList(); + + verify(mAddGuestPreference).setVisible(false); + } + + @Test + public void updateUserList_notProvisionedDevice_shouldNotShowAddUser() { + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 0); + mUserCapabilities.mCanAddUser = true; + + mFragment.updateUserList(); + + verify(mAddUserPreference).setVisible(false); + } + + @Test + public void updateUserList_notProvisionedDevice_shouldNotShowAddGuest() { + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 0); + mUserCapabilities.mCanAddGuest = true; + + mFragment.updateUserList(); + + verify(mAddGuestPreference).setVisible(false); + } + + @Test + public void updateUserList_userSwitcherDisabled_shouldNotShowAddUser() { + mUserCapabilities.mCanAddUser = true; + mUserCapabilities.mUserSwitcherEnabled = false; + + mFragment.updateUserList(); + + verify(mAddUserPreference).setVisible(false); + } + + @Test + public void updateUserList_userSwitcherDisabled_shouldNotShowAddGuest() { + mUserCapabilities.mCanAddGuest = true; + mUserCapabilities.mUserSwitcherEnabled = false; + + mFragment.updateUserList(); + + verify(mAddGuestPreference).setVisible(false); + } + + @Test + public void updateUserList_shouldAddAdminUserPreference() { + givenUsers(getAdminUser(true)); + + mFragment.updateUserList(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + verify(mFragment.mUserListCategory).addPreference(captor.capture()); + UserPreference adminPref = captor.getValue(); + assertThat(adminPref).isSameAs(mMePreference); + } + + @Test + public void updateUserList_existingGuest_shouldAddGuestUserPreference() { + givenUsers(getAdminUser(true), getGuest(false)); + + mFragment.updateUserList(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + verify(mFragment.mUserListCategory, times(2)) + .addPreference(captor.capture()); + UserPreference guestPref = captor.getAllValues().get(1); + assertThat(guestPref.getUserId()).isEqualTo(INACTIVE_GUEST_USER_ID); + assertThat(guestPref.getTitle()).isEqualTo("Guest"); + assertThat(guestPref.getIcon()).isNotNull(); + assertThat(guestPref.getKey()).isEqualTo(KEY_USER_GUEST); + assertThat(guestPref.isEnabled()).isEqualTo(true); + assertThat(guestPref.isSelectable()).isEqualTo(true); + assertThat(guestPref.getOnPreferenceClickListener()).isSameAs(mFragment); + } + + @Test + public void updateUserList_existingSecondaryUser_shouldAddSecondaryUserPreference() { + givenUsers(getAdminUser(true), getSecondaryUser(false)); + + mFragment.updateUserList(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + verify(mFragment.mUserListCategory, times(2)) + .addPreference(captor.capture()); + UserPreference userPref = captor.getAllValues().get(1); + assertThat(userPref.getUserId()).isEqualTo(INACTIVE_SECONDARY_USER_ID); + assertThat(userPref.getTitle()).isEqualTo(SECONDARY_USER_NAME); + assertThat(userPref.getIcon()).isNotNull(); + assertThat(userPref.getKey()).isEqualTo("id=" + INACTIVE_SECONDARY_USER_ID); + assertThat(userPref.isEnabled()).isEqualTo(true); + assertThat(userPref.isSelectable()).isEqualTo(true); + assertThat(userPref.getOnPreferenceClickListener()).isSameAs(mFragment); + } + + @Test + public void updateUserList_existingRestrictedUser_shouldAddRestrictedUserPreference() { + givenUsers(getAdminUser(true), getRestrictedUser(false)); + + mFragment.updateUserList(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + verify(mFragment.mUserListCategory, times(2)) + .addPreference(captor.capture()); + UserPreference userPref = captor.getAllValues().get(1); + assertThat(userPref.getUserId()).isEqualTo(INACTIVE_RESTRICTED_USER_ID); + assertThat(userPref.getTitle()).isEqualTo(RESTRICTED_USER_NAME); + assertThat(userPref.getIcon()).isNotNull(); + assertThat(userPref.getKey()).isEqualTo("id=" + INACTIVE_RESTRICTED_USER_ID); + assertThat(userPref.getSummary()).isEqualTo("Restricted profile"); + assertThat(userPref.isEnabled()).isEqualTo(true); + assertThat(userPref.isSelectable()).isEqualTo(true); + assertThat(userPref.getOnPreferenceClickListener()).isSameAs(mFragment); + } + + @Test + public void updateUserList_existingManagedUser_shouldNotAddUserPreference() { + givenUsers(getAdminUser(true), getManagedUser()); + + mFragment.updateUserList(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + verify(mFragment.mUserListCategory).addPreference(captor.capture()); + List userPreferences = captor.getAllValues(); + assertThat(userPreferences.size()).isEqualTo(1); + assertThat(userPreferences.get(0).getUserId()).isEqualTo(ACTIVE_USER_ID); + } + + @Test + public void updateUserList_uninitializedRestrictedUser_shouldAddUserPreference() { + UserInfo restrictedUser = getRestrictedUser(false); + removeFlag(restrictedUser, UserInfo.FLAG_INITIALIZED); + givenUsers(getAdminUser(true), restrictedUser); + doReturn(SWITCHABILITY_STATUS_OK).when(mUserManager).getUserSwitchability(); + mUserCapabilities.mDisallowSwitchUser = false; + + mFragment.updateUserList(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + verify(mFragment.mUserListCategory, times(2)) + .addPreference(captor.capture()); + UserPreference userPref = captor.getAllValues().get(1); + assertThat(userPref.getUserId()).isEqualTo(INACTIVE_RESTRICTED_USER_ID); + assertThat(userPref.getTitle()).isEqualTo(RESTRICTED_USER_NAME); + assertThat(userPref.getIcon()).isNotNull(); + assertThat(userPref.getKey()).isEqualTo("id=" + INACTIVE_RESTRICTED_USER_ID); + assertThat(userPref.getSummary()).isEqualTo("Not set up - Restricted profile"); + assertThat(userPref.isEnabled()).isEqualTo(true); + assertThat(userPref.isSelectable()).isEqualTo(true); + assertThat(userPref.getOnPreferenceClickListener()).isSameAs(mFragment); + } + + @Test + public void updateUserList_uninitializedUserAndCanNotSwitchUser_shouldDisablePref() { + UserInfo uninitializedUser = getSecondaryUser(false); + removeFlag(uninitializedUser, UserInfo.FLAG_INITIALIZED); + givenUsers(getAdminUser(true), uninitializedUser); + doReturn(SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED) + .when(mUserManager).getUserSwitchability(); + mUserCapabilities.mDisallowSwitchUser = false; + + mFragment.updateUserList(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + verify(mFragment.mUserListCategory, times(2)) + .addPreference(captor.capture()); + UserPreference userPref = captor.getAllValues().get(1); + assertThat(userPref.getUserId()).isEqualTo(INACTIVE_SECONDARY_USER_ID); + assertThat(userPref.getTitle()).isEqualTo(SECONDARY_USER_NAME); + assertThat(userPref.getIcon()).isNotNull(); + assertThat(userPref.getKey()).isEqualTo("id=" + INACTIVE_SECONDARY_USER_ID); + assertThat(userPref.getSummary()).isEqualTo("Not set up"); + assertThat(userPref.isEnabled()).isEqualTo(false); + assertThat(userPref.isSelectable()).isEqualTo(true); + assertThat(userPref.getOnPreferenceClickListener()).isSameAs(mFragment); + } + + @Test + public void updateUserList_guestWithoutInitializedFlag_shouldNotSetSummary() { + UserInfo guest = getGuest(false); + removeFlag(guest, UserInfo.FLAG_INITIALIZED); + givenUsers(getAdminUser(true), guest); + + mFragment.updateUserList(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + verify(mFragment.mUserListCategory, times(2)) + .addPreference(captor.capture()); + UserPreference userPref = captor.getAllValues().get(1); + assertThat(userPref.getUserId()).isEqualTo(INACTIVE_GUEST_USER_ID); + assertThat(userPref.getSummary()).isNull(); + } + + @Test + public void updateUserList_activeUserWithoutInitializedFlag_shouldNotSetSummary() { + UserInfo activeUser = getSecondaryUser(true); + removeFlag(activeUser, UserInfo.FLAG_INITIALIZED); + givenUsers(activeUser); + + mFragment.updateUserList(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + verify(mFragment.mUserListCategory).addPreference(captor.capture()); + UserPreference userPref = captor.getValue(); + assertThat(userPref.getUserId()).isEqualTo(ACTIVE_USER_ID); + assertThat(userPref.getSummary()).isNull(); + } + + @Test + public void updateUserList_guestIsAlreadyCreated_shouldNotShowAddGuest() { + givenUsers(getAdminUser(true), getGuest(true)); + mUserCapabilities.mCanAddGuest = true; + + mFragment.updateUserList(); + + verify(mAddGuestPreference).setVisible(false); + } + + @Test + public void updateUserList_userIconLoaded_shouldNotLoadIcon() { + UserInfo currentUser = getAdminUser(true); + currentUser.iconPath = "/data/system/users/0/photo.png"; + givenUsers(currentUser); + mFragment.mUserIcons.put(ACTIVE_USER_ID, + Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888)); + + mFragment.updateUserList(); + + verify(mUserManager, never()).getUserIcon(anyInt()); + // updateUserList should be called only once + verify(mUserManager).getUsers(true); + } + + @Test + public void updateUserList_userIconMissing_shouldLoadIcon() { + UserInfo currentUser = getAdminUser(true); + currentUser.iconPath = "/data/system/users/0/photo.png"; + givenUsers(currentUser); + // create a non-empty sparsearray + mFragment.mUserIcons.put(5, Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888)); + Bitmap userIcon = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888); + doReturn(userIcon).when(mUserManager).getUserIcon(ACTIVE_USER_ID); + + mFragment.updateUserList(); + + verify(mUserManager).getUserIcon(ACTIVE_USER_ID); + // updateUserList should be called another time after loading the icons + verify(mUserManager, times(2)).getUsers(true); + } + + @Test + public void getRealUsersCount_onlyAdmin_shouldCount() { + givenUsers(getAdminUser(true)); + + int result = mFragment.getRealUsersCount(); + + assertThat(result).isEqualTo(1); + verify(mUserManager).getUsers(); + } + + @Test + public void getRealUsersCount_secondaryUser_shouldCount() { + givenUsers(getAdminUser(true), getSecondaryUser(false)); + + int result = mFragment.getRealUsersCount(); + + assertThat(result).isEqualTo(2); + verify(mUserManager).getUsers(); + } + + @Test + public void getRealUsersCount_restrictedUser_shouldCount() { + givenUsers(getAdminUser(true), getSecondaryUser(false)); + + int result = mFragment.getRealUsersCount(); + + assertThat(result).isEqualTo(2); + verify(mUserManager).getUsers(); + } + + @Test + public void getRealUsersCount_guest_shouldNotCount() { + givenUsers(getAdminUser(true), getGuest(false)); + + int result = mFragment.getRealUsersCount(); + + assertThat(result).isEqualTo(1); + verify(mUserManager).getUsers(); + } + + @Test + public void getRealUsersCount_managedUser_shouldNotCount() { + givenUsers(getAdminUser(true), getManagedUser()); + + int result = mFragment.getRealUsersCount(); + + assertThat(result).isEqualTo(1); + verify(mUserManager).getUsers(); + } + + private void givenUsers(UserInfo... userInfo) { + List users = Arrays.asList(userInfo); + doReturn(users).when(mUserManager).getUsers(); + doReturn(users).when(mUserManager).getUsers(anyBoolean()); + } + + private static void removeFlag(UserInfo userInfo, int flag) { + userInfo.flags &= ~flag; + } + + private static UserInfo getAdminUser(boolean active) { + return new UserInfo(active ? ACTIVE_USER_ID : INACTIVE_ADMIN_USER_ID, ADMIN_USER_NAME, + null, + UserInfo.FLAG_FULL | UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_ADMIN, + UserManager.USER_TYPE_FULL_SYSTEM); + } + + private static UserInfo getSecondaryUser(boolean active) { + return new UserInfo(active ? ACTIVE_USER_ID : INACTIVE_SECONDARY_USER_ID, + SECONDARY_USER_NAME, null, + UserInfo.FLAG_FULL | UserInfo.FLAG_INITIALIZED, + UserManager.USER_TYPE_FULL_SECONDARY); + } + + private static UserInfo getRestrictedUser(boolean active) { + return new UserInfo(active ? ACTIVE_USER_ID : INACTIVE_RESTRICTED_USER_ID, + RESTRICTED_USER_NAME, null, + UserInfo.FLAG_FULL | UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_RESTRICTED, + UserManager.USER_TYPE_FULL_RESTRICTED); + } + + private static UserInfo getManagedUser() { + return new UserInfo(MANAGED_USER_ID, + MANAGED_USER_NAME, null, + UserInfo.FLAG_PROFILE | UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_MANAGED_PROFILE, + UserManager.USER_TYPE_PROFILE_MANAGED); + } + + private static UserInfo getGuest(boolean active) { + return new UserInfo(active ? ACTIVE_USER_ID : INACTIVE_GUEST_USER_ID, GUEST_USER_NAME, + null, + UserInfo.FLAG_FULL | UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_GUEST, + UserManager.USER_TYPE_FULL_GUEST); + } + + }