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.avatarpicker; 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.util.Log; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.ImageView; 33 34 import androidx.annotation.NonNull; 35 import androidx.core.graphics.drawable.RoundedBitmapDrawable; 36 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; 37 import androidx.recyclerview.widget.GridLayoutManager; 38 import androidx.recyclerview.widget.RecyclerView; 39 40 import com.android.internal.util.UserIcons; 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 Log.e(this.getClass().getName(), "File authority must be provided"); 142 finish(); 143 } 144 return authority; 145 } 146 147 @Override onActivityResult(int requestCode, int resultCode, Intent data)148 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 149 mWaitingForActivityResult = false; 150 mAvatarPhotoController.onActivityResult(requestCode, resultCode, data); 151 } 152 153 @Override onSaveInstanceState(@onNull Bundle outState)154 protected void onSaveInstanceState(@NonNull Bundle outState) { 155 outState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult); 156 outState.putInt(KEY_SELECTED_POSITION, mAdapter.mSelectedPosition); 157 super.onSaveInstanceState(outState); 158 } 159 restoreState(Bundle savedInstanceState)160 private void restoreState(Bundle savedInstanceState) { 161 if (savedInstanceState != null) { 162 mWaitingForActivityResult = savedInstanceState.getBoolean(KEY_AWAITING_RESULT, false); 163 mAdapter.mSelectedPosition = 164 savedInstanceState.getInt(KEY_SELECTED_POSITION, AvatarAdapter.NONE); 165 mDoneButton.setEnabled(mAdapter.mSelectedPosition != AvatarAdapter.NONE); 166 } 167 } 168 169 @Override startActivityForResult(Intent intent, int requestCode)170 public void startActivityForResult(Intent intent, int requestCode) { 171 mWaitingForActivityResult = true; 172 super.startActivityForResult(intent, requestCode); 173 } 174 returnUriResult(Uri uri)175 void returnUriResult(Uri uri) { 176 Intent resultData = new Intent(); 177 resultData.setData(uri); 178 setResult(RESULT_OK, resultData); 179 finish(); 180 } 181 returnColorResult(int color)182 void returnColorResult(int color) { 183 Intent resultData = new Intent(); 184 resultData.putExtra(EXTRA_DEFAULT_ICON_TINT_COLOR, color); 185 setResult(RESULT_OK, resultData); 186 finish(); 187 } 188 cancel()189 private void cancel() { 190 setResult(RESULT_CANCELED); 191 finish(); 192 } 193 194 private class AvatarAdapter extends RecyclerView.Adapter<AvatarViewHolder> { 195 196 private static final int NONE = -1; 197 198 private final int mTakePhotoPosition; 199 private final int mChoosePhotoPosition; 200 private final int mPreselectedImageStartPosition; 201 202 private final List<Drawable> mImageDrawables; 203 private final List<String> mImageDescriptions; 204 private final TypedArray mPreselectedImages; 205 private final int[] mUserIconColors; 206 private int mSelectedPosition = NONE; 207 208 private int mLastSelectedPosition = NONE; 209 AvatarAdapter()210 AvatarAdapter() { 211 final boolean canTakePhoto = 212 PhotoCapabilityUtils.canTakePhoto(AvatarPickerActivity.this); 213 final boolean canChoosePhoto = 214 PhotoCapabilityUtils.canChoosePhoto(AvatarPickerActivity.this); 215 mTakePhotoPosition = (canTakePhoto ? 0 : NONE); 216 mChoosePhotoPosition = (canChoosePhoto ? (canTakePhoto ? 1 : 0) : NONE); 217 mPreselectedImageStartPosition = (canTakePhoto ? 1 : 0) + (canChoosePhoto ? 1 : 0); 218 219 mPreselectedImages = getResources().obtainTypedArray(R.array.avatar_images); 220 mUserIconColors = UserIcons.getUserIconColors(getResources()); 221 mImageDrawables = buildDrawableList(); 222 mImageDescriptions = buildDescriptionsList(); 223 } 224 225 @NonNull 226 @Override onCreateViewHolder(@onNull ViewGroup parent, int position)227 public AvatarViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) { 228 LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); 229 View itemView = layoutInflater.inflate(R.layout.avatar_item, parent, false); 230 return new AvatarViewHolder(itemView); 231 } 232 233 @Override onBindViewHolder(@onNull AvatarViewHolder viewHolder, int position)234 public void onBindViewHolder(@NonNull AvatarViewHolder viewHolder, int position) { 235 if (position == mTakePhotoPosition) { 236 viewHolder.setDrawable(getDrawable(R.drawable.avatar_take_photo_circled)); 237 viewHolder.setContentDescription(getString(R.string.user_image_take_photo)); 238 239 } else if (position == mChoosePhotoPosition) { 240 viewHolder.setDrawable(getDrawable(R.drawable.avatar_choose_photo_circled)); 241 viewHolder.setContentDescription(getString(R.string.user_image_choose_photo)); 242 243 } else if (position >= mPreselectedImageStartPosition) { 244 int index = indexFromPosition(position); 245 viewHolder.setSelected(position == mSelectedPosition); 246 viewHolder.setDrawable(mImageDrawables.get(index)); 247 if (mImageDescriptions != null && index < mImageDescriptions.size()) { 248 viewHolder.setContentDescription(mImageDescriptions.get(index)); 249 } else { 250 viewHolder.setContentDescription(getString( 251 R.string.default_user_icon_description)); 252 } 253 } 254 viewHolder.setClickListener(view -> onViewHolderSelected(position)); 255 } 256 onViewHolderSelected(int position)257 private void onViewHolderSelected(int position) { 258 if ((mTakePhotoPosition == position) && (mLastSelectedPosition != position)) { 259 mAvatarPhotoController.takePhoto(); 260 } else if ((mChoosePhotoPosition == position) && (mLastSelectedPosition != position)) { 261 mAvatarPhotoController.choosePhoto(); 262 } else { 263 if (mSelectedPosition == position) { 264 deselect(position); 265 } else { 266 select(position); 267 } 268 } 269 mLastSelectedPosition = position; 270 } 271 onAdapterResume()272 public void onAdapterResume() { 273 mLastSelectedPosition = NONE; 274 } 275 276 @Override getItemCount()277 public int getItemCount() { 278 return mPreselectedImageStartPosition + mImageDrawables.size(); 279 } 280 buildDrawableList()281 private List<Drawable> buildDrawableList() { 282 List<Drawable> result = new ArrayList<>(); 283 284 for (int i = 0; i < mPreselectedImages.length(); i++) { 285 Drawable drawable = mPreselectedImages.getDrawable(i); 286 if (drawable instanceof BitmapDrawable) { 287 result.add(circularDrawableFrom((BitmapDrawable) drawable)); 288 } else { 289 throw new IllegalStateException("Avatar drawables must be bitmaps"); 290 } 291 } 292 if (!result.isEmpty()) { 293 return result; 294 } 295 296 // No preselected images. Use tinted default icon. 297 for (int i = 0; i < mUserIconColors.length; i++) { 298 result.add(UserIcons.getDefaultUserIconInColor(getResources(), mUserIconColors[i])); 299 } 300 return result; 301 } 302 buildDescriptionsList()303 private List<String> buildDescriptionsList() { 304 if (mPreselectedImages.length() > 0) { 305 return Arrays.asList( 306 getResources().getStringArray(R.array.avatar_image_descriptions)); 307 } 308 309 return null; 310 } 311 circularDrawableFrom(BitmapDrawable drawable)312 private Drawable circularDrawableFrom(BitmapDrawable drawable) { 313 Bitmap bitmap = drawable.getBitmap(); 314 315 RoundedBitmapDrawable roundedBitmapDrawable = 316 RoundedBitmapDrawableFactory.create(getResources(), bitmap); 317 roundedBitmapDrawable.setCircular(true); 318 319 return roundedBitmapDrawable; 320 } 321 indexFromPosition(int position)322 private int indexFromPosition(int position) { 323 return position - mPreselectedImageStartPosition; 324 } 325 select(int position)326 private void select(int position) { 327 final int oldSelection = mSelectedPosition; 328 mSelectedPosition = position; 329 notifyItemChanged(position); 330 if (oldSelection != NONE) { 331 notifyItemChanged(oldSelection); 332 } else { 333 mDoneButton.setEnabled(true); 334 } 335 } 336 deselect(int position)337 private void deselect(int position) { 338 mSelectedPosition = NONE; 339 notifyItemChanged(position); 340 mDoneButton.setEnabled(false); 341 } 342 returnSelectionResult()343 private void returnSelectionResult() { 344 int index = indexFromPosition(mSelectedPosition); 345 if (mPreselectedImages.length() > 0) { 346 int resourceId = mPreselectedImages.getResourceId(index, -1); 347 if (resourceId == -1) { 348 throw new IllegalStateException("Preselected avatar images must be resources."); 349 } 350 returnUriResult(uriForResourceId(resourceId)); 351 } else { 352 returnColorResult( 353 mUserIconColors[index]); 354 } 355 } 356 uriForResourceId(int resourceId)357 private Uri uriForResourceId(int resourceId) { 358 return new Uri.Builder() 359 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 360 .authority(getResources().getResourcePackageName(resourceId)) 361 .appendPath(getResources().getResourceTypeName(resourceId)) 362 .appendPath(getResources().getResourceEntryName(resourceId)) 363 .build(); 364 } 365 } 366 367 private static class AvatarViewHolder extends RecyclerView.ViewHolder { 368 private final ImageView mImageView; 369 AvatarViewHolder(View view)370 AvatarViewHolder(View view) { 371 super(view); 372 mImageView = view.findViewById(R.id.avatar_image); 373 } 374 setDrawable(Drawable drawable)375 public void setDrawable(Drawable drawable) { 376 mImageView.setImageDrawable(drawable); 377 } 378 setContentDescription(String desc)379 public void setContentDescription(String desc) { 380 mImageView.setContentDescription(desc); 381 } 382 setClickListener(View.OnClickListener listener)383 public void setClickListener(View.OnClickListener listener) { 384 mImageView.setOnClickListener(listener); 385 } 386 setSelected(boolean selected)387 public void setSelected(boolean selected) { 388 mImageView.setSelected(selected); 389 } 390 } 391 } 392