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