Home | History | Annotate | Download | only in users
      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 
     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 
    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 
    133     public Bitmap getNewUserPhotoBitmap() {
    134         return mNewUserPhotoBitmap;
    135     }
    136 
    137     public Drawable getNewUserPhotoDrawable() {
    138         return mNewUserPhotoDrawable;
    139     }
    140 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    391     static Bitmap loadNewUserPhotoBitmap(File file) {
    392         return BitmapFactory.decodeFile(file.getAbsolutePath());
    393     }
    394 
    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          */
    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
    428         public String toString() {
    429             return mTitle;
    430         }
    431 
    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 
    445         final boolean isRestrictedByAdmin() {
    446             return mAdmin != null;
    447         }
    448 
    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> {
    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
    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