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