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