| /* |
| * 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; |
| } |
| } |