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