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.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 
     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 
    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 
    126     public Bitmap getNewUserPhotoBitmap() {
    127         return mNewUserPhotoBitmap;
    128     }
    129 
    130     public Drawable getNewUserPhotoDrawable() {
    131         return mNewUserPhotoDrawable;
    132     }
    133 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    362     static Bitmap loadNewUserPhotoBitmap(File file) {
    363         return BitmapFactory.decodeFile(file.getAbsolutePath());
    364     }
    365 
    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          */
    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
    399         public String toString() {
    400             return mTitle;
    401         }
    402 
    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 
    416         final boolean isRestrictedByAdmin() {
    417             return mAdmin != null;
    418         }
    419 
    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> {
    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
    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