diff --git a/packages/SettingsLib/res/drawable/ic_person_add.xml b/packages/SettingsLib/res/drawable/ic_person_add.xml
new file mode 100644
index 0000000000000..d138c69acb6a9
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_person_add.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/packages/SettingsLib/res/layout/dialog_with_icon.xml b/packages/SettingsLib/res/layout/dialog_with_icon.xml
index 9081ca5cc1bb8..54f8096b87bfe 100644
--- a/packages/SettingsLib/res/layout/dialog_with_icon.xml
+++ b/packages/SettingsLib/res/layout/dialog_with_icon.xml
@@ -13,87 +13,88 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-
-
-
+
+
+
-
-
-
-
-
-
-
+
diff --git a/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml b/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml
index 33c2e4855b64d..4ffaf1b0c3e49 100644
--- a/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml
+++ b/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml
@@ -28,7 +28,8 @@
android:orientation="vertical">
+ android:paddingLeft="@dimen/dialog_content_padding"
+ android:paddingBottom="@dimen/dialog_content_padding"
+ android:paddingRight="@dimen/dialog_content_padding">
+
diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml
index 2372c802168cd..91549d73dfdb8 100644
--- a/packages/SettingsLib/res/values/dimens.xml
+++ b/packages/SettingsLib/res/values/dimens.xml
@@ -112,8 +112,9 @@
44dp
16dp
-
- 16dp
+ 16dp
+ 12dp
+ 8dp
16dp
8dp
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
new file mode 100644
index 0000000000000..e61c8f5ab152c
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2023 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.annotation.IntDef;
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.util.UserIcons;
+import com.android.settingslib.R;
+import com.android.settingslib.RestrictedLockUtils;
+import com.android.settingslib.RestrictedLockUtilsInternal;
+import com.android.settingslib.drawable.CircleFramedDrawable;
+import com.android.settingslib.utils.CustomDialogHelper;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class encapsulates a Dialog for editing the user nickname and photo.
+ */
+public class CreateUserDialogController {
+
+ private static final String KEY_AWAITING_RESULT = "awaiting_result";
+ private static final String KEY_CURRENT_STATE = "current_state";
+ private static final String KEY_SAVED_PHOTO = "pending_photo";
+ private static final String KEY_SAVED_NAME = "saved_name";
+ private static final String KEY_IS_ADMIN = "admin_status";
+ private static final String KEY_ADD_USER_LONG_MESSAGE_DISPLAYED =
+ "key_add_user_long_message_displayed";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({EXIT_DIALOG, INITIAL_DIALOG, GRANT_ADMIN_DIALOG,
+ EDIT_NAME_DIALOG, CREATE_USER_AND_CLOSE})
+ public @interface AddUserState {}
+
+ private static final int EXIT_DIALOG = -1;
+ private static final int INITIAL_DIALOG = 0;
+ private static final int GRANT_ADMIN_DIALOG = 1;
+ private static final int EDIT_NAME_DIALOG = 2;
+ private static final int CREATE_USER_AND_CLOSE = 3;
+
+ private @AddUserState int mCurrentState;
+
+ private CustomDialogHelper mCustomDialogHelper;
+
+ private EditUserPhotoController mEditUserPhotoController;
+ private Bitmap mSavedPhoto;
+ private String mSavedName;
+ private Drawable mSavedDrawable;
+ private Boolean mIsAdmin;
+ private Dialog mUserCreationDialog;
+ private View mGrantAdminView;
+ private View mEditUserInfoView;
+ private EditText mUserNameView;
+ private Activity mActivity;
+ private ActivityStarter mActivityStarter;
+ private boolean mWaitingForActivityResult;
+ private NewUserData mSuccessCallback;
+
+ private final String mFileAuthority;
+
+ public CreateUserDialogController(String fileAuthority) {
+ mFileAuthority = fileAuthority;
+ }
+
+ /**
+ * Resets saved values.
+ */
+ public void clear() {
+ mUserCreationDialog = null;
+ mCustomDialogHelper = null;
+ mEditUserPhotoController = null;
+ mSavedPhoto = null;
+ mSavedName = null;
+ mSavedDrawable = null;
+ mIsAdmin = null;
+ mActivity = null;
+ mActivityStarter = null;
+ mGrantAdminView = null;
+ mEditUserInfoView = null;
+ mUserNameView = null;
+ mSuccessCallback = null;
+ mCurrentState = INITIAL_DIALOG;
+ }
+
+ /**
+ * Notifies that the containing activity or fragment was reinitialized.
+ */
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ String pendingPhoto = savedInstanceState.getString(KEY_SAVED_PHOTO);
+ if (pendingPhoto != null) {
+ ThreadUtils.postOnBackgroundThread(() -> {
+ mSavedPhoto = EditUserPhotoController.loadNewUserPhotoBitmap(
+ new File(pendingPhoto));
+ });
+ }
+ mCurrentState = savedInstanceState.getInt(KEY_CURRENT_STATE);
+ if (savedInstanceState.containsKey(KEY_IS_ADMIN)) {
+ mIsAdmin = savedInstanceState.getBoolean(KEY_IS_ADMIN);
+ }
+ mSavedName = savedInstanceState.getString(KEY_SAVED_NAME);
+ mWaitingForActivityResult = savedInstanceState.getBoolean(KEY_AWAITING_RESULT, false);
+ }
+
+ /**
+ * Notifies that the containing activity or fragment is saving its state for later use.
+ */
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ if (mUserCreationDialog != null && mEditUserPhotoController != null) {
+ // Bitmap cannot be stored into bundle because it may exceed parcel limit
+ // Store it in a temporary file instead
+ ThreadUtils.postOnBackgroundThread(() -> {
+ File file = mEditUserPhotoController.saveNewUserPhotoBitmap();
+ if (file != null) {
+ savedInstanceState.putString(KEY_SAVED_PHOTO, file.getPath());
+ }
+ });
+ }
+ if (mIsAdmin != null) {
+ savedInstanceState.putBoolean(KEY_IS_ADMIN, Boolean.TRUE.equals(mIsAdmin));
+ }
+ savedInstanceState.putString(KEY_SAVED_NAME, mUserNameView.getText().toString().trim());
+ savedInstanceState.putInt(KEY_CURRENT_STATE, mCurrentState);
+ savedInstanceState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult);
+ }
+
+ /**
+ * Notifies that an activity has started.
+ */
+ public void startingActivityForResult() {
+ mWaitingForActivityResult = true;
+ }
+
+ /**
+ * Notifies that the result from activity has been received.
+ */
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ mWaitingForActivityResult = false;
+ if (mEditUserPhotoController != null) {
+ mEditUserPhotoController.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ /**
+ * Creates an add user dialog with option to set the user's name and photo and choose their
+ * admin status.
+ */
+ public Dialog createDialog(Activity activity,
+ ActivityStarter activityStarter, boolean isMultipleAdminEnabled,
+ NewUserData successCallback, Runnable cancelCallback) {
+ mActivity = activity;
+ mCustomDialogHelper = new CustomDialogHelper(activity);
+ mSuccessCallback = successCallback;
+ mActivityStarter = activityStarter;
+ addCustomViews(isMultipleAdminEnabled);
+ mUserCreationDialog = mCustomDialogHelper.getDialog();
+ updateLayout();
+ mUserCreationDialog.setOnDismissListener(view -> {
+ cancelCallback.run();
+ clear();
+ });
+ mUserCreationDialog.setCanceledOnTouchOutside(true);
+ return mUserCreationDialog;
+ }
+
+ private void addCustomViews(boolean isMultipleAdminEnabled) {
+ addGrantAdminView();
+ addUserInfoEditView();
+ mCustomDialogHelper.setPositiveButton(R.string.next, view -> {
+ mCurrentState++;
+ if (mCurrentState == GRANT_ADMIN_DIALOG && !isMultipleAdminEnabled) {
+ mCurrentState++;
+ }
+ updateLayout();
+ });
+ mCustomDialogHelper.setNegativeButton(R.string.back, view -> {
+ mCurrentState--;
+ if (mCurrentState == GRANT_ADMIN_DIALOG && !isMultipleAdminEnabled) {
+ mCurrentState--;
+ }
+ updateLayout();
+ });
+ return;
+ }
+
+ private void updateLayout() {
+ switch (mCurrentState) {
+ case INITIAL_DIALOG:
+ mEditUserInfoView.setVisibility(View.GONE);
+ mGrantAdminView.setVisibility(View.GONE);
+ final SharedPreferences preferences = mActivity.getPreferences(
+ Context.MODE_PRIVATE);
+ final boolean longMessageDisplayed = preferences.getBoolean(
+ KEY_ADD_USER_LONG_MESSAGE_DISPLAYED, false);
+ final int messageResId = longMessageDisplayed
+ ? R.string.user_add_user_message_short
+ : R.string.user_add_user_message_long;
+ if (!longMessageDisplayed) {
+ preferences.edit().putBoolean(
+ KEY_ADD_USER_LONG_MESSAGE_DISPLAYED,
+ true).apply();
+ }
+ Drawable icon = mActivity.getDrawable(R.drawable.ic_person_add);
+ mCustomDialogHelper.setVisibility(mCustomDialogHelper.ICON, true)
+ .setVisibility(mCustomDialogHelper.TITLE, true)
+ .setVisibility(mCustomDialogHelper.MESSAGE, true)
+ .setIcon(icon)
+ .setButtonEnabled(true)
+ .setTitle(R.string.user_add_user_title)
+ .setMessage(messageResId)
+ .setNegativeButtonText(R.string.cancel)
+ .setPositiveButtonText(R.string.next);
+ break;
+ case GRANT_ADMIN_DIALOG:
+ mEditUserInfoView.setVisibility(View.GONE);
+ mGrantAdminView.setVisibility(View.VISIBLE);
+ mCustomDialogHelper
+ .setVisibility(mCustomDialogHelper.ICON, true)
+ .setVisibility(mCustomDialogHelper.TITLE, true)
+ .setVisibility(mCustomDialogHelper.MESSAGE, true)
+ .setIcon(mActivity.getDrawable(R.drawable.ic_admin_panel_settings))
+ .setTitle(R.string.user_grant_admin_title)
+ .setMessage(R.string.user_grant_admin_message)
+ .setNegativeButtonText(R.string.back)
+ .setPositiveButtonText(R.string.next);
+ if (mIsAdmin == null) {
+ mCustomDialogHelper.setButtonEnabled(false);
+ }
+ break;
+ case EDIT_NAME_DIALOG:
+ mCustomDialogHelper
+ .setVisibility(mCustomDialogHelper.ICON, false)
+ .setVisibility(mCustomDialogHelper.TITLE, false)
+ .setVisibility(mCustomDialogHelper.MESSAGE, false)
+ .setNegativeButtonText(R.string.back)
+ .setPositiveButtonText(R.string.done);
+ mEditUserInfoView.setVisibility(View.VISIBLE);
+ mGrantAdminView.setVisibility(View.GONE);
+ break;
+ case CREATE_USER_AND_CLOSE:
+ Drawable newUserIcon = mEditUserPhotoController != null
+ ? mEditUserPhotoController.getNewUserPhotoDrawable()
+ : null;
+
+ String newName = mUserNameView.getText().toString().trim();
+ String defaultName = mActivity.getString(R.string.user_new_user_name);
+ String userName = !newName.isEmpty() ? newName : defaultName;
+
+ if (mSuccessCallback != null) {
+ mSuccessCallback.onSuccess(userName, newUserIcon,
+ Boolean.TRUE.equals(mIsAdmin));
+ }
+ mCustomDialogHelper.getDialog().dismiss();
+ clear();
+ break;
+ case EXIT_DIALOG:
+ mCustomDialogHelper.getDialog().dismiss();
+ break;
+ default:
+ if (mCurrentState < EXIT_DIALOG) {
+ mCurrentState = EXIT_DIALOG;
+ updateLayout();
+ } else {
+ mCurrentState = CREATE_USER_AND_CLOSE;
+ updateLayout();
+ }
+ break;
+ }
+ }
+
+ private Drawable getUserIcon(Drawable defaultUserIcon) {
+ if (mSavedPhoto != null) {
+ mSavedDrawable = CircleFramedDrawable.getInstance(mActivity, mSavedPhoto);
+ return mSavedDrawable;
+ }
+ return defaultUserIcon;
+ }
+
+ private void addUserInfoEditView() {
+ mEditUserInfoView = View.inflate(mActivity, R.layout.edit_user_info_dialog_content, null);
+ mCustomDialogHelper.addCustomView(mEditUserInfoView);
+ setUserName();
+ ImageView userPhotoView = mEditUserInfoView.findViewById(R.id.user_photo);
+
+ // if oldUserIcon param is null then we use a default gray user icon
+ Drawable defaultUserIcon = UserIcons.getDefaultUserIcon(
+ mActivity.getResources(), UserHandle.USER_NULL, false);
+ // in case a new photo was selected and the activity got recreated we have to load the image
+ Drawable userIcon = getUserIcon(defaultUserIcon);
+ userPhotoView.setImageDrawable(userIcon);
+
+ if (isChangePhotoRestrictedByBase(mActivity)) {
+ // some users can't change their photos so we need to remove the suggestive icon
+ mEditUserInfoView.findViewById(R.id.add_a_photo_icon).setVisibility(View.GONE);
+ } else {
+ RestrictedLockUtils.EnforcedAdmin adminRestriction =
+ getChangePhotoAdminRestriction(mActivity);
+ if (adminRestriction != null) {
+ userPhotoView.setOnClickListener(view ->
+ RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
+ mActivity, adminRestriction));
+ } else {
+ mEditUserPhotoController = createEditUserPhotoController(userPhotoView);
+ }
+ }
+ }
+
+ private void setUserName() {
+ mUserNameView = mEditUserInfoView.findViewById(R.id.user_name);
+ if (mSavedName == null) {
+ mUserNameView.setText(R.string.user_new_user_name);
+ } else {
+ mUserNameView.setText(mSavedName);
+ }
+ }
+
+ private void addGrantAdminView() {
+ mGrantAdminView = View.inflate(mActivity, R.layout.grant_admin_dialog_content, null);
+ mCustomDialogHelper.addCustomView(mGrantAdminView);
+ RadioGroup radioGroup = mGrantAdminView.findViewById(R.id.choose_admin);
+ radioGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ mCustomDialogHelper.setButtonEnabled(true);
+ mIsAdmin = checkedId == R.id.grant_admin_yes;
+ }
+ );
+ if (Boolean.TRUE.equals(mIsAdmin)) {
+ RadioButton button = radioGroup.findViewById(R.id.grant_admin_yes);
+ button.setChecked(true);
+ } else if (Boolean.FALSE.equals(mIsAdmin)) {
+ RadioButton button = radioGroup.findViewById(R.id.grant_admin_no);
+ button.setChecked(true);
+ }
+ }
+
+ @VisibleForTesting
+ boolean isChangePhotoRestrictedByBase(Context context) {
+ return RestrictedLockUtilsInternal.hasBaseUserRestriction(
+ context, UserManager.DISALLOW_SET_USER_ICON, UserHandle.myUserId());
+ }
+
+ @VisibleForTesting
+ RestrictedLockUtils.EnforcedAdmin getChangePhotoAdminRestriction(Context context) {
+ return RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
+ context, UserManager.DISALLOW_SET_USER_ICON, UserHandle.myUserId());
+ }
+
+ @VisibleForTesting
+ EditUserPhotoController createEditUserPhotoController(ImageView userPhotoView) {
+ return new EditUserPhotoController(mActivity, mActivityStarter, userPhotoView,
+ mSavedPhoto, mSavedDrawable, mFileAuthority);
+ }
+
+ public boolean isActive() {
+ return mCustomDialogHelper != null && mCustomDialogHelper.getDialog() != null;
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java b/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java
new file mode 100644
index 0000000000000..3d18b59258b3d
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 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.graphics.drawable.Drawable;
+
+/**
+ * Defines a callback when a new user data is filled out.
+ */
+public interface NewUserData {
+
+ /**
+ * Consumes data relevant to new user that needs to be created.
+ * @param userName New user name.
+ * @param userImage New user icon.
+ * @param isNewUserAdmin A boolean that indicated whether new user has admin status.
+ */
+ void onSuccess(String userName, Drawable userImage, Boolean isNewUserAdmin);
+
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java
new file mode 100644
index 0000000000000..e989ed27508bd
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2023 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.RadioButton;
+
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.settingslib.R;
+import com.android.settingslib.RestrictedLockUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+
+@RunWith(RobolectricTestRunner.class)
+public class CreateUserDialogControllerTest {
+
+ @Mock
+ private ActivityStarter mActivityStarter;
+
+ private boolean mPhotoRestrictedByBase;
+ private Activity mActivity;
+ private TestCreateUserDialogController mUnderTest;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mActivity = spy(ActivityController.of(new FragmentActivity()).get());
+ mActivity.setTheme(R.style.Theme_AppCompat_DayNight);
+ mUnderTest = new TestCreateUserDialogController();
+ mPhotoRestrictedByBase = false;
+ }
+
+ @Test
+ public void positiveButton_grantAdminStage_noValue_OkButtonShouldBeDisabled() {
+ Runnable cancelCallback = mock(Runnable.class);
+
+ final AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity,
+ mActivityStarter, true, null,
+ cancelCallback);
+ dialog.show();
+ assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true);
+ Button next = dialog.findViewById(R.id.button_ok);
+ next.performClick();
+ assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(false);
+ ((RadioButton) dialog.findViewById(R.id.grant_admin_yes)).setChecked(true);
+ assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true);
+ dialog.dismiss();
+ }
+
+ @Test
+ public void positiveButton_MultipleAdminDisabled_shouldSkipGrantAdminStage() {
+ Runnable cancelCallback = mock(Runnable.class);
+
+ final AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity,
+ mActivityStarter, false, null,
+ cancelCallback);
+ dialog.show();
+ assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE);
+ assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true);
+ Button next = dialog.findViewById(R.id.button_ok);
+ next.performClick();
+ assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE);
+ assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true);
+ Button back = dialog.findViewById(R.id.button_cancel);
+ back.performClick();
+ assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE);
+ dialog.dismiss();
+ }
+
+ @Test
+ public void editUserInfoController_shouldOnlyBeVisibleOnLastStage() {
+ Runnable cancelCallback = mock(Runnable.class);
+ final AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity,
+ mActivityStarter, true, null,
+ cancelCallback);
+ dialog.show();
+ assertThat(dialog.findViewById(R.id.user_info_scroll).getVisibility()).isEqualTo(View.GONE);
+ Button next = dialog.findViewById(R.id.button_ok);
+ next.performClick();
+ ((RadioButton) dialog.findViewById(R.id.grant_admin_yes)).setChecked(true);
+ assertThat(dialog.findViewById(R.id.user_info_scroll).getVisibility()).isEqualTo(View.GONE);
+ next.performClick();
+ assertThat(dialog.findViewById(R.id.user_info_scroll).getVisibility())
+ .isEqualTo(View.VISIBLE);
+ dialog.dismiss();
+ }
+
+ @Test
+ public void positiveButton_MultipleAdminEnabled_shouldShowGrantAdminStage() {
+ Runnable cancelCallback = mock(Runnable.class);
+
+ final AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity,
+ mActivityStarter, true, null,
+ cancelCallback);
+ dialog.show();
+ assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE);
+ assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true);
+ Button next = dialog.findViewById(R.id.button_ok);
+ next.performClick();
+ assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility())
+ .isEqualTo(View.VISIBLE);
+ ((RadioButton) dialog.findViewById(R.id.grant_admin_yes)).setChecked(true);
+ next.performClick();
+ assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE);
+ dialog.dismiss();
+ }
+
+ @Test
+ public void cancelCallback_isCalled_whenCancelled() {
+ NewUserData successCallback = mock(NewUserData.class);
+ Runnable cancelCallback = mock(Runnable.class);
+
+ AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity,
+ mActivityStarter, true, successCallback,
+ cancelCallback);
+ dialog.show();
+ dialog.cancel();
+ verifyNoInteractions(successCallback);
+ verify(cancelCallback, times(1))
+ .run();
+ }
+
+ @Test
+ public void cancelCallback_isCalled_whenNegativeButtonClickedOnFirstStage() {
+ NewUserData successCallback = mock(NewUserData.class);
+ Runnable cancelCallback = mock(Runnable.class);
+
+ AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity,
+ mActivityStarter, true, successCallback,
+ cancelCallback);
+ dialog.show();
+ Button back = dialog.findViewById(R.id.button_cancel);
+ back.performClick();
+ verifyNoInteractions(successCallback);
+ verify(cancelCallback, times(1))
+ .run();
+ }
+
+ @Test
+ public void cancelCallback_isNotCalled_whenNegativeButtonClickedOnSecondStage() {
+ NewUserData successCallback = mock(NewUserData.class);
+ Runnable cancelCallback = mock(Runnable.class);
+
+ AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity,
+ mActivityStarter, true, successCallback,
+ cancelCallback);
+ dialog.show();
+ Button next = dialog.findViewById(R.id.button_ok);
+ next.performClick();
+ Button back = dialog.findViewById(R.id.button_cancel);
+ back.performClick();
+ verifyNoInteractions(successCallback);
+ verifyNoInteractions(cancelCallback);
+ dialog.dismiss();
+ }
+
+ @Test
+ public void successCallback_isCalled_setNameAndAdminStatus() {
+ NewUserData successCallback = mock(NewUserData.class);
+ Runnable cancelCallback = mock(Runnable.class);
+
+ AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity,
+ mActivityStarter, true, successCallback,
+ cancelCallback);
+ // No photo chosen
+ when(mUnderTest.getPhotoController().getNewUserPhotoDrawable()).thenReturn(null);
+ dialog.show();
+ Button next = dialog.findViewById(R.id.button_ok);
+ next.performClick();
+ ((RadioButton) dialog.findViewById(R.id.grant_admin_yes)).setChecked(true);
+ next.performClick();
+ String expectedNewName = "Test";
+ EditText editText = dialog.findViewById(R.id.user_name);
+ editText.setText(expectedNewName);
+ next.performClick();
+ verify(successCallback, times(1))
+ .onSuccess(expectedNewName, null, true);
+ }
+
+ @Test
+ public void successCallback_isCalled_setName_MultipleAdminDisabled() {
+ NewUserData successCallback = mock(NewUserData.class);
+ Runnable cancelCallback = mock(Runnable.class);
+
+ AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity,
+ mActivityStarter, false, successCallback,
+ cancelCallback);
+ // No photo chosen
+ when(mUnderTest.getPhotoController().getNewUserPhotoDrawable()).thenReturn(null);
+ dialog.show();
+ Button next = dialog.findViewById(R.id.button_ok);
+ next.performClick();
+ String expectedNewName = "Test";
+ EditText editText = dialog.findViewById(R.id.user_name);
+ editText.setText(expectedNewName);
+ next.performClick();
+ verify(successCallback, times(1))
+ .onSuccess(expectedNewName, null, false);
+ }
+
+ private class TestCreateUserDialogController extends CreateUserDialogController {
+ private EditUserPhotoController mPhotoController;
+
+ TestCreateUserDialogController() {
+ super("file_authority");
+ }
+
+ private EditUserPhotoController getPhotoController() {
+ return mPhotoController;
+ }
+
+ @Override
+ EditUserPhotoController createEditUserPhotoController(ImageView userPhotoView) {
+ mPhotoController = mock(EditUserPhotoController.class, Answers.RETURNS_DEEP_STUBS);
+ return mPhotoController;
+ }
+ @Override
+ RestrictedLockUtils.EnforcedAdmin getChangePhotoAdminRestriction(Context context) {
+ return null;
+ }
+
+ @Override
+ boolean isChangePhotoRestrictedByBase(Context context) {
+ return mPhotoRestrictedByBase;
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
index 08dbb816b5e94..08b0c647628ca 100644
--- a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
@@ -32,11 +32,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.logging.UiEventLogger;
-import com.android.settingslib.users.EditUserInfoController;
-import com.android.settingslib.users.GrantAdminDialogController;
+import com.android.settingslib.users.CreateUserDialogController;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.user.utils.MultiUserActionsEvent;
import javax.inject.Inject;
@@ -61,20 +59,18 @@ public class CreateUserActivity extends Activity {
private static final String EXTRA_IS_KEYGUARD_SHOWING = "extra_is_keyguard_showing";
private final UserCreator mUserCreator;
- private final EditUserInfoController mEditUserInfoController;
+ private CreateUserDialogController mCreateUserDialogController;
private final IActivityManager mActivityManager;
private final ActivityStarter mActivityStarter;
private final UiEventLogger mUiEventLogger;
- private Dialog mGrantAdminDialog;
private Dialog mSetupUserDialog;
private final OnBackInvokedCallback mBackCallback = this::onBackInvoked;
- private boolean mGrantAdminRights;
@Inject
public CreateUserActivity(UserCreator userCreator,
- EditUserInfoController editUserInfoController, IActivityManager activityManager,
+ CreateUserDialogController createUserDialogController, IActivityManager activityManager,
ActivityStarter activityStarter, UiEventLogger uiEventLogger) {
mUserCreator = userCreator;
- mEditUserInfoController = editUserInfoController;
+ mCreateUserDialogController = createUserDialogController;
mActivityManager = activityManager;
mActivityStarter = activityStarter;
mUiEventLogger = uiEventLogger;
@@ -86,19 +82,10 @@ public class CreateUserActivity extends Activity {
setShowWhenLocked(true);
setContentView(R.layout.activity_create_new_user);
if (savedInstanceState != null) {
- mEditUserInfoController.onRestoreInstanceState(savedInstanceState);
- }
- boolean isKeyguardShowing = getIntent().getBooleanExtra(EXTRA_IS_KEYGUARD_SHOWING, true);
- // Display grant admin dialog only on unlocked device to admin users if multiple admins
- // are allowed on this device.
- if (mUserCreator.isMultipleAdminEnabled() && mUserCreator.isUserAdmin()
- && !isKeyguardShowing) {
- mGrantAdminDialog = buildGrantAdminDialog();
- mGrantAdminDialog.show();
- } else {
- mSetupUserDialog = createDialog();
- mSetupUserDialog.show();
+ mCreateUserDialogController.onRestoreInstanceState(savedInstanceState);
}
+ mSetupUserDialog = createDialog();
+ mSetupUserDialog.show();
getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
mBackCallback);
@@ -110,7 +97,7 @@ public class CreateUserActivity extends Activity {
outState.putBundle(DIALOG_STATE_KEY, mSetupUserDialog.onSaveInstanceState());
}
- mEditUserInfoController.onSaveInstanceState(outState);
+ mCreateUserDialogController.onSaveInstanceState(outState);
super.onSaveInstanceState(outState);
}
@@ -125,48 +112,21 @@ public class CreateUserActivity extends Activity {
private Dialog createDialog() {
String defaultUserName = getString(com.android.settingslib.R.string.user_new_user_name);
-
- return mEditUserInfoController.createDialog(
+ boolean isKeyguardShowing = getIntent().getBooleanExtra(EXTRA_IS_KEYGUARD_SHOWING, true);
+ return mCreateUserDialogController.createDialog(
this,
this::startActivity,
- null,
- defaultUserName,
- getString(com.android.settingslib.R.string.user_add_user),
+ (mUserCreator.isMultipleAdminEnabled() && mUserCreator.isUserAdmin()
+ && !isKeyguardShowing),
this::addUserNow,
this::finish
);
}
- /**
- * Returns dialog that allows to grant user admin rights.
- */
- private Dialog buildGrantAdminDialog() {
- return new GrantAdminDialogController().createDialog(
- this,
- (grantAdminRights) -> {
- mGrantAdminDialog.dismiss();
- mGrantAdminRights = grantAdminRights;
- if (mGrantAdminRights) {
- mUiEventLogger.log(MultiUserActionsEvent
- .GRANT_ADMIN_FROM_USER_SWITCHER_CREATION_DIALOG);
- } else {
- mUiEventLogger.log(MultiUserActionsEvent
- .NOT_GRANT_ADMIN_FROM_USER_SWITCHER_CREATION_DIALOG);
- }
- mSetupUserDialog = createDialog();
- mSetupUserDialog.show();
- },
- () -> {
- mGrantAdminRights = false;
- finish();
- }
- );
- }
-
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
- mEditUserInfoController.onActivityResult(requestCode, resultCode, data);
+ mCreateUserDialogController.onActivityResult(requestCode, resultCode, data);
}
@Override
@@ -178,9 +138,6 @@ public class CreateUserActivity extends Activity {
if (mSetupUserDialog != null) {
mSetupUserDialog.dismiss();
}
- if (mGrantAdminDialog != null) {
- mGrantAdminDialog.dismiss();
- }
finish();
}
@@ -190,7 +147,7 @@ public class CreateUserActivity extends Activity {
super.onDestroy();
}
- private void addUserNow(String userName, Drawable userIcon) {
+ private void addUserNow(String userName, Drawable userIcon, Boolean isAdmin) {
mSetupUserDialog.dismiss();
userName = (userName == null || userName.trim().isEmpty())
? getString(com.android.settingslib.R.string.user_new_user_name)
@@ -198,7 +155,7 @@ public class CreateUserActivity extends Activity {
mUserCreator.createUser(userName, userIcon,
userInfo -> {
- if (mGrantAdminRights) {
+ if (isAdmin) {
mUserCreator.setUserAdmin(userInfo.id);
}
switchToUser(userInfo.id);
@@ -230,7 +187,7 @@ public class CreateUserActivity extends Activity {
*/
private void startActivity(Intent intent, int requestCode) {
mActivityStarter.dismissKeyguardThenExecute(() -> {
- mEditUserInfoController.startingActivityForResult();
+ mCreateUserDialogController.startingActivityForResult();
startActivityForResult(intent, requestCode);
return true;
}, /* cancel= */ null, /* afterKeyguardGone= */ true);
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
index b2bf9727b5347..d8ee686ea60f1 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java
+++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
@@ -18,6 +18,7 @@ package com.android.systemui.user;
import android.os.UserHandle;
+import com.android.settingslib.users.CreateUserDialogController;
import com.android.settingslib.users.EditUserInfoController;
import com.android.systemui.user.data.repository.UserRepositoryModule;
import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule;
@@ -45,6 +46,12 @@ public abstract class UserModule {
return new EditUserInfoController(FILE_PROVIDER_AUTHORITY);
}
+ /** Provides {@link CreateUserDialogController} */
+ @Provides
+ public static CreateUserDialogController provideCreateUserDialogController() {
+ return new CreateUserDialogController(FILE_PROVIDER_AUTHORITY);
+ }
+
/**
* Provides the {@link UserHandle} for the user associated with this System UI process.
*
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index c2922c4d6f34b..27c348ba329a8 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -50,6 +50,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
+import com.android.systemui.user.CreateUserActivity
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.user.data.source.UserRecord
@@ -455,13 +456,16 @@ constructor(
UserActionModel.ADD_USER -> {
uiEventLogger.log(MultiUserActionsEvent.CREATE_USER_FROM_USER_SWITCHER)
val currentUser = repository.getSelectedUserInfo()
- showDialog(
- ShowDialogRequestModel.ShowAddUserDialog(
- userHandle = currentUser.userHandle,
- isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
- showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral,
- dialogShower = dialogShower,
- )
+ dismissDialog()
+ activityStarter.startActivity(
+ CreateUserActivity.createIntentForStart(
+ applicationContext,
+ keyguardInteractor.isKeyguardShowing()
+ ),
+ /* dismissShade= */ true,
+ /* animationController */ null,
+ /* showOverLockscreenWhenLocked */ true,
+ /* userHandle */ currentUser.getUserHandle(),
)
}
UserActionModel.ADD_SUPERVISED_USER -> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt
index 2b86cfdec04e7..6db35ae94a3f1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt
@@ -6,6 +6,7 @@ import android.testing.TestableLooper
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.mockito.whenever
@@ -27,9 +28,7 @@ class CreateUserActivityTest : SysuiTestCase() {
createDialog(
/* activity = */ nullable(),
/* activityStarter = */ nullable(),
- /* oldUserIcon = */ nullable(),
- /* defaultUserName = */ nullable(),
- /* title = */ nullable(),
+ /* isMultipleAdminsEnabled = */ any(),
/* successCallback = */ nullable(),
/* cancelCallback = */ nullable()
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
index d252d5317d066..ca83d49b19cad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
@@ -482,30 +482,17 @@ class UserInteractorTest : SysuiTestCase() {
}
@Test
- fun executeAction_addUser_dialogShown() =
+ fun executeAction_addUser_dismissesDialogAndStartsActivity() =
testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
keyguardRepository.setKeyguardShowing(false)
- val dialogRequest = collectLastValue(underTest.dialogShowRequests)
- val dialogShower: UserSwitchDialogController.DialogShower = mock()
- underTest.executeAction(UserActionModel.ADD_USER, dialogShower)
+ underTest.executeAction(UserActionModel.ADD_USER)
verify(uiEventLogger, times(1))
.log(MultiUserActionsEvent.CREATE_USER_FROM_USER_SWITCHER)
- assertThat(dialogRequest())
- .isEqualTo(
- ShowDialogRequestModel.ShowAddUserDialog(
- userHandle = userInfos[0].userHandle,
- isKeyguardShowing = false,
- showEphemeralMessage = false,
- dialogShower = dialogShower,
- )
- )
-
underTest.onDialogShown()
- assertThat(dialogRequest()).isNull()
}
@Test
@@ -862,7 +849,7 @@ class UserInteractorTest : SysuiTestCase() {
// Dialog is shown.
assertThat(dialogRequest())
- .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable))
+ .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable))
underTest.onDialogShown()
assertThat(dialogRequest()).isNull()