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