Add UI for multiple admins on Headless

In a series of CLs under topic add_ui_for_hsum_admins UI and functionality for allowing multiple admins on HSUM build is added. In User settings and User switcher when creating a new user there is a new dialog prompting to choose admin status of the user to be created. In User details view there is a toggle that is visible to admin users that allows to modify admin status of existing users. This toggle is only applicable to full users that are not supervised, guests or a main device user.

Bug: 252790451
Test: croot && make RunSettingsRoboTests -j40 ROBOTEST_FILTER="com.android.settings.users.UserDetailsSettingsTest"
Change-Id: I447dc168be30aa614aeb3f8b71ad14a7223fd7c1
diff --git a/res/drawable/ic_admin_panel_settings.xml b/res/drawable/ic_admin_panel_settings.xml
new file mode 100644
index 0000000..2d3a7f1
--- /dev/null
+++ b/res/drawable/ic_admin_panel_settings.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24.0dp"
+    android:height="24.0dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M17,17Q17.625,17 18.062,16.562Q18.5,16.125 18.5,15.5Q18.5,14.875 18.062,14.438Q17.625,14 17,14Q16.375,14 15.938,14.438Q15.5,14.875 15.5,15.5Q15.5,16.125 15.938,16.562Q16.375,17 17,17ZM17,20Q17.775,20 18.425,19.637Q19.075,19.275 19.475,18.675Q18.925,18.35 18.3,18.175Q17.675,18 17,18Q16.325,18 15.7,18.175Q15.075,18.35 14.525,18.675Q14.925,19.275 15.575,19.637Q16.225,20 17,20ZM12,22Q8.525,21.125 6.263,18.012Q4,14.9 4,11.1V5L12,2L20,5V10.675Q19.525,10.475 19.025,10.312Q18.525,10.15 18,10.075V6.4L12,4.15L6,6.4V11.1Q6,12.275 6.312,13.45Q6.625,14.625 7.188,15.688Q7.75,16.75 8.55,17.65Q9.35,18.55 10.325,19.15Q10.6,19.95 11.05,20.675Q11.5,21.4 12.075,21.975Q12.05,21.975 12.038,21.988Q12.025,22 12,22ZM17,22Q14.925,22 13.463,20.538Q12,19.075 12,17Q12,14.925 13.463,13.462Q14.925,12 17,12Q19.075,12 20.538,13.462Q22,14.925 22,17Q22,19.075 20.538,20.538Q19.075,22 17,22ZM12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Z"/>
+</vector>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3d73571..41d465a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -401,6 +401,8 @@
     <string name="share">Share</string>
     <!-- Button label for generic add action [CHAR LIMIT=20] -->
     <string name="add">Add</string>
+    <!-- Button label for remove action [CHAR LIMIT=20] -->
+    <string name="remove">Remove</string>
 
     <!-- Title of the Settings activity shown within the application itself. -->
     <string name="settings_label">Settings</string>
@@ -6020,12 +6022,18 @@
 
     <!-- Title of preference to enable calling and SMS [CHAR LIMIT=45] -->
     <string name="user_enable_calling_sms">Turn on phone calls &amp; SMS</string>
+    <!-- Title of preference to grant user admin privileges [CHAR LIMIT=45] -->
+    <string name="user_grant_admin">Give this user admin privileges</string>
     <!-- Title of preference to remove the user [CHAR LIMIT=35] -->
     <string name="user_remove_user">Delete user</string>
     <!-- Title for confirmation of turning on calls and SMS [CHAR LIMIT=45] -->
     <string name="user_enable_calling_and_sms_confirm_title">Turn on phone calls &amp; SMS?</string>
     <!-- Message for confirmation of turning on calls and SMS [CHAR LIMIT=none] -->
     <string name="user_enable_calling_and_sms_confirm_message">Call and SMS history will be shared with this user.</string>
+    <!-- Title for confirmation of revoking admin privileges [CHAR LIMIT=45] -->
+    <string name="user_revoke_admin_confirm_title">Remove admin privileges?</string>
+    <!-- Message for confirmation of revoking admin privileges [CHAR LIMIT=none] -->
+    <string name="user_revoke_admin_confirm_message">Are you sure you want to remove this user\'s admin privileges?</string>
     <!-- Title for the emergency info preference [CHAR LIMIT=40] -->
     <string name="emergency_info_title">Emergency information</string>
     <!-- Summary for the emergency info preference [CHAR LIMIT=40] -->
