diff --git a/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml b/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml
new file mode 100644
index 0000000000000..f66ff007fb900
--- /dev/null
+++ b/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
diff --git a/packages/SettingsLib/res/layout/restricted_popup_menu_item.xml b/packages/SettingsLib/res/layout/restricted_popup_menu_item.xml
new file mode 100644
index 0000000000000..923d022aea686
--- /dev/null
+++ b/packages/SettingsLib/res/layout/restricted_popup_menu_item.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/SettingsLib/res/layout/user_creation_progress_dialog.xml b/packages/SettingsLib/res/layout/user_creation_progress_dialog.xml
new file mode 100644
index 0000000000000..fe09aaca0b1f2
--- /dev/null
+++ b/packages/SettingsLib/res/layout/user_creation_progress_dialog.xml
@@ -0,0 +1,27 @@
+
+
+
+
diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml
index e552d78e1a453..ef4b97f7743fa 100644
--- a/packages/SettingsLib/res/values/dimens.xml
+++ b/packages/SettingsLib/res/values/dimens.xml
@@ -97,4 +97,8 @@
18dp
+
+
+ 300dp
+
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 03161d0513429..6a4c8c301cbe4 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1354,6 +1354,11 @@
Set lock
Switch to %s
+
+ Creating new user…
+
+
+ Nickname
Add guest
@@ -1362,6 +1367,13 @@
Guest
+
+ Take a photo
+
+ Choose an image
+
+ Select photo
+
Device default
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/ActivityStarter.java b/packages/SettingsLib/src/com/android/settingslib/users/ActivityStarter.java
new file mode 100644
index 0000000000000..081b5075ebbed
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/users/ActivityStarter.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 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.content.Intent;
+
+/**
+ * An interface to start activities for result. This is used as a callback from controllers where
+ * activity starting isn't possible but we want to keep the intent building logic there.
+ */
+public interface ActivityStarter {
+
+ /**
+ * Launch an activity for which you would like a result when it finished.
+ */
+ void startActivityForResult(Intent intent, int requestCode);
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java
new file mode 100644
index 0000000000000..58599532d9cb7
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.users;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.EditText;
+import android.widget.ImageView;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.util.UserIcons;
+import com.android.settingslib.R;
+import com.android.settingslib.drawable.CircleFramedDrawable;
+
+import java.io.File;
+import java.util.function.BiConsumer;
+
+/**
+ * This class encapsulates a Dialog for editing the user nickname and photo.
+ */
+public class EditUserInfoController {
+
+ private static final String KEY_AWAITING_RESULT = "awaiting_result";
+ private static final String KEY_SAVED_PHOTO = "pending_photo";
+
+ private Dialog mEditUserInfoDialog;
+ private Bitmap mSavedPhoto;
+ private EditUserPhotoController mEditUserPhotoController;
+ private boolean mWaitingForActivityResult = false;
+ private final String mFileAuthority;
+
+ public EditUserInfoController(String fileAuthority) {
+ mFileAuthority = fileAuthority;
+ }
+
+ private void clear() {
+ if (mEditUserPhotoController != null) {
+ mEditUserPhotoController.removeNewUserPhotoBitmapFile();
+ }
+ mEditUserInfoDialog = null;
+ mSavedPhoto = null;
+ }
+
+ /**
+ * This should be called when the container activity/fragment got re-initialized from a
+ * previously saved state.
+ */
+ public void onRestoreInstanceState(Bundle icicle) {
+ String pendingPhoto = icicle.getString(KEY_SAVED_PHOTO);
+ if (pendingPhoto != null) {
+ mSavedPhoto = EditUserPhotoController.loadNewUserPhotoBitmap(new File(pendingPhoto));
+ }
+ mWaitingForActivityResult = icicle.getBoolean(KEY_AWAITING_RESULT, false);
+ }
+
+ /**
+ * Should be called from the container activity/fragment when it's onSaveInstanceState is
+ * called.
+ */
+ public void onSaveInstanceState(Bundle outState) {
+ if (mEditUserInfoDialog != null && mEditUserPhotoController != null) {
+ // Bitmap cannot be stored into bundle because it may exceed parcel limit
+ // Store it in a temporary file instead
+ File file = mEditUserPhotoController.saveNewUserPhotoBitmap();
+ if (file != null) {
+ outState.putString(KEY_SAVED_PHOTO, file.getPath());
+ }
+ }
+ outState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult);
+ }
+
+ /**
+ * Should be called from the container activity/fragment when an activity has started for
+ * take/choose/crop photo actions.
+ */
+ public void startingActivityForResult() {
+ mWaitingForActivityResult = true;
+ }
+
+ /**
+ * Should be called from the container activity/fragment after it receives a result from
+ * take/choose/crop photo activity.
+ */
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ mWaitingForActivityResult = false;
+
+ if (mEditUserPhotoController != null && mEditUserInfoDialog != null) {
+ mEditUserPhotoController.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ /**
+ * Creates a user edit dialog with option to change the user's name and photo.
+ *
+ * @param activityStarter - ActivityStarter is called with appropriate intents and request
+ * codes to take photo/choose photo/crop photo.
+ */
+ public Dialog createDialog(Activity activity, ActivityStarter activityStarter,
+ @Nullable Drawable oldUserIcon, String defaultUserName, String title,
+ BiConsumer successCallback, Runnable cancelCallback) {
+ LayoutInflater inflater = LayoutInflater.from(activity);
+ View content = inflater.inflate(R.layout.edit_user_info_dialog_content, null);
+
+ EditText userNameView = content.findViewById(R.id.user_name);
+ userNameView.setText(defaultUserName);
+
+ ImageView userPhotoView = content.findViewById(R.id.user_photo);
+
+ // if oldUserIcon param is null then we use a default gray user icon
+ Drawable defaultUserIcon = oldUserIcon != null ? oldUserIcon : UserIcons.getDefaultUserIcon(
+ activity.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(activity, defaultUserIcon);
+ userPhotoView.setImageDrawable(userIcon);
+
+ if (canChangePhoto(activity)) {
+ mEditUserPhotoController = createEditUserPhotoController(activity, activityStarter,
+ userPhotoView);
+ } else {
+ // some users can't change their photos so we need to remove suggestive
+ // background from the photoView
+ userPhotoView.setBackground(null);
+ }
+
+ mEditUserInfoDialog = buildDialog(activity, content, userNameView, oldUserIcon,
+ defaultUserName, title, successCallback, cancelCallback);
+
+ // Make sure the IME is up.
+ mEditUserInfoDialog.getWindow()
+ .setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+
+ return mEditUserInfoDialog;
+ }
+
+ private Drawable getUserIcon(Activity activity, Drawable defaultUserIcon) {
+ if (mSavedPhoto != null) {
+ return CircleFramedDrawable.getInstance(activity, mSavedPhoto);
+ }
+ return defaultUserIcon;
+ }
+
+ private Dialog buildDialog(Activity activity, View content, EditText userNameView,
+ @Nullable Drawable oldUserIcon, String defaultUserName, String title,
+ BiConsumer successCallback, Runnable cancelCallback) {
+ return new AlertDialog.Builder(activity)
+ .setTitle(title)
+ .setView(content)
+ .setCancelable(true)
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ Drawable newUserIcon = mEditUserPhotoController != null
+ ? mEditUserPhotoController.getNewUserPhotoDrawable()
+ : null;
+ Drawable userIcon = newUserIcon != null
+ ? newUserIcon
+ : oldUserIcon;
+
+ String newName = userNameView.getText().toString().trim();
+ String userName = !newName.isEmpty() ? newName : defaultUserName;
+
+ clear();
+ if (successCallback != null) {
+ successCallback.accept(userName, userIcon);
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, (dialog, which) -> {
+ clear();
+ if (cancelCallback != null) {
+ cancelCallback.run();
+ }
+ })
+ .setOnCancelListener(dialog -> {
+ clear();
+ if (cancelCallback != null) {
+ cancelCallback.run();
+ }
+ })
+ .create();
+ }
+
+ @VisibleForTesting
+ boolean canChangePhoto(Context context) {
+ return (PhotoCapabilityUtils.canCropPhoto(context)
+ && PhotoCapabilityUtils.canChoosePhoto(context))
+ || PhotoCapabilityUtils.canTakePhoto(context);
+ }
+
+ @VisibleForTesting
+ EditUserPhotoController createEditUserPhotoController(Activity activity,
+ ActivityStarter activityStarter, ImageView userPhotoView) {
+ return new EditUserPhotoController(activity, activityStarter, userPhotoView,
+ mSavedPhoto, mWaitingForActivityResult, mFileAuthority);
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java
new file mode 100644
index 0000000000000..ecd40667843e0
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java
@@ -0,0 +1,509 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.users;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.StrictMode;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.ContactsContract.DisplayPhoto;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListPopupWindow;
+import android.widget.TextView;
+
+import androidx.core.content.FileProvider;
+
+import com.android.settingslib.R;
+import com.android.settingslib.RestrictedLockUtils;
+import com.android.settingslib.RestrictedLockUtilsInternal;
+import com.android.settingslib.drawable.CircleFramedDrawable;
+
+import libcore.io.Streams;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class contains logic for starting activities to take/choose/crop photo, reads and transforms
+ * the result image.
+ */
+public class EditUserPhotoController {
+ private static final String TAG = "EditUserPhotoController";
+
+ // It seems that this class generates custom request codes and they may
+ // collide with ours, these values are very unlikely to have a conflict.
+ private static final int REQUEST_CODE_CHOOSE_PHOTO = 1001;
+ private static final int REQUEST_CODE_TAKE_PHOTO = 1002;
+ private static final int REQUEST_CODE_CROP_PHOTO = 1003;
+ // in rare cases we get a null Cursor when querying for DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI
+ // so we need a default photo size
+ private static final int DEFAULT_PHOTO_SIZE = 500;
+
+ private static final String IMAGES_DIR = "multi_user";
+ private static final String CROP_PICTURE_FILE_NAME = "CropEditUserPhoto.jpg";
+ private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto.jpg";
+ private static final String NEW_USER_PHOTO_FILE_NAME = "NewUserPhoto.png";
+
+ private final int mPhotoSize;
+
+ private final Activity mActivity;
+ private final ActivityStarter mActivityStarter;
+ private final ImageView mImageView;
+ private final String mFileAuthority;
+
+ private final File mImagesDir;
+ private final Uri mCropPictureUri;
+ private final Uri mTakePictureUri;
+
+ private Bitmap mNewUserPhotoBitmap;
+ private Drawable mNewUserPhotoDrawable;
+
+ public EditUserPhotoController(Activity activity, ActivityStarter activityStarter,
+ ImageView view, Bitmap bitmap, boolean waiting, String fileAuthority) {
+ mActivity = activity;
+ mActivityStarter = activityStarter;
+ mImageView = view;
+ mFileAuthority = fileAuthority;
+
+ mImagesDir = new File(activity.getCacheDir(), IMAGES_DIR);
+ mImagesDir.mkdir();
+ mCropPictureUri = createTempImageUri(activity, CROP_PICTURE_FILE_NAME, !waiting);
+ mTakePictureUri = createTempImageUri(activity, TAKE_PICTURE_FILE_NAME, !waiting);
+ mPhotoSize = getPhotoSize(activity);
+ mImageView.setOnClickListener(v -> showUpdatePhotoPopup());
+ mNewUserPhotoBitmap = bitmap;
+ }
+
+ /**
+ * Handles activity result from containing activity/fragment after a take/choose/crop photo
+ * action result is received.
+ */
+ public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode != Activity.RESULT_OK) {
+ return false;
+ }
+ final Uri pictureUri = data != null && data.getData() != null
+ ? data.getData() : mTakePictureUri;
+ switch (requestCode) {
+ case REQUEST_CODE_CROP_PHOTO:
+ onPhotoCropped(pictureUri);
+ return true;
+ case REQUEST_CODE_TAKE_PHOTO:
+ case REQUEST_CODE_CHOOSE_PHOTO:
+ if (mTakePictureUri.equals(pictureUri)) {
+ if (PhotoCapabilityUtils.canCropPhoto(mActivity)) {
+ cropPhoto();
+ } else {
+ onPhotoNotCropped(pictureUri);
+ }
+ } else {
+ copyAndCropPhoto(pictureUri);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public Drawable getNewUserPhotoDrawable() {
+ return mNewUserPhotoDrawable;
+ }
+
+ private void showUpdatePhotoPopup() {
+ final Context context = mImageView.getContext();
+ final boolean canTakePhoto = PhotoCapabilityUtils.canTakePhoto(context);
+ final boolean canChoosePhoto = PhotoCapabilityUtils.canChoosePhoto(context);
+
+ if (!canTakePhoto && !canChoosePhoto) {
+ return;
+ }
+
+ final List items = new ArrayList<>();
+
+ if (canTakePhoto) {
+ final String title = context.getString(R.string.user_image_take_photo);
+ items.add(new RestrictedMenuItem(context, title, UserManager.DISALLOW_SET_USER_ICON,
+ this::takePhoto));
+ }
+
+ if (canChoosePhoto) {
+ final String title = context.getString(R.string.user_image_choose_photo);
+ items.add(new RestrictedMenuItem(context, title, UserManager.DISALLOW_SET_USER_ICON,
+ this::choosePhoto));
+ }
+
+ final ListPopupWindow listPopupWindow = new ListPopupWindow(context);
+
+ listPopupWindow.setAnchorView(mImageView);
+ listPopupWindow.setModal(true);
+ listPopupWindow.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
+ listPopupWindow.setAdapter(new RestrictedPopupMenuAdapter(context, items));
+
+ final int width = Math.max(mImageView.getWidth(), context.getResources()
+ .getDimensionPixelSize(R.dimen.update_user_photo_popup_min_width));
+ listPopupWindow.setWidth(width);
+ listPopupWindow.setDropDownGravity(Gravity.START);
+
+ listPopupWindow.setOnItemClickListener((parent, view, position, id) -> {
+ listPopupWindow.dismiss();
+ final RestrictedMenuItem item =
+ (RestrictedMenuItem) parent.getAdapter().getItem(position);
+ item.doAction();
+ });
+
+ listPopupWindow.show();
+ }
+
+ private void takePhoto() {
+ Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE_SECURE);
+ appendOutputExtra(intent, mTakePictureUri);
+ mActivityStarter.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO);
+ }
+
+ private void choosePhoto() {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+ intent.setType("image/*");
+ appendOutputExtra(intent, mTakePictureUri);
+ mActivityStarter.startActivityForResult(intent, REQUEST_CODE_CHOOSE_PHOTO);
+ }
+
+ private void copyAndCropPhoto(final Uri pictureUri) {
+ // TODO: Replace AsyncTask
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ final ContentResolver cr = mActivity.getContentResolver();
+ try (InputStream in = cr.openInputStream(pictureUri);
+ OutputStream out = cr.openOutputStream(mTakePictureUri)) {
+ Streams.copy(in, out);
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to copy photo", e);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ if (!mActivity.isFinishing() && !mActivity.isDestroyed()) {
+ cropPhoto();
+ }
+ }
+ }.execute();
+ }
+
+ private void cropPhoto() {
+ // TODO: Use a public intent, when there is one.
+ Intent intent = new Intent("com.android.camera.action.CROP");
+ intent.setDataAndType(mTakePictureUri, "image/*");
+ appendOutputExtra(intent, mCropPictureUri);
+ appendCropExtras(intent);
+ if (intent.resolveActivity(mActivity.getPackageManager()) != null) {
+ try {
+ StrictMode.disableDeathOnFileUriExposure();
+ mActivityStarter.startActivityForResult(intent, REQUEST_CODE_CROP_PHOTO);
+ } finally {
+ StrictMode.enableDeathOnFileUriExposure();
+ }
+ } else {
+ onPhotoNotCropped(mTakePictureUri);
+ }
+ }
+
+ private void appendOutputExtra(Intent intent, Uri pictureUri) {
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri);
+ intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, pictureUri));
+ }
+
+ private void appendCropExtras(Intent intent) {
+ intent.putExtra("crop", "true");
+ intent.putExtra("scale", true);
+ intent.putExtra("scaleUpIfNeeded", true);
+ intent.putExtra("aspectX", 1);
+ intent.putExtra("aspectY", 1);
+ intent.putExtra("outputX", mPhotoSize);
+ intent.putExtra("outputY", mPhotoSize);
+ }
+
+ private void onPhotoCropped(final Uri data) {
+ // TODO: Replace AsyncTask to avoid possible memory leaks and handle configuration change
+ new AsyncTask() {
+ @Override
+ protected Bitmap doInBackground(Void... params) {
+ InputStream imageStream = null;
+ try {
+ imageStream = mActivity.getContentResolver()
+ .openInputStream(data);
+ return BitmapFactory.decodeStream(imageStream);
+ } catch (FileNotFoundException fe) {
+ Log.w(TAG, "Cannot find image file", fe);
+ return null;
+ } finally {
+ if (imageStream != null) {
+ try {
+ imageStream.close();
+ } catch (IOException ioe) {
+ Log.w(TAG, "Cannot close image stream", ioe);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ onPhotoProcessed(bitmap);
+
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
+ }
+
+ private void onPhotoNotCropped(final Uri data) {
+ // TODO: Replace AsyncTask to avoid possible memory leaks and handle configuration change
+ new AsyncTask() {
+ @Override
+ protected Bitmap doInBackground(Void... params) {
+ // Scale and crop to a square aspect ratio
+ Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize,
+ Config.ARGB_8888);
+ Canvas canvas = new Canvas(croppedImage);
+ Bitmap fullImage;
+ try {
+ InputStream imageStream = mActivity.getContentResolver()
+ .openInputStream(data);
+ fullImage = BitmapFactory.decodeStream(imageStream);
+ } catch (FileNotFoundException fe) {
+ return null;
+ }
+ if (fullImage != null) {
+ int rotation = getRotation(mActivity, data);
+ final int squareSize = Math.min(fullImage.getWidth(),
+ fullImage.getHeight());
+ final int left = (fullImage.getWidth() - squareSize) / 2;
+ final int top = (fullImage.getHeight() - squareSize) / 2;
+
+ Matrix matrix = new Matrix();
+ RectF rectSource = new RectF(left, top,
+ left + squareSize, top + squareSize);
+ RectF rectDest = new RectF(0, 0, mPhotoSize, mPhotoSize);
+ matrix.setRectToRect(rectSource, rectDest, Matrix.ScaleToFit.CENTER);
+ matrix.postRotate(rotation, mPhotoSize / 2f, mPhotoSize / 2f);
+ canvas.drawBitmap(fullImage, matrix, new Paint());
+ return croppedImage;
+ } else {
+ // Bah! Got nothin.
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ onPhotoProcessed(bitmap);
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
+ }
+
+ /**
+ * Reads the image's exif data and determines the rotation degree needed to display the image
+ * in portrait mode.
+ */
+ private int getRotation(Context context, Uri selectedImage) {
+ int rotation = -1;
+ try {
+ InputStream imageStream = context.getContentResolver().openInputStream(selectedImage);
+ ExifInterface exif = new ExifInterface(imageStream);
+ rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1);
+ } catch (IOException exception) {
+ Log.e(TAG, "Error while getting rotation", exception);
+ }
+
+ switch (rotation) {
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ return 90;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ return 180;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ return 270;
+ default:
+ return 0;
+ }
+ }
+
+ private void onPhotoProcessed(Bitmap bitmap) {
+ if (bitmap != null) {
+ mNewUserPhotoBitmap = bitmap;
+ mNewUserPhotoDrawable = CircleFramedDrawable
+ .getInstance(mImageView.getContext(), mNewUserPhotoBitmap);
+ mImageView.setImageDrawable(mNewUserPhotoDrawable);
+ }
+ new File(mImagesDir, TAKE_PICTURE_FILE_NAME).delete();
+ new File(mImagesDir, CROP_PICTURE_FILE_NAME).delete();
+ }
+
+ private static int getPhotoSize(Context context) {
+ try (Cursor cursor = context.getContentResolver().query(
+ DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
+ new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null)) {
+ if (cursor != null) {
+ cursor.moveToFirst();
+ return cursor.getInt(0);
+ } else {
+ return DEFAULT_PHOTO_SIZE;
+ }
+ }
+ }
+
+ private Uri createTempImageUri(Context context, String fileName, boolean purge) {
+ final File fullPath = new File(mImagesDir, fileName);
+ if (purge) {
+ fullPath.delete();
+ }
+ return FileProvider.getUriForFile(context, mFileAuthority, fullPath);
+ }
+
+ File saveNewUserPhotoBitmap() {
+ if (mNewUserPhotoBitmap == null) {
+ return null;
+ }
+ try {
+ File file = new File(mImagesDir, NEW_USER_PHOTO_FILE_NAME);
+ OutputStream os = new FileOutputStream(file);
+ mNewUserPhotoBitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
+ os.flush();
+ os.close();
+ return file;
+ } catch (IOException e) {
+ Log.e(TAG, "Cannot create temp file", e);
+ }
+ return null;
+ }
+
+ static Bitmap loadNewUserPhotoBitmap(File file) {
+ return BitmapFactory.decodeFile(file.getAbsolutePath());
+ }
+
+ void removeNewUserPhotoBitmapFile() {
+ new File(mImagesDir, NEW_USER_PHOTO_FILE_NAME).delete();
+ }
+
+ private static final class RestrictedMenuItem {
+ private final Context mContext;
+ private final String mTitle;
+ private final Runnable mAction;
+ private final RestrictedLockUtils.EnforcedAdmin mAdmin;
+ // Restriction may be set by system or something else via UserManager.setUserRestriction().
+ private final boolean mIsRestrictedByBase;
+
+ /**
+ * The menu item, used for popup menu. Any element of such a menu can be disabled by admin.
+ *
+ * @param context A context.
+ * @param title The title of the menu item.
+ * @param restriction The restriction, that if is set, blocks the menu item.
+ * @param action The action on menu item click.
+ */
+ RestrictedMenuItem(Context context, String title, String restriction,
+ Runnable action) {
+ mContext = context;
+ mTitle = title;
+ mAction = action;
+
+ final int myUserId = UserHandle.myUserId();
+ mAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context,
+ restriction, myUserId);
+ mIsRestrictedByBase = RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext,
+ restriction, myUserId);
+ }
+
+ @Override
+ public String toString() {
+ return mTitle;
+ }
+
+ void doAction() {
+ if (isRestrictedByBase()) {
+ return;
+ }
+
+ if (isRestrictedByAdmin()) {
+ RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mContext, mAdmin);
+ return;
+ }
+
+ mAction.run();
+ }
+
+ boolean isRestrictedByAdmin() {
+ return mAdmin != null;
+ }
+
+ boolean isRestrictedByBase() {
+ return mIsRestrictedByBase;
+ }
+ }
+
+ /**
+ * Provide this adapter to ListPopupWindow.setAdapter() to have a popup window menu, where
+ * any element can be restricted by admin (profile owner or device owner).
+ */
+ private static final class RestrictedPopupMenuAdapter extends ArrayAdapter {
+ RestrictedPopupMenuAdapter(Context context, List items) {
+ super(context, R.layout.restricted_popup_menu_item, R.id.text, items);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final View view = super.getView(position, convertView, parent);
+ final RestrictedMenuItem item = getItem(position);
+ final TextView text = (TextView) view.findViewById(R.id.text);
+ final ImageView image = (ImageView) view.findViewById(R.id.restricted_icon);
+
+ text.setEnabled(!item.isRestrictedByAdmin() && !item.isRestrictedByBase());
+ image.setVisibility(item.isRestrictedByAdmin() && !item.isRestrictedByBase()
+ ? ImageView.VISIBLE : ImageView.GONE);
+
+ return view;
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/PhotoCapabilityUtils.java b/packages/SettingsLib/src/com/android/settingslib/users/PhotoCapabilityUtils.java
new file mode 100644
index 0000000000000..165c2808f16d9
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/users/PhotoCapabilityUtils.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.users;
+
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.provider.MediaStore;
+
+/**
+ * Utility class that contains helper methods to determine if the current user has permission and
+ * the device is in a proper state to start an activity for a given action.
+ */
+public class PhotoCapabilityUtils {
+
+ /**
+ * Check if the current user can perform any activity for
+ * android.media.action.IMAGE_CAPTURE action.
+ */
+ public static boolean canTakePhoto(Context context) {
+ return context.getPackageManager().queryIntentActivities(
+ new Intent(MediaStore.ACTION_IMAGE_CAPTURE),
+ PackageManager.MATCH_DEFAULT_ONLY).size() > 0;
+ }
+
+ /**
+ * Check if the current user can perform any activity for
+ * android.intent.action.GET_CONTENT action for images.
+ * Returns false if the device is currently locked and
+ * requires a PIN, pattern or password to unlock.
+ */
+ public static boolean canChoosePhoto(Context context) {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("image/*");
+ boolean canPerformActivityForGetImage =
+ context.getPackageManager().queryIntentActivities(intent, 0).size() > 0;
+ // on locked device we can't access the images
+ return canPerformActivityForGetImage && !isDeviceLocked(context);
+ }
+
+ /**
+ * Check if the current user can perform any activity for
+ * com.android.camera.action.CROP action for images.
+ * Returns false if the device is currently locked and
+ * requires a PIN, pattern or password to unlock.
+ */
+ public static boolean canCropPhoto(Context context) {
+ Intent intent = new Intent("com.android.camera.action.CROP");
+ intent.setType("image/*");
+ boolean canPerformActivityForCropping =
+ context.getPackageManager().queryIntentActivities(intent, 0).size() > 0;
+ // on locked device we can't start a cropping activity
+ return canPerformActivityForCropping && !isDeviceLocked(context);
+ }
+
+ private static boolean isDeviceLocked(Context context) {
+ KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
+ return keyguardManager == null || keyguardManager.isDeviceLocked();
+ }
+
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/UserCreatingDialog.java b/packages/SettingsLib/src/com/android/settingslib/users/UserCreatingDialog.java
new file mode 100644
index 0000000000000..075635c87b1b1
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/users/UserCreatingDialog.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.users;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import com.android.settingslib.R;
+
+/**
+ * Dialog to show when a user creation is in progress.
+ */
+public class UserCreatingDialog extends AlertDialog {
+
+ public UserCreatingDialog(Context context) {
+ // hardcoding theme to be consistent with UserSwitchingDialog's theme
+ // todo replace both to adapt to the device's theme
+ super(context, com.android.internal.R.style.Theme_DeviceDefault_Light_Dialog_Alert);
+
+ inflateContent();
+ getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
+
+ WindowManager.LayoutParams attrs = getWindow().getAttributes();
+ attrs.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_SYSTEM_ERROR
+ | WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+ getWindow().setAttributes(attrs);
+ }
+
+ private void inflateContent() {
+ // using the same design as UserSwitchingDialog
+ setCancelable(false);
+ View view = LayoutInflater.from(getContext())
+ .inflate(R.layout.user_creation_progress_dialog, null);
+ String message = getContext().getString(R.string.creating_new_user_dialog_message);
+ view.setAccessibilityPaneTitle(message);
+ ((TextView) view.findViewById(R.id.message)).setText(message);
+ setView(view);
+ }
+
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java
new file mode 100644
index 0000000000000..d6c8816ecc585
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2018 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.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.widget.EditText;
+import android.widget.ImageView;
+
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.settingslib.R;
+
+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;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowDialog;
+
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@RunWith(RobolectricTestRunner.class)
+public class EditUserInfoControllerTest {
+ private static final int MAX_USER_NAME_LENGTH = 100;
+
+ @Mock
+ private Drawable mCurrentIcon;
+ @Mock
+ private ActivityStarter mActivityStarter;
+
+ private boolean mCanChangePhoto;
+ private Activity mActivity;
+ private TestEditUserInfoController mController;
+
+ public class TestEditUserInfoController extends EditUserInfoController {
+ private EditUserPhotoController mPhotoController;
+
+ TestEditUserInfoController() {
+ super("file_authority");
+ }
+
+ private EditUserPhotoController getPhotoController() {
+ return mPhotoController;
+ }
+
+ @Override
+ EditUserPhotoController createEditUserPhotoController(Activity activity,
+ ActivityStarter activityStarter, ImageView userPhotoView) {
+ mPhotoController = mock(EditUserPhotoController.class, Answers.RETURNS_DEEP_STUBS);
+ return mPhotoController;
+ }
+
+ @Override
+ boolean canChangePhoto(Context context) {
+ return mCanChangePhoto;
+ }
+ }
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mActivity = spy(ActivityController.of(new FragmentActivity()).get());
+ mActivity.setTheme(R.style.Theme_AppCompat_DayNight);
+ mController = new TestEditUserInfoController();
+ mCanChangePhoto = true;
+ }
+
+ @Test
+ public void photoControllerOnActivityResult_whenWaiting_isCalled() {
+ mController.createDialog(mActivity, mActivityStarter, mCurrentIcon, "test user",
+ "title", null, null);
+ mController.startingActivityForResult();
+ Intent resultData = new Intent();
+ mController.onActivityResult(0, 0, resultData);
+ EditUserPhotoController photoController = mController.getPhotoController();
+
+ assertThat(photoController).isNotNull();
+ verify(photoController).onActivityResult(0, 0, resultData);
+ }
+
+ @Test
+ @Config(shadows = ShadowDialog.class)
+ public void userNameView_inputLongName_shouldBeConstrained() {
+ // generate a string of 200 'A's
+ final String longName = Stream.generate(
+ () -> String.valueOf('A')).limit(200).collect(Collectors.joining());
+
+ final AlertDialog dialog = (AlertDialog) mController.createDialog(mActivity,
+ mActivityStarter, mCurrentIcon,
+ "test user", "title", null,
+ null);
+ dialog.show();
+ final EditText userNameEditText = dialog.findViewById(R.id.user_name);
+ userNameEditText.setText(longName);
+
+ assertThat(userNameEditText.getText().length()).isEqualTo(MAX_USER_NAME_LENGTH);
+ }
+
+ @Test
+ public void cancelCallback_isCalled_whenCancelled() {
+ BiConsumer successCallback = mock(BiConsumer.class);
+ Runnable cancelCallback = mock(Runnable.class);
+
+ AlertDialog dialog = (AlertDialog) mController.createDialog(
+ mActivity, mActivityStarter, mCurrentIcon, "test",
+ "title", successCallback, cancelCallback);
+ dialog.show();
+ dialog.cancel();
+
+ verifyZeroInteractions(successCallback);
+ verify(cancelCallback, times(1))
+ .run();
+ }
+
+ @Test
+ public void cancelCallback_isCalled_whenNegativeClicked() {
+ BiConsumer successCallback = mock(BiConsumer.class);
+ Runnable cancelCallback = mock(Runnable.class);
+
+ AlertDialog dialog = (AlertDialog) mController.createDialog(
+ mActivity, mActivityStarter, mCurrentIcon, "test",
+ "title", successCallback, cancelCallback);
+ dialog.show();
+ dialog.getButton(Dialog.BUTTON_NEGATIVE).performClick();
+
+ verifyZeroInteractions(successCallback);
+ verify(cancelCallback, times(1))
+ .run();
+ }
+
+ @Test
+ public void successCallback_isCalled_whenNothingChanged() {
+ BiConsumer successCallback = mock(BiConsumer.class);
+ Runnable cancelCallback = mock(Runnable.class);
+
+ Drawable oldUserIcon = mCurrentIcon;
+ AlertDialog dialog = (AlertDialog) mController.createDialog(
+ mActivity, mActivityStarter, oldUserIcon, "test",
+ "title", successCallback, cancelCallback);
+ // No change to the photo.
+ when(mController.getPhotoController().getNewUserPhotoDrawable()).thenReturn(null);
+ dialog.show();
+ dialog.getButton(Dialog.BUTTON_POSITIVE).performClick();
+
+ verify(successCallback, times(1))
+ .accept("test", oldUserIcon);
+ verifyZeroInteractions(cancelCallback);
+ }
+
+ @Test
+ public void successCallback_calledWithNullIcon_whenOldIconIsNullAndNothingChanged() {
+ BiConsumer successCallback = mock(BiConsumer.class);
+ Runnable cancelCallback = mock(Runnable.class);
+
+ AlertDialog dialog = (AlertDialog) mController.createDialog(
+ mActivity, mActivityStarter, null, "test",
+ "title", successCallback, cancelCallback);
+ // No change to the photo.
+ when(mController.getPhotoController().getNewUserPhotoDrawable()).thenReturn(null);
+ dialog.show();
+ dialog.getButton(Dialog.BUTTON_POSITIVE).performClick();
+
+ verify(successCallback, times(1))
+ .accept("test", null);
+ verifyZeroInteractions(cancelCallback);
+ }
+
+ @Test
+ public void successCallback_isCalled_whenLabelChanges() {
+ BiConsumer successCallback = mock(BiConsumer.class);
+ Runnable cancelCallback = mock(Runnable.class);
+
+ AlertDialog dialog = (AlertDialog) mController.createDialog(
+ mActivity, mActivityStarter, mCurrentIcon, "test",
+ "title", successCallback, cancelCallback);
+ // No change to the photo.
+ when(mController.getPhotoController().getNewUserPhotoDrawable()).thenReturn(null);
+ dialog.show();
+ String expectedNewName = "new test user";
+ EditText editText = (EditText) dialog.findViewById(R.id.user_name);
+ editText.setText(expectedNewName);
+ dialog.getButton(Dialog.BUTTON_POSITIVE).performClick();
+
+ verify(successCallback, times(1))
+ .accept(expectedNewName, mCurrentIcon);
+ verifyZeroInteractions(cancelCallback);
+ }
+
+ @Test
+ public void successCallback_isCalled_whenPhotoChanges() {
+ BiConsumer successCallback = mock(BiConsumer.class);
+ Runnable cancelCallback = mock(Runnable.class);
+
+ AlertDialog dialog = (AlertDialog) mController.createDialog(
+ mActivity, mActivityStarter, mCurrentIcon, "test",
+ "title", successCallback, cancelCallback);
+ // A different drawable.
+ Drawable newPhoto = mock(Drawable.class);
+ when(mController.getPhotoController().getNewUserPhotoDrawable()).thenReturn(newPhoto);
+ dialog.show();
+ dialog.getButton(Dialog.BUTTON_POSITIVE).performClick();
+
+ verify(successCallback, times(1))
+ .accept("test", newPhoto);
+ verifyZeroInteractions(cancelCallback);
+ }
+
+ @Test
+ public void successCallback_isCalledWithChangedPhoto_whenOldIconIsNullAndPhotoChanges() {
+ BiConsumer successCallback = mock(BiConsumer.class);
+ Runnable cancelCallback = mock(Runnable.class);
+
+ AlertDialog dialog = (AlertDialog) mController.createDialog(
+ mActivity, mActivityStarter, null, "test",
+ "title", successCallback, cancelCallback);
+ // A different drawable.
+ Drawable newPhoto = mock(Drawable.class);
+ when(mController.getPhotoController().getNewUserPhotoDrawable()).thenReturn(newPhoto);
+ dialog.show();
+ dialog.getButton(Dialog.BUTTON_POSITIVE).performClick();
+
+ verify(successCallback, times(1))
+ .accept("test", newPhoto);
+ verifyZeroInteractions(cancelCallback);
+ }
+
+ @Test
+ public void createDialog_canNotChangePhoto_nullPhotoController() {
+ mCanChangePhoto = false;
+
+ mController.createDialog(mActivity, mActivityStarter, mCurrentIcon,
+ "test", "title", null, null);
+
+ assertThat(mController.mPhotoController).isNull();
+ }
+}
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 505ef7a588431..4c9005a5b6bad 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -606,6 +606,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 2b0a963bff185..283918912c76c 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -637,6 +637,13 @@
- @*android:color/primary_text_material_dark
+
+
diff --git a/packages/SystemUI/res/xml/fileprovider.xml b/packages/SystemUI/res/xml/fileprovider.xml
index fa6468fefe048..b67378e638e13 100644
--- a/packages/SystemUI/res/xml/fileprovider.xml
+++ b/packages/SystemUI/res/xml/fileprovider.xml
@@ -18,4 +18,5 @@
+
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
index 28bcf3a351177..ba88a599b785e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
@@ -26,6 +26,7 @@ import com.android.systemui.settings.BrightnessDialog;
import com.android.systemui.tuner.TunerActivity;
import com.android.systemui.usb.UsbDebuggingActivity;
import com.android.systemui.usb.UsbDebuggingSecondaryUserActivity;
+import com.android.systemui.user.CreateUserActivity;
import dagger.Binds;
import dagger.Module;
@@ -85,4 +86,10 @@ public abstract class DefaultActivityBinder {
@ClassKey(UsbDebuggingSecondaryUserActivity.class)
public abstract Activity bindUsbDebuggingSecondaryUserActivity(
UsbDebuggingSecondaryUserActivity activity);
+
+ /** Inject into CreateUserActivity. */
+ @Binds
+ @IntoMap
+ @ClassKey(CreateUserActivity.class)
+ public abstract Activity bindCreateUserActivity(CreateUserActivity activity);
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 8f4e738e5a5fe..90dc213ce6e6c 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -43,6 +43,7 @@ import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule;
import com.android.systemui.tuner.dagger.TunerModule;
+import com.android.systemui.user.UserModule;
import com.android.systemui.util.concurrency.SysUIConcurrencyModule;
import com.android.systemui.util.dagger.UtilModule;
import com.android.systemui.util.sensors.SensorModule;
@@ -76,6 +77,7 @@ import dagger.Provides;
StatusBarPolicyModule.class,
SysUIConcurrencyModule.class,
TunerModule.class,
+ UserModule.class,
UtilModule.class,
VolumeModule.class
},
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index 201ed9c9ebec7..8ddd4c9816cd3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -155,7 +155,7 @@ public class UserDetailView extends PseudoGridView {
}
view.setActivated(true);
}
- switchTo(tag);
+ onUserListItemClicked(tag);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java
index f52a6e0191a19..f45178cd22300 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java
@@ -342,7 +342,7 @@ public class KeyguardUserSwitcher {
}
v.setActivated(true);
}
- switchTo(user);
+ onUserListItemClicked(user);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index 17fcb1dd6f1a9..72e8e38735e9b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -23,6 +23,7 @@ import static com.android.systemui.DejankUtils.whitelistIpcs;
import android.app.ActivityManager;
import android.app.Dialog;
+import android.app.IActivityTaskManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
@@ -53,7 +54,6 @@ import android.widget.BaseAdapter;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.internal.util.UserIcons;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.systemui.Dumpable;
import com.android.systemui.GuestResumeSessionReceiver;
@@ -69,6 +69,7 @@ import com.android.systemui.plugins.qs.DetailAdapter;
import com.android.systemui.qs.QSUserSwitcherEvent;
import com.android.systemui.qs.tiles.UserDetailView;
import com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.android.systemui.user.CreateUserActivity;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -104,6 +105,7 @@ public class UserSwitcherController implements Dumpable {
protected final Handler mHandler;
private final ActivityStarter mActivityStarter;
private final BroadcastDispatcher mBroadcastDispatcher;
+ private final IActivityTaskManager mActivityTaskManager;
private ArrayList mUsers = new ArrayList<>();
private Dialog mExitGuestDialog;
@@ -121,9 +123,11 @@ public class UserSwitcherController implements Dumpable {
@Inject
public UserSwitcherController(Context context, KeyguardStateController keyguardStateController,
@Main Handler handler, ActivityStarter activityStarter,
- BroadcastDispatcher broadcastDispatcher, UiEventLogger uiEventLogger) {
+ BroadcastDispatcher broadcastDispatcher, UiEventLogger uiEventLogger,
+ IActivityTaskManager activityTaskManager) {
mContext = context;
mBroadcastDispatcher = broadcastDispatcher;
+ mActivityTaskManager = activityTaskManager;
mUiEventLogger = uiEventLogger;
if (!UserManager.isGuestUserEphemeral()) {
mGuestResumeSessionReceiver.register(mBroadcastDispatcher);
@@ -363,7 +367,7 @@ public class UserSwitcherController implements Dumpable {
}
}
- public void switchTo(UserRecord record) {
+ private void onUserListItemClicked(UserRecord record) {
int id;
if (record.isGuest && record.info == null) {
// No guest user. Create one.
@@ -408,19 +412,6 @@ public class UserSwitcherController implements Dumpable {
switchToUserId(id);
}
- public void switchTo(int userId) {
- final int count = mUsers.size();
- for (int i = 0; i < count; ++i) {
- UserRecord record = mUsers.get(i);
- if (record.info != null && record.info.id == userId) {
- switchTo(record);
- return;
- }
- }
-
- Log.e(TAG, "Couldn't switch to user, id=" + userId);
- }
-
protected void switchToUserId(int id) {
try {
pauseRefreshUsers();
@@ -666,8 +657,11 @@ public class UserSwitcherController implements Dumpable {
return position;
}
- public void switchTo(UserRecord record) {
- mController.switchTo(record);
+ /**
+ * It handles click events on user list items.
+ */
+ public void onUserListItemClicked(UserRecord record) {
+ mController.onUserListItemClicked(record);
}
public String getName(Context context, UserRecord item) {
@@ -924,18 +918,33 @@ public class UserSwitcherController implements Dumpable {
if (ActivityManager.isUserAMonkey()) {
return;
}
- UserInfo user = mUserManager.createUser(
- mContext.getString(R.string.user_new_user_name), 0 /* flags */);
- if (user == null) {
- // Couldn't create user, most likely because there are too many, but we haven't
- // been able to reload the list yet.
- return;
+ Intent intent = CreateUserActivity.createIntentForStart(getContext());
+
+ // There are some differences between ActivityStarter and ActivityTaskManager in
+ // terms of how they start an activity. ActivityStarter hides the notification bar
+ // before starting the activity to make sure nothing is in front of the new
+ // activity. ActivityStarter also tries to unlock the device if it's locked.
+ // When locked with PIN/pattern/password then it shows the prompt, if there are no
+ // security steps then it dismisses the keyguard and then starts the activity.
+ // ActivityTaskManager doesn't hide the notification bar or unlocks the device, but
+ // it can start an activity on top of the locked screen.
+ if (!mKeyguardStateController.isUnlocked()
+ && !mKeyguardStateController.canDismissLockScreen()) {
+ // Device is locked and can't be unlocked without a PIN/pattern/password so we
+ // need to use ActivityTaskManager to start the activity on top of the locked
+ // screen.
+ try {
+ mActivityTaskManager.startActivity(null,
+ mContext.getBasePackageName(), mContext.getAttributionTag(), intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()), null,
+ null, 0, 0, null, null);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ Log.e(TAG, "Couldn't start create user activity", e);
+ }
+ } else {
+ mActivityStarter.startActivity(intent, true);
}
- int id = user.id;
- Bitmap icon = UserIcons.convertToBitmap(UserIcons.getDefaultUserIcon(
- mContext.getResources(), id, /* light= */ false));
- mUserManager.setUserIcon(id, icon);
- switchToUserId(id);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
new file mode 100644
index 0000000000000..890ee5f453093
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2020 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.systemui.user;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.IActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.users.EditUserInfoController;
+import com.android.systemui.R;
+
+import javax.inject.Inject;
+
+/**
+ * This screen shows a Dialog for choosing nickname and photo for a new user, and then delegates the
+ * user creation to a UserCreator.
+ */
+public class CreateUserActivity extends Activity {
+
+ /**
+ * Creates an intent to start this activity.
+ */
+ public static Intent createIntentForStart(Context context) {
+ return new Intent(context, CreateUserActivity.class);
+ }
+
+ private static final String TAG = "CreateUserActivity";
+ private static final String DIALOG_STATE_KEY = "create_user_dialog_state";
+
+ private final UserCreator mUserCreator;
+ private final EditUserInfoController mEditUserInfoController;
+ private final IActivityManager mActivityManager;
+
+ private Dialog mSetupUserDialog;
+
+ @Inject
+ public CreateUserActivity(UserCreator userCreator,
+ EditUserInfoController editUserInfoController, IActivityManager activityManager) {
+ mUserCreator = userCreator;
+ mEditUserInfoController = editUserInfoController;
+ mActivityManager = activityManager;
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setShowWhenLocked(true);
+ setContentView(R.layout.activity_create_new_user);
+
+ if (savedInstanceState != null) {
+ mEditUserInfoController.onRestoreInstanceState(savedInstanceState);
+ }
+
+ mSetupUserDialog = createDialog();
+ mSetupUserDialog.show();
+ }
+
+ @Override
+ protected void onSaveInstanceState(@NonNull Bundle outState) {
+ if (mSetupUserDialog != null && mSetupUserDialog.isShowing()) {
+ outState.putBundle(DIALOG_STATE_KEY, mSetupUserDialog.onSaveInstanceState());
+ }
+
+ mEditUserInfoController.onSaveInstanceState(outState);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ Bundle savedDialogState = savedInstanceState.getBundle(DIALOG_STATE_KEY);
+ if (savedDialogState != null && mSetupUserDialog != null) {
+ mSetupUserDialog.onRestoreInstanceState(savedDialogState);
+ }
+ }
+
+ private Dialog createDialog() {
+ String defaultUserName = getString(com.android.settingslib.R.string.user_new_user_name);
+
+ return mEditUserInfoController.createDialog(
+ this,
+ (intent, requestCode) -> {
+ mEditUserInfoController.startingActivityForResult();
+ startActivityForResult(intent, requestCode);
+ },
+ null,
+ defaultUserName,
+ getString(R.string.user_add_user),
+ this::addUserNow,
+ this::finish
+ );
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ mEditUserInfoController.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ if (mSetupUserDialog != null) {
+ mSetupUserDialog.dismiss();
+ }
+ }
+
+ private void addUserNow(String userName, Drawable userIcon) {
+ mSetupUserDialog.dismiss();
+
+ userName = (userName == null || userName.trim().isEmpty())
+ ? getString(R.string.user_new_user_name)
+ : userName;
+
+ mUserCreator.createUser(userName, userIcon,
+ userInfo -> {
+ switchToUser(userInfo.id);
+ finishIfNeeded();
+ }, () -> {
+ Log.e(TAG, "Unable to create user");
+ finishIfNeeded();
+ });
+ }
+
+ private void finishIfNeeded() {
+ if (!isFinishing() && !isDestroyed()) {
+ finish();
+ }
+ }
+
+ private void switchToUser(int userId) {
+ try {
+ mActivityManager.switchUser(userId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't switch user.", e);
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserCreator.java b/packages/SystemUI/src/com/android/systemui/user/UserCreator.java
new file mode 100644
index 0000000000000..3a270bb77e461
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/UserCreator.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2020 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.systemui.user;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.graphics.drawable.Drawable;
+import android.os.UserManager;
+
+import com.android.internal.util.UserIcons;
+import com.android.settingslib.users.UserCreatingDialog;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+/**
+ * A class to do the user creation process. It shows a progress dialog, and manages the user
+ * creation
+ */
+public class UserCreator {
+
+ private final Context mContext;
+ private final UserManager mUserManager;
+
+ @Inject
+ public UserCreator(Context context, UserManager userManager) {
+ mContext = context;
+ mUserManager = userManager;
+ }
+
+ /**
+ * Shows a progress dialog then starts the user creation process on the main thread.
+ *
+ * @param successCallback is called when the user creation is successful.
+ * @param errorCallback is called when userManager.createUser returns null.
+ * (Exceptions are not handled by this class)
+ */
+ public void createUser(String userName, Drawable userIcon, Consumer successCallback,
+ Runnable errorCallback) {
+
+ Dialog userCreationProgressDialog = new UserCreatingDialog(mContext);
+ userCreationProgressDialog.show();
+
+ // userManager.createUser will block the thread so post is needed for the dialog to show
+ ThreadUtils.postOnMainThread(() -> {
+ UserInfo user =
+ mUserManager.createUser(userName, UserManager.USER_TYPE_FULL_SECONDARY, 0);
+ if (user == null) {
+ // Couldn't create user for some reason
+ userCreationProgressDialog.dismiss();
+ errorCallback.run();
+ return;
+ }
+
+ Drawable newUserIcon = userIcon;
+ if (newUserIcon == null) {
+ newUserIcon = UserIcons.getDefaultUserIcon(mContext.getResources(), user.id, false);
+ }
+ mUserManager.setUserIcon(user.id, UserIcons.convertToBitmap(newUserIcon));
+
+ userCreationProgressDialog.dismiss();
+ successCallback.accept(user);
+ });
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
new file mode 100644
index 0000000000000..0ad0984e8231d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2020 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.systemui.user;
+
+import com.android.settingslib.users.EditUserInfoController;
+
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Dagger module for User related classes.
+ */
+@Module
+public class UserModule {
+
+ private static final String FILE_PROVIDER_AUTHORITY = "com.android.systemui.fileprovider";
+
+ @Provides
+ EditUserInfoController provideEditUserInfoController() {
+ return new EditUserInfoController(FILE_PROVIDER_AUTHORITY);
+ }
+}