• 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.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