Create single dialog for adding a user.

Bug: 265431885
Test: atest CreateUserDialogControllerTest
Change-Id: I8829725e856024a984c192a5444f402e32e792d9
This commit is contained in:
Tetiana Meronyk
2023-04-20 09:49:45 +00:00
parent 0f74f7d7d2
commit 709264ad6a
9 changed files with 811 additions and 80 deletions

View File

@@ -0,0 +1,25 @@
<!--
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M18,14V11H15V9H18V6H20V9H23V11H20V14ZM9,12Q7.35,12 6.175,10.825Q5,9.65 5,8Q5,6.35 6.175,5.175Q7.35,4 9,4Q10.65,4 11.825,5.175Q13,6.35 13,8Q13,9.65 11.825,10.825Q10.65,12 9,12ZM1,20V17.2Q1,16.35 1.438,15.637Q1.875,14.925 2.6,14.55Q4.15,13.775 5.75,13.387Q7.35,13 9,13Q10.65,13 12.25,13.387Q13.85,13.775 15.4,14.55Q16.125,14.925 16.562,15.637Q17,16.35 17,17.2V20ZM3,18H15V17.2Q15,16.925 14.863,16.7Q14.725,16.475 14.5,16.35Q13.15,15.675 11.775,15.337Q10.4,15 9,15Q7.6,15 6.225,15.337Q4.85,15.675 3.5,16.35Q3.275,16.475 3.138,16.7Q3,16.925 3,17.2ZM9,10Q9.825,10 10.413,9.412Q11,8.825 11,8Q11,7.175 10.413,6.588Q9.825,6 9,6Q8.175,6 7.588,6.588Q7,7.175 7,8Q7,8.825 7.588,9.412Q8.175,10 9,10ZM9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8ZM9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Z"/>
</vector>

View File

@@ -13,87 +13,88 @@
~ 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:gravity="center"
android:padding="@dimen/grant_admin_dialog_padding"
android:paddingBottom="0dp">
<ImageView
android:id="@+id/dialog_with_icon_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription=""/>
<TextView
android:id="@+id/dialog_with_icon_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="@style/DialogWithIconTitle"
android:text="@string/user_grant_admin_title"
android:textDirection="locale"/>
<TextView
android:id="@+id/dialog_with_icon_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:gravity="center"
style="@style/TextAppearanceSmall"
android:text=""
android:textDirection="locale"/>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/new_user_dialog_id">
<LinearLayout
android:id="@+id/custom_layout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingBottom="0dp">
</LinearLayout>
<LinearLayout
android:id="@+id/button_panel"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:paddingBottom="0dp">
<Button
android:id="@+id/button_cancel"
style="@style/DialogButtonNegative"
android:layout_width="wrap_content"
android:buttonCornerRadius="28dp"
android:layout_height="wrap_content"
android:visibility="gone"/>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" >
</Space>
<Button
android:id="@+id/button_back"
android:padding="@dimen/dialog_content_padding">
<ImageView
android:id="@+id/dialog_with_icon_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/DialogButtonNegative"
android:buttonCornerRadius="40dp"
android:clickable="true"
android:focusable="true"
android:text="Back"
android:visibility="gone"
/>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="0.05"
>
</Space>
<Button
android:id="@+id/button_ok"
android:importantForAccessibility="no"/>
<TextView
android:id="@+id/dialog_with_icon_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/DialogButtonPositive"
android:clickable="true"
android:focusable="true"
android:visibility="gone"
/>
android:gravity="center"
style="@style/DialogWithIconTitle"/>
<TextView
android:id="@+id/dialog_with_icon_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:gravity="center"
style="@style/TextAppearanceSmall"/>
<LinearLayout
android:id="@+id/custom_layout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
</LinearLayout>
<LinearLayout
android:id="@+id/button_panel"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<Button
android:id="@+id/button_cancel"
style="@style/DialogButtonNegative"
android:layout_width="wrap_content"
android:buttonCornerRadius="28dp"
android:layout_height="wrap_content"
android:visibility="gone"/>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1">
</Space>
<Button
android:id="@+id/button_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/DialogButtonNegative"
android:buttonCornerRadius="40dp"
android:visibility="gone"/>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="0.05">
</Space>
<Button
android:id="@+id/button_ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/DialogButtonPositive"
android:visibility="gone"
/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -28,7 +28,8 @@
android:orientation="vertical">
<TextView
android:id="@+id/user_info_title"
android:layout_width="wrap_content"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/EditUserDialogTitle"
android:text="@string/user_info_settings_title"

View File

@@ -14,10 +14,14 @@
~ limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/grant_admin_view"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/grant_admin_dialog_padding">
android:paddingLeft="@dimen/dialog_content_padding"
android:paddingBottom="@dimen/dialog_content_padding"
android:paddingRight="@dimen/dialog_content_padding">
<RadioGroup
android:id="@+id/choose_admin"
android:layout_width="match_parent"
@@ -25,13 +29,19 @@
android:orientation="vertical">
<RadioButton
android:id="@+id/grant_admin_yes"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/radio_button_padding"
android:paddingBottom="@dimen/radio_button_padding"
android:paddingStart="@dimen/radio_button_padding_start"
android:text="@string/grant_admin"/>
<RadioButton
android:id="@+id/grant_admin_no"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/radio_button_padding"
android:paddingBottom="@dimen/radio_button_padding"
android:paddingStart="@dimen/radio_button_padding_start"
android:text="@string/not_grant_admin"/>
</RadioGroup>
</LinearLayout>

View File

@@ -110,8 +110,9 @@
<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>
<dimen name="dialog_content_padding">16dp</dimen>
<dimen name="radio_button_padding">12dp</dimen>
<dimen name="radio_button_padding_start">8dp</dimen>
<dimen name="dialog_button_horizontal_padding">16dp</dimen>
<dimen name="dialog_button_vertical_padding">8dp</dimen>

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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.
*