diff --git a/res/xml/user_details_settings.xml b/res/xml/user_details_settings.xml
index 2301bac..068039c 100644
--- a/res/xml/user_details_settings.xml
+++ b/res/xml/user_details_settings.xml
@@ -22,6 +22,10 @@
             android:key="switch_user"
             android:icon="@drawable/ic_swap" />
     <SwitchPreference
+            android:key="user_grant_admin"
+            android:icon="@drawable/ic_admin_panel_settings"
+            android:title="@string/user_grant_admin" />
+    <SwitchPreference
             android:key="enable_calling"
             android:icon="@drawable/ic_phone"
             android:title="@string/user_enable_calling_sms" />
diff --git a/src/com/android/settings/users/UserDetailsSettings.java b/src/com/android/settings/users/UserDetailsSettings.java
index 7728b3e..9b4a4b9 100644
--- a/src/com/android/settings/users/UserDetailsSettings.java
+++ b/src/com/android/settings/users/UserDetailsSettings.java
@@ -61,6 +61,7 @@
     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 String KEY_GRANT_ADMIN = "user_grant_admin";
     private static final String KEY_APP_AND_CONTENT_ACCESS = "app_and_content_access";
     private static final String KEY_APP_COPYING = "app_copying";
 
@@ -72,6 +73,7 @@
     private static final int DIALOG_SETUP_USER = 3;
     private static final int DIALOG_CONFIRM_RESET_GUEST = 4;
     private static final int DIALOG_CONFIRM_RESET_GUEST_AND_SWITCH_USER = 5;
+    private static final int DIALOG_CONFIRM_REVOKE_ADMIN = 6;
 
     /** Whether to enable the app_copying fragment. */
     private static final boolean SHOW_APP_COPYING_PREF = false;
@@ -91,6 +93,8 @@
     Preference mAppCopyingPref;
     @VisibleForTesting
     Preference mRemoveUserPref;
+    @VisibleForTesting
+    SwitchPreference mGrantAdminPref;
 
     @VisibleForTesting
     /** The user being studied (not the user doing the studying). */
@@ -179,6 +183,12 @@
             mMetricsFeatureProvider.action(getActivity(),
                     SettingsEnums.ACTION_DISABLE_USER_CALL);
             enableCallsAndSms(false);
+        } else if (preference == mGrantAdminPref) {
+            if (Boolean.FALSE.equals(newValue)) {
+                showDialog(DIALOG_CONFIRM_REVOKE_ADMIN);
+                return false;
+            }
+            updateUserAdminStatus(true);
         }
         return true;
     }
@@ -192,6 +202,8 @@
                 return SettingsEnums.DIALOG_USER_REMOVE;
             case DIALOG_CONFIRM_ENABLE_CALLING_AND_SMS:
                 return SettingsEnums.DIALOG_USER_ENABLE_CALLING_AND_SMS;
+            case DIALOG_CONFIRM_REVOKE_ADMIN:
+                return SettingsEnums.DIALOG_REVOKE_USER_ADMIN;
             case DIALOG_SETUP_USER:
                 return SettingsEnums.DIALOG_USER_SETUP;
             default:
@@ -235,6 +247,9 @@
                     return UserDialogs.createRemoveGuestDialog(getActivity(),
                         (dialog, which) -> switchUser());
                 }
+            case DIALOG_CONFIRM_REVOKE_ADMIN:
+                return UserDialogs.createConfirmRevokeAdmin(getActivity(),
+                        (dialog, which) -> updateUserAdminStatus(false));
         }
         throw new IllegalArgumentException("Unsupported dialogId " + dialogId);
     }
@@ -277,6 +292,9 @@
         mRemoveUserPref = findPreference(KEY_REMOVE_USER);
         mAppAndContentAccessPref = findPreference(KEY_APP_AND_CONTENT_ACCESS);
         mAppCopyingPref = findPreference(KEY_APP_COPYING);
