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