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.Fragment; 21 import android.content.ClipData; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.PackageManager; 26 import android.database.Cursor; 27 import android.graphics.Bitmap; 28 import android.graphics.Bitmap.Config; 29 import android.graphics.BitmapFactory; 30 import android.graphics.Canvas; 31 import android.graphics.Paint; 32 import android.graphics.Rect; 33 import android.graphics.drawable.Drawable; 34 import android.net.Uri; 35 import android.os.AsyncTask; 36 import android.os.StrictMode; 37 import android.os.UserHandle; 38 import android.os.UserManager; 39 import android.provider.ContactsContract.DisplayPhoto; 40 import android.provider.MediaStore; 41 import android.support.v4.content.FileProvider; 42 import android.util.Log; 43 import android.view.Gravity; 44 import android.view.View; 45 import android.view.View.OnClickListener; 46 import android.view.ViewGroup; 47 import android.widget.AdapterView; 48 import android.widget.ArrayAdapter; 49 import android.widget.ImageView; 50 import android.widget.ListPopupWindow; 51 import android.widget.TextView; 52 53 import com.android.settings.R; 54 import com.android.settingslib.RestrictedLockUtils; 55 import com.android.settingslib.drawable.CircleFramedDrawable; 56 57 import libcore.io.Streams; 58 59 import java.io.File; 60 import java.io.FileNotFoundException; 61 import java.io.FileOutputStream; 62 import java.io.IOException; 63 import java.io.InputStream; 64 import java.io.OutputStream; 65 import java.util.ArrayList; 66 import java.util.List; 67 68 public class EditUserPhotoController { 69 private static final String TAG = "EditUserPhotoController"; 70 71 // It seems that this class generates custom request codes and they may 72 // collide with ours, these values are very unlikely to have a conflict. 73 private static final int REQUEST_CODE_CHOOSE_PHOTO = 1001; 74 private static final int REQUEST_CODE_TAKE_PHOTO = 1002; 75 private static final int REQUEST_CODE_CROP_PHOTO = 1003; 76 77 private static final String CROP_PICTURE_FILE_NAME = "CropEditUserPhoto.jpg"; 78 private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto2.jpg"; 79 private static final String NEW_USER_PHOTO_FILE_NAME = "NewUserPhoto.png"; 80 81 private final int mPhotoSize; 82 83 private final Context mContext; 84 private final Fragment mFragment; 85 private final ImageView mImageView; 86 87 private final Uri mCropPictureUri; 88 private final Uri mTakePictureUri; 89 90 private Bitmap mNewUserPhotoBitmap; 91 private Drawable mNewUserPhotoDrawable; 92 EditUserPhotoController(Fragment fragment, ImageView view, Bitmap bitmap, Drawable drawable, boolean waiting)93 public EditUserPhotoController(Fragment fragment, ImageView view, 94 Bitmap bitmap, Drawable drawable, boolean waiting) { 95 mContext = view.getContext(); 96 mFragment = fragment; 97 mImageView = view; 98 mCropPictureUri = createTempImageUri(mContext, CROP_PICTURE_FILE_NAME, !waiting); 99 mTakePictureUri = createTempImageUri(mContext, TAKE_PICTURE_FILE_NAME, !waiting); 100 mPhotoSize = getPhotoSize(mContext); 101 mImageView.setOnClickListener(new OnClickListener() { 102 @Override 103 public void onClick(View v) { 104 showUpdatePhotoPopup(); 105 } 106 }); 107 mNewUserPhotoBitmap = bitmap; 108 mNewUserPhotoDrawable = drawable; 109 } 110 onActivityResult(int requestCode, int resultCode, Intent data)111 public boolean onActivityResult(int requestCode, int resultCode, Intent data) { 112 if (resultCode != Activity.RESULT_OK) { 113 return false; 114 } 115 final Uri pictureUri = data != null && data.getData() != null 116 ? data.getData() : mTakePictureUri; 117 switch (requestCode) { 118 case REQUEST_CODE_CROP_PHOTO: 119 onPhotoCropped(pictureUri, true); 120 return true; 121 case REQUEST_CODE_TAKE_PHOTO: 122 case REQUEST_CODE_CHOOSE_PHOTO: 123 if (mTakePictureUri.equals(pictureUri)) { 124 cropPhoto(); 125 } else { 126 copyAndCropPhoto(pictureUri); 127 } 128 return true; 129 } 130 return false; 131 } 132 getNewUserPhotoBitmap()133 public Bitmap getNewUserPhotoBitmap() { 134 return mNewUserPhotoBitmap; 135 } 136 getNewUserPhotoDrawable()137 public Drawable getNewUserPhotoDrawable() { 138 return mNewUserPhotoDrawable; 139 } 140 showUpdatePhotoPopup()141 private void showUpdatePhotoPopup() { 142 final boolean canTakePhoto = canTakePhoto(); 143 final boolean canChoosePhoto = canChoosePhoto(); 144 145 if (!canTakePhoto && !canChoosePhoto) { 146 return; 147 } 148 149 final Context context = mImageView.getContext(); 150 final List<EditUserPhotoController.RestrictedMenuItem> items = new ArrayList<>(); 151 152 if (canTakePhoto) { 153 final String title = context.getString(R.string.user_image_take_photo); 154 final Runnable action = new Runnable() { 155 @Override 156 public void run() { 157 takePhoto(); 158 } 159 }; 160 items.add(new RestrictedMenuItem(context, title, UserManager.DISALLOW_SET_USER_ICON, 161 action)); 162 } 163 164 if (canChoosePhoto) { 165 final String title = context.getString(R.string.user_image_choose_photo); 166 final Runnable action = new Runnable() { 167 @Override 168 public void run() { 169 choosePhoto(); 170 } 171 }; 172 items.add(new RestrictedMenuItem(context, title, UserManager.DISALLOW_SET_USER_ICON, 173 action)); 174 } 175 176 final ListPopupWindow listPopupWindow = new ListPopupWindow(context); 177 178 listPopupWindow.setAnchorView(mImageView); 179 listPopupWindow.setModal(true); 180 listPopupWindow.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 181 listPopupWindow.setAdapter(new RestrictedPopupMenuAdapter(context, items)); 182 183 final int width = Math.max(mImageView.getWidth(), context.getResources() 184 .getDimensionPixelSize(R.dimen.update_user_photo_popup_min_width)); 185 listPopupWindow.setWidth(width); 186 listPopupWindow.setDropDownGravity(Gravity.START); 187 188 listPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() { 189 @Override 190 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 191 listPopupWindow.dismiss(); 192 final RestrictedMenuItem item = 193 (RestrictedMenuItem) parent.getAdapter().getItem(position); 194 item.doAction(); 195 } 196 }); 197 198 listPopupWindow.show(); 199 } 200 canTakePhoto()201 private boolean canTakePhoto() { 202 return mImageView.getContext().getPackageManager().queryIntentActivities( 203 new Intent(MediaStore.ACTION_IMAGE_CAPTURE), 204 PackageManager.MATCH_DEFAULT_ONLY).size() > 0; 205 } 206 canChoosePhoto()207 private boolean canChoosePhoto() { 208 Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 209 intent.setType("image/*"); 210 return mImageView.getContext().getPackageManager().queryIntentActivities( 211 intent, 0).size() > 0; 212 } 213 takePhoto()214 private void takePhoto() { 215 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 216 appendOutputExtra(intent, mTakePictureUri); 217 mFragment.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO); 218 } 219 choosePhoto()220 private void choosePhoto() { 221 Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null); 222 intent.setType("image/*"); 223 appendOutputExtra(intent, mTakePictureUri); 224 mFragment.startActivityForResult(intent, REQUEST_CODE_CHOOSE_PHOTO); 225 } 226 copyAndCropPhoto(final Uri pictureUri)227 private void copyAndCropPhoto(final Uri pictureUri) { 228 new AsyncTask<Void, Void, Void>() { 229 @Override 230 protected Void doInBackground(Void... params) { 231 final ContentResolver cr = mContext.getContentResolver(); 232 try (InputStream in = cr.openInputStream(pictureUri); 233 OutputStream out = cr.openOutputStream(mTakePictureUri)) { 234 Streams.copy(in, out); 235 } catch (IOException e) { 236 Log.w(TAG, "Failed to copy photo", e); 237 } 238 return null; 239 } 240 241 @Override 242 protected void onPostExecute(Void result) { 243 if (!mFragment.isAdded()) return; 244 cropPhoto(); 245 } 246 }.execute(); 247 } 248 cropPhoto()249 private void cropPhoto() { 250 // TODO: Use a public intent, when there is one. 251 Intent intent = new Intent("com.android.camera.action.CROP"); 252 intent.setDataAndType(mTakePictureUri, "image/*"); 253 appendOutputExtra(intent, mCropPictureUri); 254 appendCropExtras(intent); 255 if (intent.resolveActivity(mContext.getPackageManager()) != null) { 256 try { 257 StrictMode.disableDeathOnFileUriExposure(); 258 mFragment.startActivityForResult(intent, REQUEST_CODE_CROP_PHOTO); 259 } finally { 260 StrictMode.enableDeathOnFileUriExposure(); 261 } 262 } else { 263 onPhotoCropped(mTakePictureUri, false); 264 } 265 } 266 appendOutputExtra(Intent intent, Uri pictureUri)267 private void appendOutputExtra(Intent intent, Uri pictureUri) { 268 intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri); 269 intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION 270 | Intent.FLAG_GRANT_READ_URI_PERMISSION); 271 intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, pictureUri)); 272 } 273 appendCropExtras(Intent intent)274 private void appendCropExtras(Intent intent) { 275 intent.putExtra("crop", "true"); 276 intent.putExtra("scale", true); 277 intent.putExtra("scaleUpIfNeeded", true); 278 intent.putExtra("aspectX", 1); 279 intent.putExtra("aspectY", 1); 280 intent.putExtra("outputX", mPhotoSize); 281 intent.putExtra("outputY", mPhotoSize); 282 } 283 onPhotoCropped(final Uri data, final boolean cropped)284 private void onPhotoCropped(final Uri data, final boolean cropped) { 285 new AsyncTask<Void, Void, Bitmap>() { 286 @Override 287 protected Bitmap doInBackground(Void... params) { 288 if (cropped) { 289 InputStream imageStream = null; 290 try { 291 imageStream = mContext.getContentResolver() 292 .openInputStream(data); 293 return BitmapFactory.decodeStream(imageStream); 294 } catch (FileNotFoundException fe) { 295 Log.w(TAG, "Cannot find image file", fe); 296 return null; 297 } finally { 298 if (imageStream != null) { 299 try { 300 imageStream.close(); 301 } catch (IOException ioe) { 302 Log.w(TAG, "Cannot close image stream", ioe); 303 } 304 } 305 } 306 } else { 307 // Scale and crop to a square aspect ratio 308 Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize, 309 Config.ARGB_8888); 310 Canvas canvas = new Canvas(croppedImage); 311 Bitmap fullImage = null; 312 try { 313 InputStream imageStream = mContext.getContentResolver() 314 .openInputStream(data); 315 fullImage = BitmapFactory.decodeStream(imageStream); 316 } catch (FileNotFoundException fe) { 317 return null; 318 } 319 if (fullImage != null) { 320 final int squareSize = Math.min(fullImage.getWidth(), 321 fullImage.getHeight()); 322 final int left = (fullImage.getWidth() - squareSize) / 2; 323 final int top = (fullImage.getHeight() - squareSize) / 2; 324 Rect rectSource = new Rect(left, top, 325 left + squareSize, top + squareSize); 326 Rect rectDest = new Rect(0, 0, mPhotoSize, mPhotoSize); 327 Paint paint = new Paint(); 328 canvas.drawBitmap(fullImage, rectSource, rectDest, paint); 329 return croppedImage; 330 } else { 331 // Bah! Got nothin. 332 return null; 333 } 334 } 335 } 336 337 @Override 338 protected void onPostExecute(Bitmap bitmap) { 339 if (bitmap != null) { 340 mNewUserPhotoBitmap = bitmap; 341 mNewUserPhotoDrawable = CircleFramedDrawable 342 .getInstance(mImageView.getContext(), mNewUserPhotoBitmap); 343 mImageView.setImageDrawable(mNewUserPhotoDrawable); 344 } 345 new File(mContext.getCacheDir(), TAKE_PICTURE_FILE_NAME).delete(); 346 new File(mContext.getCacheDir(), CROP_PICTURE_FILE_NAME).delete(); 347 } 348 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); 349 } 350 getPhotoSize(Context context)351 private static int getPhotoSize(Context context) { 352 Cursor cursor = context.getContentResolver().query( 353 DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, 354 new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null); 355 try { 356 cursor.moveToFirst(); 357 return cursor.getInt(0); 358 } finally { 359 cursor.close(); 360 } 361 } 362 createTempImageUri(Context context, String fileName, boolean purge)363 private Uri createTempImageUri(Context context, String fileName, boolean purge) { 364 final File folder = context.getCacheDir(); 365 folder.mkdirs(); 366 final File fullPath = new File(folder, fileName); 367 if (purge) { 368 fullPath.delete(); 369 } 370 return FileProvider.getUriForFile(context, 371 RestrictedProfileSettings.FILE_PROVIDER_AUTHORITY, fullPath); 372 } 373 saveNewUserPhotoBitmap()374 File saveNewUserPhotoBitmap() { 375 if (mNewUserPhotoBitmap == null) { 376 return null; 377 } 378 try { 379 File file = new File(mContext.getCacheDir(), NEW_USER_PHOTO_FILE_NAME); 380 OutputStream os = new FileOutputStream(file); 381 mNewUserPhotoBitmap.compress(Bitmap.CompressFormat.PNG, 100, os); 382 os.flush(); 383 os.close(); 384 return file; 385 } catch (IOException e) { 386 Log.e(TAG, "Cannot create temp file", e); 387 } 388 return null; 389 } 390 loadNewUserPhotoBitmap(File file)391 static Bitmap loadNewUserPhotoBitmap(File file) { 392 return BitmapFactory.decodeFile(file.getAbsolutePath()); 393 } 394 removeNewUserPhotoBitmapFile()395 void removeNewUserPhotoBitmapFile() { 396 new File(mContext.getCacheDir(), NEW_USER_PHOTO_FILE_NAME).delete(); 397 } 398 399 private static final class RestrictedMenuItem { 400 private final Context mContext; 401 private final String mTitle; 402 private final Runnable mAction; 403 private final RestrictedLockUtils.EnforcedAdmin mAdmin; 404 // Restriction may be set by system or something else via UserManager.setUserRestriction(). 405 private final boolean mIsRestrictedByBase; 406 407 /** 408 * The menu item, used for popup menu. Any element of such a menu can be disabled by admin. 409 * @param context A context. 410 * @param title The title of the menu item. 411 * @param restriction The restriction, that if is set, blocks the menu item. 412 * @param action The action on menu item click. 413 */ RestrictedMenuItem(Context context, String title, String restriction, Runnable action)414 public RestrictedMenuItem(Context context, String title, String restriction, 415 Runnable action) { 416 mContext = context; 417 mTitle = title; 418 mAction = action; 419 420 final int myUserId = UserHandle.myUserId(); 421 mAdmin = RestrictedLockUtils.checkIfRestrictionEnforced(context, 422 restriction, myUserId); 423 mIsRestrictedByBase = RestrictedLockUtils.hasBaseUserRestriction(mContext, 424 restriction, myUserId); 425 } 426 427 @Override toString()428 public String toString() { 429 return mTitle; 430 } 431 doAction()432 final void doAction() { 433 if (isRestrictedByBase()) { 434 return; 435 } 436 437 if (isRestrictedByAdmin()) { 438 RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mContext, mAdmin); 439 return; 440 } 441 442 mAction.run(); 443 } 444 isRestrictedByAdmin()445 final boolean isRestrictedByAdmin() { 446 return mAdmin != null; 447 } 448 isRestrictedByBase()449 final boolean isRestrictedByBase() { 450 return mIsRestrictedByBase; 451 } 452 } 453 454 /** 455 * Provide this adapter to ListPopupWindow.setAdapter() to have a popup window menu, where 456 * any element can be restricted by admin (profile owner or device owner). 457 */ 458 private static final class RestrictedPopupMenuAdapter extends ArrayAdapter<RestrictedMenuItem> { RestrictedPopupMenuAdapter(Context context, List<RestrictedMenuItem> items)459 public RestrictedPopupMenuAdapter(Context context, List<RestrictedMenuItem> items) { 460 super(context, R.layout.restricted_popup_menu_item, R.id.text, items); 461 } 462 463 @Override getView(int position, View convertView, ViewGroup parent)464 public View getView(int position, View convertView, ViewGroup parent) { 465 final View view = super.getView(position, convertView, parent); 466 final RestrictedMenuItem item = getItem(position); 467 final TextView text = (TextView) view.findViewById(R.id.text); 468 final ImageView image = (ImageView) view.findViewById(R.id.restricted_icon); 469 470 text.setEnabled(!item.isRestrictedByAdmin() && !item.isRestrictedByBase()); 471 image.setVisibility(item.isRestrictedByAdmin() && !item.isRestrictedByBase() ? 472 ImageView.VISIBLE : ImageView.GONE); 473 474 return view; 475 } 476 } 477 } 478