+        mGrantAdminPref = findPreference(KEY_GRANT_ADMIN);
+
+        mGrantAdminPref.setChecked(mUserInfo.isAdmin());
 
         mSwitchUserPref.setTitle(
                 context.getString(com.android.settingslib.R.string.user_switch_to_user,
@@ -289,10 +307,15 @@
             mSwitchUserPref.setSelectable(true);
             mSwitchUserPref.setOnPreferenceClickListener(this);
         }
-
+        //TODO(b/261700461): remove preference for supervised user
+        //TODO(b/262371063): check whether multiple admins allowed, not for HSUM
+        if (mUserInfo.isMain() || mUserInfo.isGuest() || !UserManager.isHeadlessSystemUserMode()) {
+            removePreference(KEY_GRANT_ADMIN);
+        }
         if (!mUserManager.isAdminUser()) { // non admin users can't remove users and allow calls
             removePreference(KEY_ENABLE_TELEPHONY);
             removePreference(KEY_REMOVE_USER);
+            removePreference(KEY_GRANT_ADMIN);
             removePreference(KEY_APP_AND_CONTENT_ACCESS);
             removePreference(KEY_APP_COPYING);
         } else {
@@ -339,6 +362,7 @@
 
             mRemoveUserPref.setOnPreferenceClickListener(this);
             mPhonePref.setOnPreferenceChangeListener(this);
+            mGrantAdminPref.setOnPreferenceChangeListener(this);
             mAppAndContentAccessPref.setOnPreferenceClickListener(this);
             mAppCopyingPref.setOnPreferenceClickListener(this);
         }
@@ -401,6 +425,20 @@
         mUserManager.setUserRestriction(UserManager.DISALLOW_SMS, !enabled, userHandle);
     }
 
+    /**
+     * Sets admin status of selected user. Method is called when toggle in
+     * user details settings is switched.
+     * @param isSetAdmin indicates if user admin status needs to be set to true.
+     */
+    private void updateUserAdminStatus(boolean isSetAdmin) {
+        mGrantAdminPref.setChecked(isSetAdmin);
+        if (!isSetAdmin) {
+            mUserManager.revokeUserAdmin(mUserInfo.id);
+        } else if ((mUserInfo.flags & UserInfo.FLAG_ADMIN) == 0) {
+            mUserManager.setUserAdmin(mUserInfo.id);
+        }
+    }
+
     private void removeUser() {
         mUserManager.removeUser(mUserInfo.id);
         finishFragment();
diff --git a/src/com/android/settings/users/UserDialogs.java b/src/com/android/settings/users/UserDialogs.java
index c2f2528..1e26eff 100644
--- a/src/com/android/settings/users/UserDialogs.java
+++ b/src/com/android/settings/users/UserDialogs.java
@@ -202,4 +202,19 @@
                 .setNegativeButton(android.R.string.cancel, null)
                 .create();
     }
+
+    /**
+     * Creates a dialog to confirm that the admin privileges of the user should be revoked.
+     *
+     * @param onConfirmListener Callback object for positive action
+     */
+    public static Dialog createConfirmRevokeAdmin(Context context,
+            DialogInterface.OnClickListener onConfirmListener) {
+        return new AlertDialog.Builder(context)
+                .setTitle(R.string.user_revoke_admin_confirm_title)
+                .setMessage(R.string.user_revoke_admin_confirm_message)
+                .setPositiveButton(R.string.remove, onConfirmListener)
+                .setNegativeButton(android.R.string.cancel, null)
+                .create();
+    }
 }
diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java
index da6339b..54d0b45 100644
--- a/src/com/android/settings/users/UserSettings.java
+++ b/src/com/android/settings/users/UserSettings.java
@@ -84,6 +84,7 @@
 import com.android.settingslib.search.SearchIndexable;
 import com.android.settingslib.search.SearchIndexableRaw;
 import com.android.settingslib.users.EditUserInfoController;
+import com.android.settingslib.users.GrantAdminDialogController;
 import com.android.settingslib.users.UserCreatingDialog;
 import com.android.settingslib.utils.ThreadUtils;
 
@@ -156,6 +157,7 @@
     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 DIALOG_GRANT_ADMIN = 16;
 
     private static final int MESSAGE_UPDATE_LIST = 1;
     private static final int MESSAGE_USER_CREATED = 2;
@@ -215,6 +217,9 @@
     private static SparseArray<Bitmap> sDarkDefaultUserBitmapCache = new SparseArray<>();
 
     private MultiUserSwitchBarController mSwitchBarController;
