1 /* 2 * Copyright (C) 2022 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.ContentResolver; 21 import android.content.Intent; 22 import android.content.res.TypedArray; 23 import android.graphics.Bitmap; 24 import android.graphics.drawable.BitmapDrawable; 25 import android.graphics.drawable.Drawable; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import android.view.LayoutInflater; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.widget.ImageView; 32 33 import androidx.annotation.NonNull; 34 import androidx.core.graphics.drawable.RoundedBitmapDrawable; 35 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; 36 import androidx.recyclerview.widget.GridLayoutManager; 37 import androidx.recyclerview.widget.RecyclerView; 38 39 import com.android.internal.util.UserIcons; 40 import com.android.settingslib.R; 41 42 import com.google.android.setupcompat.template.FooterBarMixin; 43 import com.google.android.setupcompat.template.FooterButton; 44 import com.google.android.setupdesign.GlifLayout; 45 import com.google.android.setupdesign.util.ThemeHelper; 46 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.List; 50 51 /** 52 * Activity to allow the user to choose a user profile picture. 53 * 54 * <p>Options are provided to take a photo or choose a photo using the photo picker. In addition, 55 * preselected avatar images may be provided in the resource array {@code avatar_images}. If 56 * provided, every element of that array must be a bitmap drawable. 57 * 58 * <p>If preselected images are not provided, the default avatar will be shown instead, in a range 59 * of colors. 60 * 61 * <p>This activity should be started with startActivityForResult. If a photo or a preselected image 62 * is selected, a Uri will be returned in the data field of the result intent. If a colored default 63 * avatar is selected, the chosen color will be returned as {@code EXTRA_DEFAULT_ICON_TINT_COLOR} 64 * and the data field will be empty. 65 */ 66 public class AvatarPickerActivity extends Activity { 67 68 static final String EXTRA_FILE_AUTHORITY = "file_authority"; 69 static final String EXTRA_DEFAULT_ICON_TINT_COLOR = "default_icon_tint_color"; 70 71 private static final String KEY_AWAITING_RESULT = "awaiting_result"; 72 private static final String KEY_SELECTED_POSITION = "selected_position"; 73 74 private boolean mWaitingForActivityResult; 75 76 private FooterButton mDoneButton; 77 private AvatarAdapter mAdapter; 78 79 private AvatarPhotoController mAvatarPhotoController; 80 81 @Override onCreate(Bundle savedInstanceState)82 protected void onCreate(Bundle savedInstanceState) { 83 super.onCreate(savedInstanceState); 84 setTheme(R.style.SudThemeGlifV3_DayNight); 85 ThemeHelper.trySetDynamicColor(this); 86 setContentView(R.layout.avatar_picker); 87 setUpButtons(); 88 89 RecyclerView recyclerView = findViewById(R.id.avatar_grid); 90 mAdapter = new AvatarAdapter(); 91 recyclerView.setAdapter(mAdapter); 92 recyclerView.setLayoutManager(new GridLayoutManager(this, 93 getResources().getInteger(R.integer.avatar_picker_columns))); 94 95 restoreState(savedInstanceState); 96 97 mAvatarPhotoController = new AvatarPhotoController( 98 new AvatarPhotoController.AvatarUiImpl(this), 99 new AvatarPhotoController.ContextInjectorImpl(this, getFileAuthority()), 100 mWaitingForActivityResult); 101 } 102 setUpButtons()103 private void setUpButtons() { 104 GlifLayout glifLayout = findViewById(R.id.glif_layout); 105 FooterBarMixin mixin = glifLayout.getMixin(FooterBarMixin.class); 106 107 FooterButton secondaryButton = 108 new FooterButton.Builder(this) 109 .setText(getString(android.R.string.cancel)) 110 .setListener(view -> cancel()) 111 .build(); 112 113 mDoneButton = 114 new FooterButton.Builder(this) 115 .setText(getString(R.string.done)) 116 .setListener(view -> mAdapter.returnSelectionResult()) 117 .build(); 118 mDoneButton.setEnabled(false); 119 120 mixin.setSecondaryButton(secondaryButton); 121 mixin.setPrimaryButton(mDoneButton); 122 } 123 getFileAuthority()124 private String getFileAuthority() { 125 String authority = getIntent().getStringExtra(EXTRA_FILE_AUTHORITY); 126 if (authority == null) { 127 throw new IllegalStateException("File authority must be provided"); 128 } 129 return authority; 130 } 131 132 @Override onActivityResult(int requestCode, int resultCode, Intent data)133 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 134 mWaitingForActivityResult = false; 135 mAvatarPhotoController.onActivityResult(requestCode, resultCode, data); 136 } 137 138 @Override onSaveInstanceState(@onNull Bundle outState)139 protected void onSaveInstanceState(@NonNull Bundle outState) { 140 outState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult); 141 outState.putInt(KEY_SELECTED_POSITION, mAdapter.mSelectedPosition); 142 super.onSaveInstanceState(outState); 143 } 144 restoreState(Bundle savedInstanceState)145 private void restoreState(Bundle savedInstanceState) { 146 if (savedInstanceState != null) { 147 mWaitingForActivityResult = savedInstanceState.getBoolean(KEY_AWAITING_RESULT, false); 148 mAdapter.mSelectedPosition = 149 savedInstanceState.getInt(KEY_SELECTED_POSITION, AvatarAdapter.NONE); 150 mDoneButton.setEnabled(mAdapter.mSelectedPosition != AvatarAdapter.NONE); 151 } 152 } 153 154 @Override startActivityForResult(Intent intent, int requestCode)155 public void startActivityForResult(Intent intent, int requestCode) { 156 mWaitingForActivityResult = true; 157 super.startActivityForResult(intent, requestCode); 158 } 159 returnUriResult(Uri uri)160 void returnUriResult(Uri uri) { 161 Intent resultData = new Intent(); 162 resultData.setData(uri); 163 setResult(RESULT_OK, resultData); 164 finish(); 165 } 166 returnColorResult(int color)167 void returnColorResult(int color) { 168 Intent resultData = new Intent(); 169 resultData.putExtra(EXTRA_DEFAULT_ICON_TINT_COLOR, color); 170 setResult(RESULT_OK, resultData); 171 finish(); 172 } 173 cancel()174 private void cancel() { 175 setResult(RESULT_CANCELED); 176 finish(); 177 } 178 179 private class AvatarAdapter extends RecyclerView.Adapter<AvatarViewHolder> { 180 181 private static final int NONE = -1; 182 183 private final int mTakePhotoPosition; 184 private final int mChoosePhotoPosition; 185 private final int mPreselectedImageStartPosition; 186 187 private final List<Drawable> mImageDrawables; 188 private final List<String> mImageDescriptions; 189 private final TypedArray mPreselectedImages; 190 private final int[] mUserIconColors; 191 private int mSelectedPosition = NONE; 192 AvatarAdapter()193 AvatarAdapter() { 194 final boolean canTakePhoto = 195 PhotoCapabilityUtils.canTakePhoto(AvatarPickerActivity.this); 196 final boolean canChoosePhoto = 197 PhotoCapabilityUtils.canChoosePhoto(AvatarPickerActivity.this); 198 mTakePhotoPosition = (canTakePhoto ? 0 : NONE); 199 mChoosePhotoPosition = (canChoosePhoto ? (canTakePhoto ? 1 : 0) : NONE); 200 mPreselectedImageStartPosition = (canTakePhoto ? 1 : 0) + (canChoosePhoto ? 1 : 0); 201 202 mPreselectedImages = getResources().obtainTypedArray(R.array.avatar_images); 203 mUserIconColors = UserIcons.getUserIconColors(getResources()); 204 mImageDrawables = buildDrawableList(); 205 mImageDescriptions = buildDescriptionsList(); 206 } 207 208 @NonNull 209 @Override onCreateViewHolder(@onNull ViewGroup parent, int position)210 public AvatarViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) { 211 LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); 212 View itemView = layoutInflater.inflate(R.layout.avatar_item, parent, false); 213 return new AvatarViewHolder(itemView); 214 } 215 216 @Override onBindViewHolder(@onNull AvatarViewHolder viewHolder, int position)217 public void onBindViewHolder(@NonNull AvatarViewHolder viewHolder, int position) { 218 if (position == mTakePhotoPosition) { 219 viewHolder.setDrawable(getDrawable(R.drawable.avatar_take_photo_circled)); 220 viewHolder.setContentDescription(getString(R.string.user_image_take_photo)); 221 viewHolder.setClickListener(view -> mAvatarPhotoController.takePhoto()); 222 223 } else if (position == mChoosePhotoPosition) { 224 viewHolder.setDrawable(getDrawable(R.drawable.avatar_choose_photo_circled)); 225 viewHolder.setContentDescription(getString(R.string.user_image_choose_photo)); 226 viewHolder.setClickListener(view -> mAvatarPhotoController.choosePhoto()); 227 228 } else if (position >= mPreselectedImageStartPosition) { 229 int index = indexFromPosition(position); 230 viewHolder.setSelected(position == mSelectedPosition); 231 viewHolder.setDrawable(mImageDrawables.get(index)); 232 if (mImageDescriptions != null) { 233 viewHolder.setContentDescription(mImageDescriptions.get(index)); 234 } else { 235 viewHolder.setContentDescription( 236 getString(R.string.default_user_icon_description)); 237 } 238 viewHolder.setClickListener(view -> { 239 if (mSelectedPosition == position) { 240 deselect(position); 241 } else { 242 select(position); 243 } 244 }); 245 } 246 } 247 248 @Override getItemCount()249 public int getItemCount() { 250 return mPreselectedImageStartPosition + mImageDrawables.size(); 251 } 252 buildDrawableList()253 private List<Drawable> buildDrawableList() { 254 List<Drawable> result = new ArrayList<>(); 255 256 for (int i = 0; i < mPreselectedImages.length(); i++) { 257 Drawable drawable = mPreselectedImages.getDrawable(i); 258 if (drawable instanceof BitmapDrawable) { 259 result.add(circularDrawableFrom((BitmapDrawable) drawable)); 260 } else { 261 throw new IllegalStateException("Avatar drawables must be bitmaps"); 262 } 263 } 264 if (!result.isEmpty()) { 265 return result; 266 } 267 268 // No preselected images. Use tinted default icon. 269 for (int i = 0; i < mUserIconColors.length; i++) { 270 result.add(UserIcons.getDefaultUserIconInColor(getResources(), mUserIconColors[i])); 271 } 272 return result; 273 } 274 buildDescriptionsList()275 private List<String> buildDescriptionsList() { 276 if (mPreselectedImages.length() > 0) { 277 return Arrays.asList( 278 getResources().getStringArray(R.array.avatar_image_descriptions)); 279 } 280 281 return null; 282 } 283 circularDrawableFrom(BitmapDrawable drawable)284 private Drawable circularDrawableFrom(BitmapDrawable drawable) { 285 Bitmap bitmap = drawable.getBitmap(); 286 287 RoundedBitmapDrawable roundedBitmapDrawable = 288 RoundedBitmapDrawableFactory.create(getResources(), bitmap); 289 roundedBitmapDrawable.setCircular(true); 290 291 return roundedBitmapDrawable; 292 } 293 indexFromPosition(int position)294 private int indexFromPosition(int position) { 295 return position - mPreselectedImageStartPosition; 296 } 297 select(int position)298 private void select(int position) { 299 final int oldSelection = mSelectedPosition; 300 mSelectedPosition = position; 301 notifyItemChanged(position); 302 if (oldSelection != NONE) { 303 notifyItemChanged(oldSelection); 304 } else { 305 mDoneButton.setEnabled(true); 306 } 307 } 308 deselect(int position)309 private void deselect(int position) { 310 mSelectedPosition = NONE; 311 notifyItemChanged(position); 312 mDoneButton.setEnabled(false); 313 } 314 returnSelectionResult()315 private void returnSelectionResult() { 316 int index = indexFromPosition(mSelectedPosition); 317 if (mPreselectedImages.length() > 0) { 318 int resourceId = mPreselectedImages.getResourceId(index, -1); 319 if (resourceId == -1) { 320 throw new IllegalStateException("Preselected avatar images must be resources."); 321 } 322 returnUriResult(uriForResourceId(resourceId)); 323 } else { 324 returnColorResult( 325 mUserIconColors[index]); 326 } 327 } 328 uriForResourceId(int resourceId)329 private Uri uriForResourceId(int resourceId) { 330 return new Uri.Builder() 331 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 332 .authority(getResources().getResourcePackageName(resourceId)) 333 .appendPath(getResources().getResourceTypeName(resourceId)) 334 .appendPath(getResources().getResourceEntryName(resourceId)) 335 .build(); 336 } 337 } 338 339 private static class AvatarViewHolder extends RecyclerView.ViewHolder { 340 private final ImageView mImageView; 341 AvatarViewHolder(View view)342 AvatarViewHolder(View view) { 343 super(view); 344 mImageView = view.findViewById(R.id.avatar_image); 345 } 346 setDrawable(Drawable drawable)347 public void setDrawable(Drawable drawable) { 348 mImageView.setImageDrawable(drawable); 349 } 350 setContentDescription(String desc)351 public void setContentDescription(String desc) { 352 mImageView.setContentDescription(desc); 353 } 354 setClickListener(View.OnClickListener listener)355 public void setClickListener(View.OnClickListener listener) { 356 mImageView.setOnClickListener(listener); 357 } 358 setSelected(boolean selected)359 public void setSelected(boolean selected) { 360 mImageView.setSelected(selected); 361 } 362 } 363 } 364