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