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:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
@@ -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>
|
||||
|
||||
@@ -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]-->
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
23
packages/SystemUI/res/layout/activity_create_new_user.xml
Normal file
23
packages/SystemUI/res/layout/activity_create_new_user.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -155,7 +155,7 @@ public class UserDetailView extends PseudoGridView {
|
||||
}
|
||||
view.setActivated(true);
|
||||
}
|
||||
switchTo(tag);
|
||||
onUserListItemClicked(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,7 @@ public class KeyguardUserSwitcher {
|
||||
}
|
||||
v.setActivated(true);
|
||||
}
|
||||
switchTo(user);
|
||||
onUserListItemClicked(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user