+
+    private GrantAdminDialogController mGrantAdminDialogController =
+            new GrantAdminDialogController();
     private EditUserInfoController mEditUserInfoController =
             new EditUserInfoController(Utils.FILE_PROVIDER_AUTHORITY);
     private AddUserWhenLockedPreferenceController mAddUserWhenLockedPreferenceController;
@@ -228,6 +233,7 @@
 
     private CharSequence mPendingUserName;
     private Drawable mPendingUserIcon;
+    private boolean mGrantAdmin;
 
     // A place to cache the generated default avatar
     private Drawable mDefaultIconDrawable;
@@ -287,7 +293,6 @@
         mSwitchBarController = new MultiUserSwitchBarController(activity,
                 new MainSwitchBarController(switchBar), this /* listener */);
         getSettingsLifecycle().addObserver(mSwitchBarController);
-
         boolean openUserEditDialog = getIntent().getBooleanExtra(
                 EXTRA_OPEN_DIALOG_USER_PROFILE_EDITOR, false);
         if (switchBar.isChecked() && openUserEditDialog) {
@@ -306,7 +311,7 @@
         }
 
         mGuestUserAutoCreated = getPrefContext().getResources().getBoolean(
-                        com.android.internal.R.bool.config_guestUserAutoCreated);
+                com.android.internal.R.bool.config_guestUserAutoCreated);
 
         mAddUserWhenLockedPreferenceController = new AddUserWhenLockedPreferenceController(
                 activity, KEY_ADD_USER_WHEN_LOCKED);
@@ -712,18 +717,27 @@
                         .setPositiveButton(android.R.string.ok,
                                 new DialogInterface.OnClickListener() {
                                     public void onClick(DialogInterface dialog, int which) {
-                                        showDialog(DIALOG_USER_PROFILE_EDITOR_ADD_USER);
                                         if (!longMessageDisplayed) {
                                             preferences.edit().putBoolean(
                                                     KEY_ADD_USER_LONG_MESSAGE_DISPLAYED,
                                                     true).apply();
                                         }
+                                        //TODO(b/262371063): check whether multiple admins allowed,
+                                        // not for HSUM
+                                        if (UserManager.isHeadlessSystemUserMode()) {
+                                            showDialog(DIALOG_GRANT_ADMIN);
+                                        } else {
+                                            showDialog(DIALOG_USER_PROFILE_EDITOR_ADD_USER);
+                                        }
                                     }
                                 })
                         .setNegativeButton(android.R.string.cancel, null)
                         .create();
                 return dlg;
             }
+            case DIALOG_GRANT_ADMIN: {
+                return buildGrantAdminDialog();
+            }
             case DIALOG_CHOOSE_USER_TYPE: {
                 List<HashMap<String, String>> data = new ArrayList<HashMap<String, String>>();
                 HashMap<String, String> addUserItem = new HashMap<String, String>();
@@ -931,6 +945,19 @@
         return d;
     }
 
+    private Dialog buildGrantAdminDialog() {
+        return mGrantAdminDialogController.createDialog(
+                getActivity(),
+                (grantAdmin) -> {
+                    mGrantAdmin = grantAdmin;
+                    showDialog(DIALOG_USER_PROFILE_EDITOR_ADD_USER);
+                },
+                () -> {
+                    mGrantAdmin = false;
+                }
+        );
+    }
+
     @Override
     public int getDialogMetricsCategory(int dialogId) {
         switch (dialogId) {
@@ -938,6 +965,8 @@
                 return SettingsEnums.DIALOG_USER_REMOVE;
             case DIALOG_USER_CANNOT_MANAGE:
                 return SettingsEnums.DIALOG_USER_CANNOT_MANAGE;
+            case DIALOG_GRANT_ADMIN:
+                return SettingsEnums.DIALOG_GRANT_USER_ADMIN;
             case DIALOG_ADD_USER:
                 return SettingsEnums.DIALOG_USER_ADD;
             case DIALOG_CHOOSE_USER_TYPE:
@@ -1031,6 +1060,9 @@
                         userName,
                         mUserManager.USER_TYPE_FULL_SECONDARY,
                         0);
+                if (mGrantAdmin) {
+                    mUserManager.setUserAdmin(user.id);
+                }
             } else {
                 user = mUserManager.createRestrictedProfile(userName);
             }
