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: I34a6d223c009b7217e5e85077c886241ff06a2cb
This commit is contained in:
Tetiana Meronyk
2022-11-24 16:04:49 +00:00
parent cb7dd5c61b
commit d6738a478f
10 changed files with 234 additions and 11 deletions

View File

@@ -51,6 +51,7 @@ interface IUserManager {
String[] getPreInstallableSystemPackages(in String userType);
void setUserEnabled(int userId);
void setUserAdmin(int userId);
void revokeUserAdmin(int userId);
void evictCredentialEncryptionKey(int userId);
boolean removeUser(int userId);
boolean removeUserEvenWhenDisallowed(int userId);

View File

@@ -4101,6 +4101,26 @@ public class UserManager {
}
}
/**
* Revokes admin privileges from the user, if such a user exists.
*
* <p>Note that this does not alter the user's pre-existing user restrictions.
*
* @param userId the id of the user to revoke admin rights from
* @hide
*/
@RequiresPermission(allOf = {
Manifest.permission.INTERACT_ACROSS_USERS_FULL,
Manifest.permission.MANAGE_USERS
})
public void revokeUserAdmin(@UserIdInt int userId) {
try {
mService.revokeUserAdmin(userId);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
/**
* Evicts the user's credential encryption key from memory by stopping and restarting the user.
*

View File

@@ -0,0 +1,37 @@
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/grant_admin_dialog_padding">
<RadioGroup
android:id="@+id/choose_admin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/grant_admin_yes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/grant_admin"/>
<RadioButton
android:id="@+id/grant_admin_no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/not_grant_admin"/>
</RadioGroup>
</LinearLayout>

View File

@@ -109,4 +109,7 @@
<dimen name="broadcast_dialog_btn_text_size">16sp</dimen>
<dimen name="broadcast_dialog_btn_minHeight">44dp</dimen>
<dimen name="broadcast_dialog_margin">16dp</dimen>
<!-- Size of grant admin privileges dialog padding -->
<dimen name="grant_admin_dialog_padding">16dp</dimen>
</resources>

View File

@@ -1368,6 +1368,10 @@
<string name="user_add_user_message_long">You can share this device with other people by creating additional users. Each user has their own space, which they can customize with apps, wallpaper, and so on. Users can also adjust device settings like Wi\u2011Fi that affect everyone.\n\nWhen you add a new user, that person needs to set up their space.\n\nAny user can update apps for all other users. Accessibility settings and services may not transfer to the new user.</string>
<!-- Message for add user confirmation dialog - short version. [CHAR LIMIT=none] -->
<string name="user_add_user_message_short">When you add a new user, that person needs to set up their space.\n\nAny user can update apps for all other users. </string>
<!-- Title for grant user admin privileges dialog [CHAR LIMIT=30] -->
<string name="user_grant_admin_title">Give this user admin privileges?</string>
<!-- Message for grant admin privileges dialog. [CHAR LIMIT=none] -->
<string name="user_grant_admin_message">As an admin, they will be able to manage other users, modify device settings and factory reset the device.</string>
<!-- Title of dialog to setup a new user [CHAR LIMIT=30] -->
<string name="user_setup_dialog_title">Set up user now?</string>
<!-- Message in dialog to setup a new user after creation [CHAR LIMIT=none] -->
@@ -1434,6 +1438,10 @@
<!-- Dialog message on action exit guest (ephemeral guest) [CHAR LIMIT=80] -->
<string name="guest_exit_dialog_message">This will delete
apps and data from the current guest session</string>
<!-- Dialog message on action grant admin privileges [CHAR LIMIT=60] -->
<string name="grant_admin">Give this user admin privileges</string>
<!-- Dialog message on action not grant admin privileges [CHAR LIMIT=60] -->
<string name="not_grant_admin">Do not give user admin privileges</string>
<!-- Dialog button on action exit guest (ephemeral guest) [CHAR LIMIT=80] -->
<string name="guest_exit_dialog_button">Exit</string>
<!-- Dialog title on action exit guest (non-ephemeral guest) [CHAR LIMIT=32] -->

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.users;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import com.android.settingslib.R;
import java.util.function.Consumer;
/**
* This class encapsulates a Dialog for choosing whether to grant admin privileges.
*/
public class GrantAdminDialogController {
/**
* Creates a dialog with option to grant user admin privileges.
*/
public Dialog createDialog(Activity activity,
Consumer<Boolean> successCallback, Runnable cancelCallback) {
LayoutInflater inflater = LayoutInflater.from(activity);
View content = inflater.inflate(R.layout.grant_admin_dialog_content, null);
RadioGroup radioGroup = content.findViewById(R.id.choose_admin);
RadioButton radioButton = radioGroup.findViewById(R.id.grant_admin_yes);
radioButton.setChecked(true);
Dialog dlg = new AlertDialog.Builder(activity)
.setView(content)
.setTitle(R.string.user_grant_admin_title)
.setMessage(R.string.user_grant_admin_message)
.setPositiveButton(android.R.string.ok,
(dialog, which) -> {
if (successCallback != null) {
successCallback.accept(radioButton.isChecked());
}
})
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
if (cancelCallback != null) {
cancelCallback.run();
}
})
.setOnCancelListener(dialog -> {
if (cancelCallback != null) {
cancelCallback.run();
}
})
.create();
return dlg;
}
}

View File

