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 
EditUserPhotoController(Fragment fragment, ImageView view, Bitmap bitmap, Drawable drawable, boolean waiting)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 
onActivityResult(int requestCode, int resultCode, Intent data)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 
getNewUserPhotoBitmap()120     public Bitmap getNewUserPhotoBitmap() {
121         return mNewUserPhotoBitmap;
122     }
123 
getNewUserPhotoDrawable()124     public Drawable getNewUserPhotoDrawable() {
125         return mNewUserPhotoDrawable;
126     }
127 
showUpdatePhotoPopup()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 
canTakePhoto()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 
canChoosePhoto()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 
takePhoto()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 
choosePhoto()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 
cropPhoto(Uri pictureUri)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 
appendOutputExtra(Intent intent, Uri pictureUri)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 
appendCropExtras(Intent intent)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 
onPhotoCropped(final Uri data, final boolean cropped)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 
getPhotoSize(Context context)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 
createTempImageUri(Context context, String fileName, boolean purge)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 
AdapterItem(String title, int id)336         public AdapterItem(String title, int id) {
337             this.title = title;
338             this.id = id;
339         }
340 
341         @Override
toString()342         public String toString() {
343             return title;
344         }
345     }
346 }