1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.users; 18 19 import android.app.Activity; 20 import android.app.AlertDialog; 21 import android.app.Dialog; 22 import android.app.Fragment; 23 import android.content.ClipData; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.content.Intent; 27 import android.content.pm.PackageManager; 28 import android.content.pm.UserInfo; 29 import android.database.Cursor; 30 import android.graphics.Bitmap; 31 import android.graphics.Bitmap.Config; 32 import android.graphics.BitmapFactory; 33 import android.graphics.Canvas; 34 import android.graphics.Paint; 35 import android.graphics.Rect; 36 import android.graphics.drawable.Drawable; 37 import android.net.Uri; 38 import android.os.AsyncTask; 39 import android.os.Bundle; 40 import android.os.UserHandle; 41 import android.provider.MediaStore; 42 import android.provider.ContactsContract.DisplayPhoto; 43 import android.support.v4.content.FileProvider; 44 import android.text.TextUtils; 45 import android.util.Log; 46 import android.view.LayoutInflater; 47 import android.view.View; 48 import android.view.ViewGroup; 49 import android.view.WindowManager; 50 import android.view.View.OnClickListener; 51 import android.widget.AdapterView; 52 import android.widget.ArrayAdapter; 53 import android.widget.EditText; 54 import android.widget.ImageView; 55 import android.widget.ListAdapter; 56 import android.widget.ListPopupWindow; 57 import android.widget.TextView; 58 59 import com.android.settings.R; 60 61 import java.io.File; 62 import java.io.FileNotFoundException; 63 import java.io.InputStream; 64 import java.util.ArrayList; 65 import java.util.List; 66 67 public class RestrictedProfileSettings extends AppRestrictionsFragment { 68 69 private static final String KEY_SAVED_PHOTO = "pending_photo"; 70 private static final String KEY_AWAITING_RESULT = "awaiting_result"; 71 private static final int DIALOG_ID_EDIT_USER_INFO = 1; 72 public static final String FILE_PROVIDER_AUTHORITY = "com.android.settings.files"; 73 74 private View mHeaderView; 75 private ImageView mUserIconView; 76 private TextView mUserNameView; 77 78 private Dialog mEditUserInfoDialog; 79 private EditUserPhotoController mEditUserPhotoController; 80 private Bitmap mSavedPhoto; 81 private boolean mWaitingForActivityResult; 82 83 @Override 84 public void onCreate(Bundle icicle) { 85 super.onCreate(icicle); 86 87 if (icicle != null) { 88 mSavedPhoto = (Bitmap) icicle.getParcelable(KEY_SAVED_PHOTO); 89 mWaitingForActivityResult = icicle.getBoolean(KEY_AWAITING_RESULT, false); 90 } 91 92 init(icicle); 93 } 94 95 @Override 96 public void onActivityCreated(Bundle savedInstanceState) { 97 if (mHeaderView == null) { 98 mHeaderView = LayoutInflater.from(getActivity()).inflate( 99 R.layout.user_info_header, null); 100 ((ViewGroup) getListView().getParent()).addView(mHeaderView, 0); 101 mHeaderView.setOnClickListener(this); 102 mUserIconView = (ImageView) mHeaderView.findViewById(android.R.id.icon); 103 mUserNameView = (TextView) mHeaderView.findViewById(android.R.id.title); 104 getListView().setFastScrollEnabled(true); 105 } 106 // This is going to bind the preferences. 107 super.onActivityCreated(savedInstanceState); 108 } 109 110 @Override 111 public void onSaveInstanceState(Bundle outState) { 112 super.onSaveInstanceState(outState); 113 if (mEditUserInfoDialog != null && mEditUserInfoDialog.isShowing() 114 && mEditUserPhotoController != null) { 115 outState.putParcelable(KEY_SAVED_PHOTO, 116 mEditUserPhotoController.getNewUserPhotoBitmap()); 117 } 118 if (mWaitingForActivityResult) { 119 outState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult); 120 } 121 } 122 123 @Override 124 public void onResume() { 125 super.onResume(); 126 127 // Check if user still exists 128 UserInfo info = getExistingUser(mUser); 129 if (info == null) { 130 finishFragment(); 131 } else { 132 ((TextView) mHeaderView.findViewById(android.R.id.title)).setText(info.name); 133 ((ImageView) mHeaderView.findViewById(android.R.id.icon)).setImageDrawable( 134 getCircularUserIcon()); 135 } 136 } 137 138 private UserInfo getExistingUser(UserHandle thisUser) { 139 final List<UserInfo> users = mUserManager.getUsers(true); // Only get non-dying 140 for (UserInfo user : users) { 141 if (user.id == thisUser.getIdentifier()) { 142 return user; 143 } 144 } 145 return null; 146 } 147 148 @Override 149 public void startActivityForResult(Intent intent, int requestCode) { 150 mWaitingForActivityResult = true; 151 super.startActivityForResult(intent, requestCode); 152 } 153 154 @Override 155 public void onActivityResult(int requestCode, int resultCode, Intent data) { 156 super.onActivityResult(requestCode, resultCode, data); 157 mWaitingForActivityResult = false; 158 159 if (mEditUserInfoDialog != null && mEditUserInfoDialog.isShowing() 160 && mEditUserPhotoController.onActivityResult(requestCode, resultCode, data)) { 161 return; 162 } 163 } 164 165 @Override 166 public void onClick(View view) { 167 if (view == mHeaderView) { 168 showDialog(DIALOG_ID_EDIT_USER_INFO); 169 } else { 170 super.onClick(view); // in AppRestrictionsFragment 171 } 172 } 173 174 @Override 175 public Dialog onCreateDialog(int dialogId) { 176 if (dialogId == DIALOG_ID_EDIT_USER_INFO) { 177 if (mEditUserInfoDialog != null) { 178 return mEditUserInfoDialog; 179 } 180 181 LayoutInflater inflater = getActivity().getLayoutInflater(); 182 View content = inflater.inflate(R.layout.edit_user_info_dialog_content, null); 183 184 UserInfo info = mUserManager.getUserInfo(mUser.getIdentifier()); 185 186 final EditText userNameView = (EditText) content.findViewById(R.id.user_name); 187 userNameView.setText(info.name); 188 189 final ImageView userPhotoView = (ImageView) content.findViewById(R.id.user_photo); 190 Drawable drawable = null; 191 if (mSavedPhoto != null) { 192 drawable = CircleFramedDrawable.getInstance(getActivity(), mSavedPhoto); 193 } else { 194 drawable = mUserIconView.getDrawable(); 195 if (drawable == null) { 196 drawable = getCircularUserIcon(); 197 } 198 } 199 userPhotoView.setImageDrawable(drawable); 200 mEditUserPhotoController = new EditUserPhotoController(this, userPhotoView, 201 mSavedPhoto, drawable, mWaitingForActivityResult); 202 203 mEditUserInfoDialog = new AlertDialog.Builder(getActivity()) 204 .setTitle(R.string.profile_info_settings_title) 205 .setIconAttribute(R.drawable.ic_settings_multiuser) 206 .setView(content) 207 .setCancelable(true) 208 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 209 @Override 210 public void onClick(DialogInterface dialog, int which) { 211 if (which == DialogInterface.BUTTON_POSITIVE) { 212 // Update the name if changed. 213 CharSequence userName = userNameView.getText(); 214 if (!TextUtils.isEmpty(userName)) { 215 CharSequence oldUserName = mUserNameView.getText(); 216 if (oldUserName == null 217 || !userName.toString().equals(oldUserName.toString())) { 218 ((TextView) mHeaderView.findViewById(android.R.id.title)) 219 .setText(userName.toString()); 220 mUserManager.setUserName(mUser.getIdentifier(), 221 userName.toString()); 222 } 223 } 224 // Update the photo if changed. 225 Drawable drawable = mEditUserPhotoController.getNewUserPhotoDrawable(); 226 Bitmap bitmap = mEditUserPhotoController.getNewUserPhotoBitmap(); 227 if (drawable != null && bitmap != null 228 && !drawable.equals(mUserIconView.getDrawable())) { 229 mUserIconView.setImageDrawable(drawable); 230 new AsyncTask<Void, Void, Void>() { 231 @Override 232 protected Void doInBackground(Void... params) { 233 mUserManager.setUserIcon(mUser.getIdentifier(), 234 mEditUserPhotoController.getNewUserPhotoBitmap()); 235 return null; 236 } 237 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); 238 } 239 removeDialog(DIALOG_ID_EDIT_USER_INFO); 240 } 241 clearEditUserInfoDialog(); 242 } 243 }) 244 .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 245 @Override 246 public void onClick(DialogInterface dialog, int which) { 247 clearEditUserInfoDialog(); 248 } 249 }) 250 .create(); 251 252 // Make sure the IME is up. 253 mEditUserInfoDialog.getWindow().setSoftInputMode( 254 WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); 255 256 return mEditUserInfoDialog; 257 } 258 259 return null; 260 } 261 262 private void clearEditUserInfoDialog() { 263 mEditUserInfoDialog = null; 264 mSavedPhoto = null; 265 } 266 267 private static class EditUserPhotoController { 268 private static final int POPUP_LIST_ITEM_ID_CHOOSE_PHOTO = 1; 269 private static final int POPUP_LIST_ITEM_ID_TAKE_PHOTO = 2; 270 271 // It seems that this class generates custom request codes and they may 272 // collide with ours, these values are very unlikely to have a conflict. 273 private static final int REQUEST_CODE_CHOOSE_PHOTO = 1; 274 private static final int REQUEST_CODE_TAKE_PHOTO = 2; 275 private static final int REQUEST_CODE_CROP_PHOTO = 3; 276 277 private static final String CROP_PICTURE_FILE_NAME = "CropEditUserPhoto.jpg"; 278 private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto2.jpg"; 279 280 private final int mPhotoSize; 281 282 private final Context mContext; 283 private final Fragment mFragment; 284 private final ImageView mImageView; 285 286 private final Uri mCropPictureUri; 287 private final Uri mTakePictureUri; 288 289 private Bitmap mNewUserPhotoBitmap; 290 private Drawable mNewUserPhotoDrawable; 291 292 public EditUserPhotoController(Fragment fragment, ImageView view, 293 Bitmap bitmap, Drawable drawable, boolean waiting) { 294 mContext = view.getContext(); 295 mFragment = fragment; 296 mImageView = view; 297 mCropPictureUri = createTempImageUri(mContext, CROP_PICTURE_FILE_NAME, !waiting); 298 mTakePictureUri = createTempImageUri(mContext, TAKE_PICTURE_FILE_NAME, !waiting); 299 mPhotoSize = getPhotoSize(mContext); 300 mImageView.setOnClickListener(new OnClickListener() { 301 @Override 302 public void onClick(View v) { 303 showUpdatePhotoPopup(); 304 } 305 }); 306 mNewUserPhotoBitmap = bitmap; 307 mNewUserPhotoDrawable = drawable; 308 } 309 310 public boolean onActivityResult(int requestCode, int resultCode, Intent data) { 311 if (resultCode != Activity.RESULT_OK) { 312 return false; 313 } 314 final Uri pictureUri = data != null && data.getData() != null 315 ? data.getData() : mTakePictureUri; 316 switch (requestCode) { 317 case REQUEST_CODE_CROP_PHOTO: 318 onPhotoCropped(pictureUri, true); 319 return true; 320 case REQUEST_CODE_TAKE_PHOTO: 321 case REQUEST_CODE_CHOOSE_PHOTO: 322 cropPhoto(pictureUri); 323 return true; 324 } 325 return false; 326 } 327 328 public Bitmap getNewUserPhotoBitmap() { 329 return mNewUserPhotoBitmap; 330 } 331 332 public Drawable getNewUserPhotoDrawable() { 333 return mNewUserPhotoDrawable; 334 } 335 336 private void showUpdatePhotoPopup() { 337 final boolean canTakePhoto = canTakePhoto(); 338 final boolean canChoosePhoto = canChoosePhoto(); 339 340 if (!canTakePhoto && !canChoosePhoto) { 341 return; 342 } 343 344 Context context = mImageView.getContext(); 345 final List<AdapterItem> items = new ArrayList<AdapterItem>(); 346 347 if (canTakePhoto()) { 348 String title = mImageView.getContext().getString( R.string.user_image_take_photo); 349 AdapterItem item = new AdapterItem(title, POPUP_LIST_ITEM_ID_TAKE_PHOTO); 350 items.add(item); 351 } 352 353 if (canChoosePhoto) { 354 String title = context.getString(R.string.user_image_choose_photo); 355 AdapterItem item = new AdapterItem(title, POPUP_LIST_ITEM_ID_CHOOSE_PHOTO); 356 items.add(item); 357 } 358 359 final ListPopupWindow listPopupWindow = new ListPopupWindow(context); 360 361 listPopupWindow.setAnchorView(mImageView); 362 listPopupWindow.setModal(true); 363 listPopupWindow.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 364 365 ListAdapter adapter = new ArrayAdapter<AdapterItem>(context, 366 R.layout.edit_user_photo_popup_item, items); 367 listPopupWindow.setAdapter(adapter); 368 369 final int width = Math.max(mImageView.getWidth(), context.getResources() 370 .getDimensionPixelSize(R.dimen.update_user_photo_popup_min_width)); 371 listPopupWindow.setWidth(width); 372 373 listPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() { 374 @Override 375 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 376 AdapterItem item = items.get(position); 377 switch (item.id) { 378 case POPUP_LIST_ITEM_ID_CHOOSE_PHOTO: { 379 choosePhoto(); 380 listPopupWindow.dismiss(); 381 } break; 382 case POPUP_LIST_ITEM_ID_TAKE_PHOTO: { 383 takePhoto(); 384 listPopupWindow.dismiss(); 385 } break; 386 } 387 } 388 }); 389 390 listPopupWindow.show(); 391 } 392 393 private boolean canTakePhoto() { 394 return mImageView.getContext().getPackageManager().queryIntentActivities( 395 new Intent(MediaStore.ACTION_IMAGE_CAPTURE), 396 PackageManager.MATCH_DEFAULT_ONLY).size() > 0; 397 } 398 399 private boolean canChoosePhoto() { 400 Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 401 intent.setType("image/*"); 402 return mImageView.getContext().getPackageManager().queryIntentActivities( 403 intent, 0).size() > 0; 404 } 405 406 private void takePhoto() { 407 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 408 appendOutputExtra(intent, mTakePictureUri); 409 mFragment.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO); 410 } 411 412 private void choosePhoto() { 413 Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null); 414 intent.setType("image/*"); 415 appendOutputExtra(intent, mTakePictureUri); 416 mFragment.startActivityForResult(intent, REQUEST_CODE_CHOOSE_PHOTO); 417 } 418 419 private void cropPhoto(Uri pictureUri) { 420 // TODO: Use a public intent, when there is one. 421 Intent intent = new Intent("com.android.camera.action.CROP"); 422 intent.setDataAndType(pictureUri, "image/*"); 423 appendOutputExtra(intent, mCropPictureUri); 424 appendCropExtras(intent); 425 if (intent.resolveActivity(mContext.getPackageManager()) != null) { 426 mFragment.startActivityForResult(intent, REQUEST_CODE_CROP_PHOTO); 427 } else { 428 onPhotoCropped(pictureUri, false); 429 } 430 } 431 432 private void appendOutputExtra(Intent intent, Uri pictureUri) { 433 intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri); 434 intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION 435 | Intent.FLAG_GRANT_READ_URI_PERMISSION); 436 intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, pictureUri)); 437 } 438 439 private void appendCropExtras(Intent intent) { 440 intent.putExtra("crop", "true"); 441 intent.putExtra("scale", true); 442 intent.putExtra("scaleUpIfNeeded", true); 443 intent.putExtra("aspectX", 1); 444 intent.putExtra("aspectY", 1); 445 intent.putExtra("outputX", mPhotoSize); 446 intent.putExtra("outputY", mPhotoSize); 447 } 448 449 private void onPhotoCropped(final Uri data, final boolean cropped) { 450 new AsyncTask<Void, Void, Bitmap>() { 451 @Override 452 protected Bitmap doInBackground(Void... params) { 453 if (cropped) { 454 try { 455 InputStream imageStream = mContext.getContentResolver() 456 .openInputStream(data); 457 return BitmapFactory.decodeStream(imageStream); 458 } catch (FileNotFoundException fe) { 459 return null; 460 } 461 } else { 462 // Scale and crop to a square aspect ratio 463 Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize, 464 Config.ARGB_8888); 465 Canvas canvas = new Canvas(croppedImage); 466 Bitmap fullImage = null; 467 try { 468 InputStream imageStream = mContext.getContentResolver() 469 .openInputStream(data); 470 fullImage = BitmapFactory.decodeStream(imageStream); 471 } catch (FileNotFoundException fe) { 472 return null; 473 } 474 if (fullImage != null) { 475 final int squareSize = Math.min(fullImage.getWidth(), 476 fullImage.getHeight()); 477 final int left = (fullImage.getWidth() - squareSize) / 2; 478 final int top = (fullImage.getHeight() - squareSize) / 2; 479 Rect rectSource = new Rect(left, top, 480 left + squareSize, top + squareSize); 481 Rect rectDest = new Rect(0, 0, mPhotoSize, mPhotoSize); 482 Paint paint = new Paint(); 483 canvas.drawBitmap(fullImage, rectSource, rectDest, paint); 484 return croppedImage; 485 } else { 486 // Bah! Got nothin. 487 return null; 488 } 489 } 490 } 491 492 @Override 493 protected void onPostExecute(Bitmap bitmap) { 494 if (bitmap != null) { 495 mNewUserPhotoBitmap = bitmap; 496 mNewUserPhotoDrawable = CircleFramedDrawable 497 .getInstance(mImageView.getContext(), mNewUserPhotoBitmap); 498 mImageView.setImageDrawable(mNewUserPhotoDrawable); 499 } 500 new File(mContext.getCacheDir(), TAKE_PICTURE_FILE_NAME).delete(); 501 new File(mContext.getCacheDir(), CROP_PICTURE_FILE_NAME).delete(); 502 } 503 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); 504 } 505 506 private static int getPhotoSize(Context context) { 507 Cursor cursor = context.getContentResolver().query( 508 DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, 509 new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null); 510 try { 511 cursor.moveToFirst(); 512 return cursor.getInt(0); 513 } finally { 514 cursor.close(); 515 } 516 } 517 518 private Uri createTempImageUri(Context context, String fileName, boolean purge) { 519 final File folder = context.getCacheDir(); 520 folder.mkdirs(); 521 final File fullPath = new File(folder, fileName); 522 if (purge) { 523 fullPath.delete(); 524 } 525 final Uri fileUri = 526 FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, fullPath); 527 return fileUri; 528 } 529 530 private static final class AdapterItem { 531 final String title; 532 final int id; 533 534 public AdapterItem(String title, int id) { 535 this.title = title; 536 this.id = id; 537 } 538 539 @Override 540 public String toString() { 541 return title; 542 } 543 } 544 } 545 546 } 547