@@ -32,6 +32,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settingslib.users.EditUserInfoController;
import com.android.settingslib.users.GrantAdminDialogController;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
@@ -60,9 +61,10 @@ public class CreateUserActivity extends Activity {
private final IActivityManager mActivityManager;
private final ActivityStarter mActivityStarter;
private Dialog mGrantAdminDialog;
private Dialog mSetupUserDialog;
private final OnBackInvokedCallback mBackCallback = this::onBackInvoked;
private Boolean mGrantAdminRights;
@Inject
public CreateUserActivity(UserCreator userCreator,
EditUserInfoController editUserInfoController, IActivityManager activityManager,
@@ -78,14 +80,17 @@ public class CreateUserActivity extends Activity {
super.onCreate(savedInstanceState);
setShowWhenLocked(true);
setContentView(R.layout.activity_create_new_user);
if (savedInstanceState != null) {
mEditUserInfoController.onRestoreInstanceState(savedInstanceState);
}
mSetupUserDialog = createDialog();
mSetupUserDialog.show();
if (mUserCreator.isHeadlessSystemUserMode()) {
mGrantAdminDialog = buildGrantAdminDialog();
mGrantAdminDialog.show();
} else {
mSetupUserDialog = createDialog();
mSetupUserDialog.show();
}
getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
mBackCallback);
@@ -124,6 +129,22 @@ public class CreateUserActivity extends Activity {
);
}
private Dialog buildGrantAdminDialog() {
return new GrantAdminDialogController().createDialog(
this,
(grantAdminRights) -> {
mGrantAdminDialog.dismiss();
mGrantAdminRights = grantAdminRights;
mSetupUserDialog = createDialog();
mSetupUserDialog.show();
},
() -> {
mGrantAdminRights = false;
finish();
}
);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@@ -139,6 +160,9 @@ public class CreateUserActivity extends Activity {
if (mSetupUserDialog != null) {
mSetupUserDialog.dismiss();
}
if (mGrantAdminDialog != null) {
mGrantAdminDialog.dismiss();
}
finish();
}
@@ -150,13 +174,15 @@ public class CreateUserActivity extends Activity {
private void addUserNow(String userName, Drawable userIcon) {
mSetupUserDialog.dismiss();
userName = (userName == null || userName.trim().isEmpty())
? getString(com.android.settingslib.R.string.user_new_user_name)
: userName;
mUserCreator.createUser(userName, userIcon,
userInfo -> {
if (mGrantAdminRights) {
mUserCreator.setUserAdmin(userInfo.id);
}
switchToUser(userInfo.id);
finishIfNeeded();
}, () -> {

View File

@@ -32,7 +32,9 @@ import javax.inject.Inject
* A class to do the user creation process. It shows a progress dialog, and manages the user
* creation
*/
class UserCreator @Inject constructor(
class UserCreator
@Inject
constructor(
private val context: Context,
private val userManager: UserManager,
@Main private val mainExecutor: Executor,
@@ -42,14 +44,14 @@ class UserCreator @Inject constructor(
* Shows a progress dialog then starts the user creation process on the main thread.
*
* @param successCallback is called when the user creation is successful.
* @param errorCallback is called when userManager.createUser returns null.
* (Exceptions are not handled by this class)
* @param errorCallback is called when userManager.createUser returns null. (Exceptions are not
* handled by this class)
*/
fun createUser(
userName: String?,
userIcon: Drawable?,
successCallback: Consumer<UserInfo?>,
errorCallback: Runnable
errorCallback: Runnable
) {
val userCreationProgressDialog: Dialog = UserCreatingDialog(context)
userCreationProgressDialog.show()
@@ -71,11 +73,21 @@ class UserCreator @Inject constructor(
newUserIcon = UserIcons.getDefaultUserIcon(res, user.id, false)
}
userManager.setUserIcon(
user.id, UserIcons.convertToBitmapAtUserIconSize(res, newUserIcon))
user.id,
UserIcons.convertToBitmapAtUserIconSize(res, newUserIcon)
)
}
userCreationProgressDialog.dismiss()
successCallback.accept(user)
}
}
}
fun setUserAdmin(userId: Int) {
userManager.setUserAdmin(userId)
}
fun isHeadlessSystemUserMode(): Boolean {
return UserManager.isHeadlessSystemUserMode()
}
}

View File

@@ -1473,6 +1473,27 @@ public class UserManagerService extends IUserManager.Stub {
}
}
@Override
public void revokeUserAdmin(@UserIdInt int userId) {
checkManageUserAndAcrossUsersFullPermission("revoke admin privileges");
synchronized (mPackagesLock) {
UserInfo info;
synchronized (mUsersLock) {
info = getUserInfoLU(userId);
}
if (info == null || !info.isAdmin()) {
// Exit if no user found with that id, or the user is not an Admin.
return;
}
info.flags ^= UserInfo.FLAG_ADMIN;
synchronized (mUsersLock) {
writeUserLP(getUserDataLU(info.id));
}
}
}
/**
* Evicts a user's CE key by stopping and restarting the user.
*

View File

@@ -617,6 +617,30 @@ public final class UserManagerTest {
assertThat(userInfo.isAdmin()).isTrue();
}
@MediumTest
@Test
public void testRevokeUserAdmin() throws Exception {
UserInfo userInfo = createUser("Admin", /*flags=*/ UserInfo.FLAG_ADMIN);
assertThat(userInfo.isAdmin()).isTrue();
mUserManager.revokeUserAdmin(userInfo.id);
userInfo = mUserManager.getUserInfo(userInfo.id);
assertThat(userInfo.isAdmin()).isFalse();
}
@MediumTest
@Test
public void testRevokeUserAdminFromNonAdmin() throws Exception {
UserInfo userInfo = createUser("NonAdmin", /*flags=*/ 0);
assertThat(userInfo.isAdmin()).isFalse();
mUserManager.revokeUserAdmin(userInfo.id);
userInfo = mUserManager.getUserInfo(userInfo.id);
assertThat(userInfo.isAdmin()).isFalse();
}
@MediumTest
@Test
public void testGetProfileParent() throws Exception {