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 e9aded0838d94..a81432b06d14f 100644
--- a/packages/SettingsLib/res/values/dimens.xml
+++ b/packages/SettingsLib/res/values/dimens.xml
@@ -110,8 +110,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/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.
*