• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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