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