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 
17 package com.android.settingslib.users;
18 
19 import android.app.Activity;
20 import android.content.Intent;
21 import android.content.res.Resources;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapFactory;
24 import android.graphics.drawable.Drawable;
25 import android.multiuser.Flags;
26 import android.net.Uri;
27 import android.util.Log;
28 import android.widget.ImageView;
29 
30 import androidx.annotation.NonNull;
31 import androidx.annotation.Nullable;
32 
33 import com.android.internal.util.UserIcons;
34 import com.android.settingslib.drawable.CircleFramedDrawable;
35 import com.android.settingslib.utils.ThreadUtils;
36 
37 import com.google.common.util.concurrent.FutureCallback;
38 import com.google.common.util.concurrent.Futures;
39 import com.google.common.util.concurrent.ListenableFuture;
40 import com.google.common.util.concurrent.ListeningExecutorService;
41 
42 import java.io.File;
43 import java.io.FileNotFoundException;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.io.OutputStream;
48 
49 /**
50  * This class contains logic for starting activities to take/choose/crop photo, reads and transforms
51  * the result image.
52  */
53 public class EditUserPhotoController {
54     private static final String TAG = "EditUserPhotoController";
55 
56     // It seems that this class generates custom request codes and they may
57     // collide with ours, these values are very unlikely to have a conflict.
58     private static final int REQUEST_CODE_PICK_AVATAR = 1004;
59 
60     private static final String IMAGES_DIR = "multi_user";
61     private static final String NEW_USER_PHOTO_FILE_NAME = "NewUserPhoto.png";
62 
63     private static final String AVATAR_PICKER_ACTION = "com.android.avatarpicker"
64             + ".FULL_SCREEN_ACTIVITY";
65     private static final String EXTRA_FILE_AUTHORITY = "file_authority";
66     private static final String EXTRA_DEFAULT_ICON_TINT_COLOR = "default_icon_tint_color";
67 
68     static final String EXTRA_IS_USER_NEW = "is_user_new";
69 
70     private final Activity mActivity;
71     private final ActivityStarter mActivityStarter;
72     private final ImageView mImageView;
73     private final String mFileAuthority;
74     private final ListeningExecutorService mExecutorService;
75     private final File mImagesDir;
76     private Bitmap mNewUserPhotoBitmap;
77     private Drawable mNewUserPhotoDrawable;
78     private String mCachedDrawablePath;
79 
EditUserPhotoController(Activity activity, ActivityStarter activityStarter, ImageView view, Bitmap savedBitmap, Drawable savedDrawable, String fileAuthority)80     public EditUserPhotoController(Activity activity, ActivityStarter activityStarter,
81             ImageView view, Bitmap savedBitmap, Drawable savedDrawable, String fileAuthority) {
82         this(activity, activityStarter, view, savedBitmap, savedDrawable, fileAuthority, true);
83     }
84 
EditUserPhotoController(Activity activity, ActivityStarter activityStarter, ImageView view, Bitmap savedBitmap, Drawable savedDrawable, String fileAuthority, boolean isUserNew)85     public EditUserPhotoController(Activity activity, ActivityStarter activityStarter,
86             ImageView view, Bitmap savedBitmap, Drawable savedDrawable, String fileAuthority,
87             boolean isUserNew) {
88         mActivity = activity;
89         mActivityStarter = activityStarter;
90         mFileAuthority = fileAuthority;
91 
92         mImagesDir = new File(activity.getCacheDir(), IMAGES_DIR);
93         mImagesDir.mkdir();
94         mImageView = view;
95         mImageView.setOnClickListener(v -> showAvatarPicker(isUserNew));
96 
97         mNewUserPhotoBitmap = savedBitmap;
98         mNewUserPhotoDrawable = savedDrawable;
99         mExecutorService = ThreadUtils.getBackgroundExecutor();
100     }
101 
102     /**
103      * Handles activity result from containing activity/fragment after a take/choose/crop photo
104      * action result is received.
105      */
onActivityResult(int requestCode, int resultCode, Intent data)106     public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
107         if (resultCode != Activity.RESULT_OK) {
108             return false;
109         }
110 
111         if (requestCode == REQUEST_CODE_PICK_AVATAR) {
112             if (data.hasExtra(EXTRA_DEFAULT_ICON_TINT_COLOR)) {
113                 int tintColor =
114                         data.getIntExtra(EXTRA_DEFAULT_ICON_TINT_COLOR, -1);
115                 onDefaultIconSelected(tintColor);
116                 return true;
117             }
118             if (data.getData() != null) {
119                 onPhotoCropped(data.getData());
120                 return true;
121             }
122         }
123         return false;
124     }
125 
getNewUserPhotoDrawable()126     public Drawable getNewUserPhotoDrawable() {
127         return mNewUserPhotoDrawable;
128     }
129 
showAvatarPicker(boolean isUserNew)130     private void showAvatarPicker(boolean isUserNew) {
131         Intent intent = new Intent(AVATAR_PICKER_ACTION);
132         intent.addCategory(Intent.CATEGORY_DEFAULT);
133         if (Flags.avatarSync()) {
134             intent.putExtra(EXTRA_IS_USER_NEW, isUserNew);
135         } else {
136             // SettingsLib is used by multiple apps therefore we need to know out of all apps
137             // using settingsLib which one is the one we return value to.
138             intent.setPackage(mImageView.getContext().getApplicationContext().getPackageName());
139         }
140         intent.putExtra(EXTRA_FILE_AUTHORITY, mFileAuthority);
141         mActivityStarter.startActivityForResult(intent, REQUEST_CODE_PICK_AVATAR);
142     }
143 
onDefaultIconSelected(int tintColor)144     private void onDefaultIconSelected(int tintColor) {
145         ListenableFuture<Bitmap> future = mExecutorService.submit(() -> {
146             Resources res = mActivity.getResources();
147             Drawable drawable =
148                     UserIcons.getDefaultUserIconInColor(res, tintColor);
149             return UserIcons.convertToBitmapAtUserIconSize(res, drawable);
150         });
151         Futures.addCallback(future, new FutureCallback<>() {
152             @Override
153             public void onSuccess(@NonNull Bitmap result) {
154                 onPhotoProcessed(result);
155             }
156 
157             @Override
158             public void onFailure(Throwable t) {
159                 Log.e(TAG, "Error processing default icon", t);
160             }
161         }, mImageView.getContext().getMainExecutor());
162     }
163 
onPhotoCropped(final Uri data)164     private void onPhotoCropped(final Uri data) {
165         ListenableFuture<Bitmap> future = mExecutorService.submit(() -> {
166             InputStream imageStream = null;
167             Bitmap bitmap = null;
168             try {
169                 imageStream = mActivity.getContentResolver()
170                         .openInputStream(data);
171                 bitmap = BitmapFactory.decodeStream(imageStream);
172             } catch (FileNotFoundException fe) {
173                 Log.w(TAG, "Cannot find image file", fe);
174             } finally {
175                 if (imageStream != null) {
176                     try {
177                         imageStream.close();
178                     } catch (IOException ioe) {
179                         Log.w(TAG, "Cannot close image stream", ioe);
180                     }
181                 }
182             }
183             return bitmap;
184         });
185         Futures.addCallback(future, new FutureCallback<>() {
186             @Override
187             public void onSuccess(@Nullable Bitmap result) {
188                 onPhotoProcessed(result);
189             }
190 
191             @Override
192             public void onFailure(Throwable t) {
193             }
194         }, mImageView.getContext().getMainExecutor());
195     }
196 
onPhotoProcessed(@ullable Bitmap bitmap)197     private void onPhotoProcessed(@Nullable Bitmap bitmap) {
198         if (bitmap != null) {
199             mNewUserPhotoBitmap = bitmap;
200             var unused = mExecutorService.submit(() -> {
201                 mCachedDrawablePath = saveNewUserPhotoBitmap().getPath();
202             });
203             mNewUserPhotoDrawable = CircleFramedDrawable
204                     .getInstance(mImageView.getContext(), mNewUserPhotoBitmap);
205             mImageView.setImageDrawable(mNewUserPhotoDrawable);
206         }
207     }
208 
saveNewUserPhotoBitmap()209     File saveNewUserPhotoBitmap() {
210         if (mNewUserPhotoBitmap == null) {
211             return null;
212         }
213         try {
214             File file = new File(mImagesDir, NEW_USER_PHOTO_FILE_NAME);
215             OutputStream os = new FileOutputStream(file);
216             mNewUserPhotoBitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
217             os.flush();
218             os.close();
219             return file;
220         } catch (IOException e) {
221             Log.e(TAG, "Cannot create temp file", e);
222         }
223         return null;
224     }
225 
loadNewUserPhotoBitmap(File file)226     static Bitmap loadNewUserPhotoBitmap(File file) {
227         return BitmapFactory.decodeFile(file.getAbsolutePath());
228     }
229 
removeNewUserPhotoBitmapFile()230     void removeNewUserPhotoBitmapFile() {
231         new File(mImagesDir, NEW_USER_PHOTO_FILE_NAME).delete();
232     }
233 
getCachedDrawablePath()234     String getCachedDrawablePath() {
235         return mCachedDrawablePath;
236     }
237 }
238