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