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