• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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