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.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.database.Cursor; 26 import android.graphics.Bitmap; 27 import android.graphics.BitmapFactory; 28 import android.graphics.Canvas; 29 import android.graphics.Paint; 30 import android.graphics.Rect; 31 import android.graphics.Bitmap.Config; 32 import android.graphics.drawable.Drawable; 33 import android.net.Uri; 34 import android.os.AsyncTask; 35 import android.provider.MediaStore; 36 import android.provider.ContactsContract.DisplayPhoto; 37 import android.support.v4.content.FileProvider; 38 import android.util.Log; 39 import android.view.View; 40 import android.view.View.OnClickListener; 41 import android.widget.AdapterView; 42 import android.widget.ArrayAdapter; 43 import android.widget.ImageView; 44 import android.widget.ListAdapter; 45 import android.widget.ListPopupWindow; 46 47 import com.android.settings.R; 48 import com.android.settings.drawable.CircleFramedDrawable; 49 50 import java.io.File; 51 import java.io.FileNotFoundException; 52 import java.io.IOException; 53 import java.io.InputStream; 54 import java.util.ArrayList; 55 import java.util.List; 56 57 public class EditUserPhotoController { 58 private static final String TAG = "EditUserPhotoController"; 59 60 private static final int POPUP_LIST_ITEM_ID_CHOOSE_PHOTO = 1; 61 private static final int POPUP_LIST_ITEM_ID_TAKE_PHOTO = 2; 62 63 // It seems that this class generates custom request codes and they may 64 // collide with ours, these values are very unlikely to have a conflict. 65 private static final int REQUEST_CODE_CHOOSE_PHOTO = 1001; 66 private static final int REQUEST_CODE_TAKE_PHOTO = 1002; 67 private static final int REQUEST_CODE_CROP_PHOTO = 1003; 68 69 private static final String CROP_PICTURE_FILE_NAME = "CropEditUserPhoto.jpg"; 70 private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto2.jpg"; 71 72 private final int mPhotoSize; 73 74 private final Context mContext; 75 private final Fragment mFragment; 76 private final ImageView mImageView; 77 78 private final Uri mCropPictureUri; 79 private final Uri mTakePictureUri; 80 81 private Bitmap mNewUserPhotoBitmap; 82 private Drawable mNewUserPhotoDrawable; 83 84 public EditUserPhotoController(Fragment fragment, ImageView view, 85 Bitmap bitmap, Drawable drawable, boolean waiting) { 86 mContext = view.getContext(); 87 mFragment = fragment; 88 mImageView = view; 89 mCropPictureUri = createTempImageUri(mContext, CROP_PICTURE_FILE_NAME, !waiting); 90 mTakePictureUri = createTempImageUri(mContext, TAKE_PICTURE_FILE_NAME, !waiting); 91 mPhotoSize = getPhotoSize(mContext); 92 mImageView.setOnClickListener(new OnClickListener() { 93 @Override 94 public void onClick(View v) { 95 showUpdatePhotoPopup(); 96 } 97 }); 98 mNewUserPhotoBitmap = bitmap; 99 mNewUserPhotoDrawable = drawable; 100 } 101 102 public boolean onActivityResult(int requestCode, int resultCode, Intent data) { 103 if (resultCode != Activity.RESULT_OK) { 104 return false; 105 } 106 final Uri pictureUri = data != null && data.getData() != null 107 ? data.getData() : mTakePictureUri; 108 switch (requestCode) { 109 case REQUEST_CODE_CROP_PHOTO: 110 onPhotoCropped(pictureUri, true); 111 return true; 112 case REQUEST_CODE_TAKE_PHOTO: 113 case REQUEST_CODE_CHOOSE_PHOTO: 114 cropPhoto(pictureUri); 115 return true; 116 } 117 return false; 118 } 119 120 public Bitmap getNewUserPhotoBitmap() { 121 return mNewUserPhotoBitmap; 122 } 123 124 public Drawable getNewUserPhotoDrawable() { 125 return mNewUserPhotoDrawable; 126 } 127 128 private void showUpdatePhotoPopup() { 129 final boolean canTakePhoto = canTakePhoto(); 130 final boolean canChoosePhoto = canChoosePhoto(); 131 132 if (!canTakePhoto && !canChoosePhoto) { 133 return; 134 } 135 136 Context context = mImageView.getContext(); 137 final List<EditUserPhotoController.AdapterItem> items = new ArrayList<EditUserPhotoController.AdapterItem>(); 138 139 if (canTakePhoto()) { 140 String title = mImageView.getContext().getString( R.string.user_image_take_photo); 141 EditUserPhotoController.AdapterItem item = new AdapterItem(title, POPUP_LIST_ITEM_ID_TAKE_PHOTO); 142 items.add(item); 143 } 144 145 if (canChoosePhoto) { 146 String title = context.getString(R.string.user_image_choose_photo); 147 EditUserPhotoController.AdapterItem item = new AdapterItem(title, POPUP_LIST_ITEM_ID_CHOOSE_PHOTO); 148 items.add(item); 149 } 150 151 final ListPopupWindow listPopupWindow = new ListPopupWindow(context); 152 153 listPopupWindow.setAnchorView(mImageView); 154 listPopupWindow.setModal(true); 155 listPopupWindow.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 156 157 ListAdapter adapter = new ArrayAdapter<EditUserPhotoController.AdapterItem>(context, 158 R.layout.edit_user_photo_popup_item, items); 159 listPopupWindow.setAdapter(adapter); 160 161 final int width = Math.max(mImageView.getWidth(), context.getResources() 162 .getDimensionPixelSize(R.dimen.update_user_photo_popup_min_width)); 163 listPopupWindow.setWidth(width); 164 165 listPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() { 166 @Override 167 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 168 EditUserPhotoController.AdapterItem item = items.get(position); 169 switch (item.id) { 170 case POPUP_LIST_ITEM_ID_CHOOSE_PHOTO: { 171 choosePhoto(); 172 listPopupWindow.dismiss(); 173 } break; 174 case POPUP_LIST_ITEM_ID_TAKE_PHOTO: { 175 takePhoto(); 176 listPopupWindow.dismiss(); 177 } break; 178 } 179 } 180 }); 181 182 listPopupWindow.show(); 183 } 184 185 private boolean canTakePhoto() { 186 return mImageView.getContext().getPackageManager().queryIntentActivities( 187 new Intent(MediaStore.ACTION_IMAGE_CAPTURE), 188 PackageManager.MATCH_DEFAULT_ONLY).size() > 0; 189 } 190 191 private boolean canChoosePhoto() { 192 Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 193 intent.setType("image/*"); 194 return mImageView.getContext().getPackageManager().queryIntentActivities( 195 intent, 0).size() > 0; 196 } 197 198 private void takePhoto() { 199 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 200 appendOutputExtra(intent, mTakePictureUri); 201 mFragment.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO); 202 } 203 204 private void choosePhoto() { 205 Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null); 206 intent.setType("image/*"); 207 appendOutputExtra(intent, mTakePictureUri); 208 mFragment.startActivityForResult(intent, REQUEST_CODE_CHOOSE_PHOTO); 209 } 210 211 private void cropPhoto(Uri pictureUri) { 212 // TODO: Use a public intent, when there is one. 213 Intent intent = new Intent("com.android.camera.action.CROP"); 214 intent.setDataAndType(pictureUri, "image/*"); 215 appendOutputExtra(intent, mCropPictureUri); 216 appendCropExtras(intent); 217 if (intent.resolveActivity(mContext.getPackageManager()) != null) { 218 mFragment.startActivityForResult(intent, REQUEST_CODE_CROP_PHOTO); 219 } else { 220 onPhotoCropped(pictureUri, false); 221 } 222 } 223 224 private void appendOutputExtra(Intent intent, Uri pictureUri) { 225 intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri); 226 intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION 227 | Intent.FLAG_GRANT_READ_URI_PERMISSION); 228 intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, pictureUri)); 229 } 230 231 private void appendCropExtras(Intent intent) { 232 intent.putExtra("crop", "true"); 233 intent.putExtra("scale", true); 234 intent.putExtra("scaleUpIfNeeded", true); 235 intent.putExtra("aspectX", 1); 236 intent.putExtra("aspectY", 1); 237 intent.putExtra("outputX", mPhotoSize); 238 intent.putExtra("outputY", mPhotoSize); 239 } 240 241 private void onPhotoCropped(final Uri data, final boolean cropped) { 242 new AsyncTask<Void, Void, Bitmap>() { 243 @Override 244 protected Bitmap doInBackground(Void... params) { 245 if (cropped) { 246 InputStream imageStream = null; 247 try { 248 imageStream = mContext.getContentResolver() 249 .openInputStream(data); 250 return BitmapFactory.decodeStream(imageStream); 251 } catch (FileNotFoundException fe) { 252 Log.w(TAG, "Cannot find image file", fe); 253 return null; 254 } finally { 255 if (imageStream != null) { 256 try { 257 imageStream.close(); 258 } catch (IOException ioe) { 259 Log.w(TAG, "Cannot close image stream", ioe); 260 } 261 } 262 } 263 } else { 264 // Scale and crop to a square aspect ratio 265 Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize, 266 Config.ARGB_8888); 267 Canvas canvas = new Canvas(croppedImage); 268 Bitmap fullImage = null; 269 try { 270 InputStream imageStream = mContext.getContentResolver() 271 .openInputStream(data); 272 fullImage = BitmapFactory.decodeStream(imageStream); 273 } catch (FileNotFoundException fe) { 274 return null; 275 } 276 if (fullImage != null) { 277 final int squareSize = Math.min(fullImage.getWidth(), 278 fullImage.getHeight()); 279 final int left = (fullImage.getWidth() - squareSize) / 2; 280 final int top = (fullImage.getHeight() - squareSize) / 2; 281 Rect rectSource = new Rect(left, top, 282 left + squareSize, top + squareSize); 283 Rect rectDest = new Rect(0, 0, mPhotoSize, mPhotoSize); 284 Paint paint = new Paint(); 285 canvas.drawBitmap(fullImage, rectSource, rectDest, paint); 286 return croppedImage; 287 } else { 288 // Bah! Got nothin. 289 return null; 290 } 291 } 292 } 293 294 @Override 295 protected void onPostExecute(Bitmap bitmap) { 296 if (bitmap != null) { 297 mNewUserPhotoBitmap = bitmap; 298 mNewUserPhotoDrawable = CircleFramedDrawable 299 .getInstance(mImageView.getContext(), mNewUserPhotoBitmap); 300 mImageView.setImageDrawable(mNewUserPhotoDrawable); 301 } 302 new File(mContext.getCacheDir(), TAKE_PICTURE_FILE_NAME).delete(); 303 new File(mContext.getCacheDir(), CROP_PICTURE_FILE_NAME).delete(); 304 } 305 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); 306 } 307 308 private static int getPhotoSize(Context context) { 309 Cursor cursor = context.getContentResolver().query( 310 DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, 311 new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null); 312 try { 313 cursor.moveToFirst(); 314 return cursor.getInt(0); 315 } finally { 316 cursor.close(); 317 } 318 } 319 320 private Uri createTempImageUri(Context context, String fileName, boolean purge) { 321 final File folder = context.getCacheDir(); 322 folder.mkdirs(); 323 final File fullPath = new File(folder, fileName); 324 if (purge) { 325 fullPath.delete(); 326 } 327 final Uri fileUri = 328 FileProvider.getUriForFile(context, RestrictedProfileSettings.FILE_PROVIDER_AUTHORITY, fullPath); 329 return fileUri; 330 } 331 332 private static final class AdapterItem { 333 final String title; 334 final int id; 335 336 public AdapterItem(String title, int id) { 337 this.title = title; 338 this.id = id; 339 } 340 341 @Override 342 public String toString() { 343 return title; 344 } 345 } 346 }