@@ -1351,20 +1383,20 @@
         mGuestResetPreference.setVisible(true);
 
         boolean isGuestFirstLogin = Settings.Secure.getIntForUser(
-                                        getContext().getContentResolver(),
-                                        SETTING_GUEST_HAS_LOGGED_IN,
-                                        0,
-                                        UserHandle.myUserId()) <= 1;
+                getContext().getContentResolver(),
+                SETTING_GUEST_HAS_LOGGED_IN,
+                0,
+                UserHandle.myUserId()) <= 1;
         String guestExitSummary;
         if (mUserCaps.mIsEphemeral) {
             guestExitSummary = getContext().getString(
-                                R.string.guest_notification_ephemeral);
+                    R.string.guest_notification_ephemeral);
         } else if (isGuestFirstLogin) {
             guestExitSummary = getContext().getString(
-                                R.string.guest_notification_non_ephemeral);
+                    R.string.guest_notification_non_ephemeral);
         } else {
             guestExitSummary = getContext().getString(
-                                R.string.guest_notification_non_ephemeral_non_first_login);
+                    R.string.guest_notification_non_ephemeral_non_first_login);
         }
         mGuestExitPreference.setSummary(guestExitSummary);
     }
diff --git a/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java
index 14ca76f..5cd513e 100644
--- a/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java
+++ b/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java
@@ -22,6 +22,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -84,6 +85,7 @@
 })
 public class UserDetailsSettingsTest {
 
+    private static final String KEY_GRANT_ADMIN = "user_grant_admin";
     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";
@@ -103,6 +105,8 @@
     @Mock
     private SwitchPreference mPhonePref;
     @Mock
+    private SwitchPreference mGrantAdminPref;
+    @Mock
     private Preference mRemoveUserPref;
     @Mock
     private Preference mAppAndContentAccessPref;
@@ -144,6 +148,7 @@
         doReturn(mock(PreferenceScreen.class)).when(mFragment).getPreferenceScreen();
 
         doReturn(mSwitchUserPref).when(mFragment).findPreference(KEY_SWITCH_USER);
+        doReturn(mGrantAdminPref).when(mFragment).findPreference(KEY_GRANT_ADMIN);
         doReturn(mPhonePref).when(mFragment).findPreference(KEY_ENABLE_TELEPHONY);
         doReturn(mRemoveUserPref).when(mFragment).findPreference(KEY_REMOVE_USER);
         doReturn(mAppAndContentAccessPref)
@@ -678,6 +683,38 @@
         assertThat(result).isFalse();
     }
 
+    @Test
+    public void initialize_userSelected_shouldShowGrantAdminPref_HSUM() {
+        setupSelectedUser();
+        ShadowUserManager.setIsHeadlessSystemUserMode(true);
+        mFragment.initialize(mActivity, mArguments);
+        assertTrue(UserManager.isHeadlessSystemUserMode());
+        verify(mFragment, never()).removePreference(KEY_GRANT_ADMIN);
+    }
+
+    @Test
+    public void initialize_userSelected_shouldNotShowGrantAdminPref() {
+        setupSelectedUser();
+        mFragment.initialize(mActivity, mArguments);
+        verify(mFragment).removePreference(KEY_GRANT_ADMIN);
+    }
+
+    @Test
+    public void initialize_mainUserSelected_shouldShowGrantAdminPref_HSUM() {
+        setupSelectedMainUser();
+        ShadowUserManager.setIsHeadlessSystemUserMode(true);
+        mFragment.initialize(mActivity, mArguments);
+        verify(mFragment).removePreference(KEY_GRANT_ADMIN);
+    }
+
+    @Test
+    public void initialize_guestSelected_shouldNotShowGrantAdminPref_HSUM() {
+        setupSelectedGuest();
+        ShadowUserManager.setIsHeadlessSystemUserMode(true);
+        mFragment.initialize(mActivity, mArguments);
+        verify(mFragment).removePreference(KEY_GRANT_ADMIN);
+    }
+
     private void setupSelectedUser() {
         mArguments.putInt("user_id", 1);
         mUserInfo = new UserInfo(1, "Tom", null,