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