Add user name photo dialog to user creation

- Remove unused method switchTo
- Move edit user controllers from settings module
- Create new activity for new user creation
- Add ActivityStarter for delegating activity starting
- Add UserCreator for user creation logic
- Add UserCreatingDialog to display while user is creating

Test: manual test && atest EditUserInfoControllerTest
Doc: http://shortn/_cJE9o6pBZR
Screenrecords: http://shortn/_VEN6rr5ta6
Bug: 147653252
Change-Id: I6a7f1dba499e95891c69569ab731f86e6fd1dc16
This commit is contained in:
Andras Kloczl
2020-07-22 11:44:41 +01:00
parent ccfe7f8295
commit 7f3c2dc240
23 changed files with 1671 additions and 31 deletions

View File

@@ -0,0 +1,51 @@
<!--
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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:id="@+id/user_photo"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="bottom"
android:contentDescription="@string/user_image_photo_selector"
android:background="@*android:drawable/spinner_background_holo_dark"
android:scaleType="fitCenter"/>
<EditText
android:id="@+id/user_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_weight="1"
android:minWidth="200dp"
android:layout_marginStart="6dp"
android:minHeight="@dimen/min_tap_target_size"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textAlignment="viewStart"
android:inputType="text|textCapWords"
android:selectAllOnFocus="true"
android:hint="@string/user_nickname"
android:maxLength="100"/>
</LinearLayout>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
** Copyright 2016, 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.
*/
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:paddingEnd="16dp"
android:paddingStart="16dp">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:ellipsize="marquee"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:textColor="?android:attr/textColorAlertDialogListItem" />
<ImageView
android:id="@+id/restricted_icon"
android:layout_width="@*android:dimen/config_restrictedIconSize"
android:layout_height="@*android:dimen/config_restrictedIconSize"
android:layout_alignParentRight="true"
android:scaleType="centerInside"
android:src="@*android:drawable/ic_info"
android:tint="?android:attr/colorAccent"
android:visibility="gone" />
</RelativeLayout>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/message"
style="?android:attr/textAppearanceListItem"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:paddingStart="?android:attr/dialogPreferredPadding"
android:paddingEnd="?android:attr/dialogPreferredPadding"
android:paddingTop="24dp"
android:paddingBottom="24dp" />

View File

@@ -97,4 +97,8 @@
<!-- Size of advanced icon -->
<dimen name="advanced_icon_size">18dp</dimen>
<!-- Minimum width for the popup for updating a user's photo. -->
<dimen name="update_user_photo_popup_min_width">300dp</dimen>
</resources>

View File

@@ -1354,6 +1354,11 @@
<string name="user_set_lock_button">Set lock</string>
<!-- Label for switching to other user in the user switcher [CHAR LIMIT=35] -->
<string name="user_switch_to_user">Switch to <xliff:g id="user_name" example="John Doe">%s</xliff:g></string>
<!-- Dialog message when creating a new user [CHAR LIMIT=40] -->
<string name="creating_new_user_dialog_message">Creating new user…</string>
<!-- Title for the preference to enter the nickname of the user to display in the user switcher [CHAR LIMIT=25]-->
<string name="user_nickname">Nickname</string>
<!-- Label for adding a new guest in the user switcher [CHAR LIMIT=35] -->
<string name="guest_new_guest">Add guest</string>
@@ -1362,6 +1367,13 @@
<!-- Name for the guest user [CHAR LIMIT=35] -->
<string name="guest_nickname">Guest</string>
<!-- An option in a photo selection dialog to take a new photo [CHAR LIMIT=50] -->
<string name="user_image_take_photo">Take a photo</string>
<!-- An option in a photo selection dialog to choose a pre-existing image [CHAR LIMIT=50] -->
<string name="user_image_choose_photo">Choose an image</string>
<!-- Accessibility message for the photo selector which is a button/popup with the current photo [CHAR LIMIT=50] -->
<string name="user_image_photo_selector">Select photo</string>
<!-- List entry in developer settings to choose default device/system behavior for the app freezer [CHAR LIMIT=30]-->
<string name="cached_apps_freezer_device_default">Device default</string>
<!-- List entry in developer settings to disable the app freezer in developer settings [CHAR LIMIT=30]-->

View File

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

View File

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

View File

@@ -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<EditUserPhotoController.RestrictedMenuItem> 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<Void, Void, Void>() {
@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<Void, Void, Bitmap>() {
@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<Void, Void, Bitmap>() {
@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<RestrictedMenuItem> {
RestrictedPopupMenuAdapter(Context context, List<RestrictedMenuItem> 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;
}
}
}

View File

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

View File

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

View File

@@ -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<String, Drawable> 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<String, Drawable> 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<String, Drawable> 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<String, Drawable> 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<String, Drawable> 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<String, Drawable> 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<String, Drawable> 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();
}
}

View File

@@ -606,6 +606,14 @@
</intent-filter>
</activity>
<activity
android:name=".user.CreateUserActivity"
android:excludeFromRecents="true"
android:exported="false"
android:finishOnCloseSystemDialogs="true"
android:launchMode="singleInstance"
android:theme="@style/Theme.CreateUser" />
<activity android:name=".Somnambulator"
android:label="@string/start_dreams"
android:icon="@mipmap/ic_launcher_dreams"

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:orientation="vertical">
</LinearLayout>

View File

@@ -637,6 +637,13 @@
<item name="wallpaperTextColor">@*android:color/primary_text_material_dark</item>
</style>
<style name="Theme.CreateUser" parent="@style/Theme.SystemUI">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">#33000000</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="TextAppearance.Control">
<item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
</style>

View File

@@ -18,4 +18,5 @@
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="leak" path="leak/"/>
<external-path name="screenrecord" path="."/>
<cache-path name="multi_user" path="multi_user/" />
</paths>

View File

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

View File

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

View File

@@ -155,7 +155,7 @@ public class UserDetailView extends PseudoGridView {
}
view.setActivated(true);
}
switchTo(tag);
onUserListItemClicked(tag);
}
}
}

View File

@@ -342,7 +342,7 @@ public class KeyguardUserSwitcher {
}
v.setActivated(true);
}
switchTo(user);
onUserListItemClicked(user);
}
}
}

View File

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

View File

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

View File

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

View File

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