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