blob: de101b1fb2e81ed0b173b07262839669820526c8 [file] [log] [blame]
Oli Lanf98ae932022-01-17 18:19:36 +00001/*
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
Anna Bauzae95044a2024-03-21 20:54:39 +000017package com.android.settingslib.avatarpicker;
Oli Lanf98ae932022-01-17 18:19:36 +000018
19import android.app.Activity;
Oli Lanb4751122022-01-18 10:05:36 +000020import android.content.ContentResolver;
Oli Lanf98ae932022-01-17 18:19:36 +000021import android.content.Intent;
22import android.content.res.TypedArray;
23import android.graphics.Bitmap;
24import android.graphics.drawable.BitmapDrawable;
25import android.graphics.drawable.Drawable;
Oli Lanb4751122022-01-18 10:05:36 +000026import android.net.Uri;
Oli Lanf98ae932022-01-17 18:19:36 +000027import android.os.Bundle;
Anna Bauza6ad6b162023-10-24 18:33:22 +000028import android.util.Log;
Oli Lanf98ae932022-01-17 18:19:36 +000029import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.widget.ImageView;
33
34import androidx.annotation.NonNull;
35import androidx.core.graphics.drawable.RoundedBitmapDrawable;
36import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
37import androidx.recyclerview.widget.GridLayoutManager;
38import androidx.recyclerview.widget.RecyclerView;
39
Oli Lan4f3a0b8f2022-01-21 10:24:34 +000040import com.android.internal.util.UserIcons;
Oli Lanf98ae932022-01-17 18:19:36 +000041
42import com.google.android.setupcompat.template.FooterBarMixin;
43import com.google.android.setupcompat.template.FooterButton;
44import com.google.android.setupdesign.GlifLayout;
45import com.google.android.setupdesign.util.ThemeHelper;
Tetiana Meronyk23d6f842022-09-21 17:19:20 +000046import com.google.android.setupdesign.util.ThemeResolver;
Oli Lanf98ae932022-01-17 18:19:36 +000047
Oli Lan4f3a0b8f2022-01-21 10:24:34 +000048import java.util.ArrayList;
Oli Lan735747f2022-01-24 15:00:14 +000049import java.util.Arrays;
Oli Lan4f3a0b8f2022-01-21 10:24:34 +000050import java.util.List;
51
Oli Lanf98ae932022-01-17 18:19:36 +000052/**
53 * Activity to allow the user to choose a user profile picture.
Oli Lan4f3a0b8f2022-01-21 10:24:34 +000054 *
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.
Oli Lanf98ae932022-01-17 18:19:36 +000066 */
67public class AvatarPickerActivity extends Activity {
68
Oli Lanb4751122022-01-18 10:05:36 +000069 static final String EXTRA_FILE_AUTHORITY = "file_authority";
Oli Lan4f3a0b8f2022-01-21 10:24:34 +000070 static final String EXTRA_DEFAULT_ICON_TINT_COLOR = "default_icon_tint_color";
71
Oli Lanb4751122022-01-18 10:05:36 +000072 private static final String KEY_AWAITING_RESULT = "awaiting_result";
73 private static final String KEY_SELECTED_POSITION = "selected_position";
74
75 private boolean mWaitingForActivityResult;
Oli Lanf98ae932022-01-17 18:19:36 +000076
77 private FooterButton mDoneButton;
78 private AvatarAdapter mAdapter;
79
Oli Lanb4751122022-01-18 10:05:36 +000080 private AvatarPhotoController mAvatarPhotoController;
81
Oli Lanf98ae932022-01-17 18:19:36 +000082 @Override
83 protected void onCreate(Bundle savedInstanceState) {
84 super.onCreate(savedInstanceState);
Tetiana Meronyk23d6f842022-09-21 17:19:20 +000085 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);
Oli Lanf98ae932022-01-17 18:19:36 +000093 ThemeHelper.trySetDynamicColor(this);
94 setContentView(R.layout.avatar_picker);
Oli Lanb4751122022-01-18 10:05:36 +000095 setUpButtons();
Oli Lanf98ae932022-01-17 18:19:36 +000096
Oli Lanf98ae932022-01-17 18:19:36 +000097 RecyclerView recyclerView = findViewById(R.id.avatar_grid);
Oli Lanb4751122022-01-18 10:05:36 +000098 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(
Oli Lane8e79f82022-02-24 15:52:13 +0000106 new AvatarPhotoController.AvatarUiImpl(this),
107 new AvatarPhotoController.ContextInjectorImpl(this, getFileAuthority()),
108 mWaitingForActivityResult);
Oli Lanb4751122022-01-18 10:05:36 +0000109 }
110
Anna Bauza1ad4dc52023-04-21 16:17:15 +0000111 @Override
112 protected void onResume() {
113 super.onResume();
114 mAdapter.onAdapterResume();
115 }
116
Oli Lanb4751122022-01-18 10:05:36 +0000117 private void setUpButtons() {
118 GlifLayout glifLayout = findViewById(R.id.glif_layout);
Oli Lanf98ae932022-01-17 18:19:36 +0000119 FooterBarMixin mixin = glifLayout.getMixin(FooterBarMixin.class);
120
121 FooterButton secondaryButton =
122 new FooterButton.Builder(this)
Oli Lanb713e272022-03-10 10:38:10 +0000123 .setText(getString(android.R.string.cancel))
Oli Lanf98ae932022-01-17 18:19:36 +0000124 .setListener(view -> cancel())
125 .build();
126
127 mDoneButton =
128 new FooterButton.Builder(this)
Oli Lanb713e272022-03-10 10:38:10 +0000129 .setText(getString(R.string.done))
Oli Lan4f3a0b8f2022-01-21 10:24:34 +0000130 .setListener(view -> mAdapter.returnSelectionResult())
Oli Lanf98ae932022-01-17 18:19:36 +0000131 .build();
132 mDoneButton.setEnabled(false);
133
134 mixin.setSecondaryButton(secondaryButton);
135 mixin.setPrimaryButton(mDoneButton);
Oli Lanf98ae932022-01-17 18:19:36 +0000136 }
137
Oli Lanb4751122022-01-18 10:05:36 +0000138 private String getFileAuthority() {
139 String authority = getIntent().getStringExtra(EXTRA_FILE_AUTHORITY);
140 if (authority == null) {
Anna Bauza6ad6b162023-10-24 18:33:22 +0000141 Log.e(this.getClass().getName(), "File authority must be provided");
142 finish();
Oli Lanb4751122022-01-18 10:05:36 +0000143 }
144 return authority;
145 }
146
147 @Override
148 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
149 mWaitingForActivityResult = false;
150 mAvatarPhotoController.onActivityResult(requestCode, resultCode, data);
151 }
152
153 @Override
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
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);
Oli Lan0ef54ab2022-05-09 15:36:10 +0100165 mDoneButton.setEnabled(mAdapter.mSelectedPosition != AvatarAdapter.NONE);
Oli Lanb4751122022-01-18 10:05:36 +0000166 }
167 }
168
169 @Override
170 public void startActivityForResult(Intent intent, int requestCode) {
171 mWaitingForActivityResult = true;
172 super.startActivityForResult(intent, requestCode);
173 }
174
Oli Lan4f3a0b8f2022-01-21 10:24:34 +0000175 void returnUriResult(Uri uri) {
Oli Lanb4751122022-01-18 10:05:36 +0000176 Intent resultData = new Intent();
177 resultData.setData(uri);
178 setResult(RESULT_OK, resultData);
Oli Lanf98ae932022-01-17 18:19:36 +0000179 finish();
180 }
181
Oli Lan4f3a0b8f2022-01-21 10:24:34 +0000182 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
Oli Lanf98ae932022-01-17 18:19:36 +0000189 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;
Oli Lanb4751122022-01-18 10:05:36 +0000197
198 private final int mTakePhotoPosition;
199 private final int mChoosePhotoPosition;
200 private final int mPreselectedImageStartPosition;
Oli Lanf98ae932022-01-17 18:19:36 +0000201
Oli Lan4f3a0b8f2022-01-21 10:24:34 +0000202 private final List<Drawable> mImageDrawables;
Oli Lan735747f2022-01-24 15:00:14 +0000203 private final List<String> mImageDescriptions;
Oli Lan4f3a0b8f2022-01-21 10:24:34 +0000204 private final TypedArray mPreselectedImages;
205 private final int[] mUserIconColors;
Oli Lanf98ae932022-01-17 18:19:36 +0000206 private int mSelectedPosition = NONE;
207
Anna Bauza1ad4dc52023-04-21 16:17:15 +0000208 private int mLastSelectedPosition = NONE;
209
Oli Lanf98ae932022-01-17 18:19:36 +0000210 AvatarAdapter() {
Oli Lanb4751122022-01-18 10:05:36 +0000211 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
Oli Lan4f3a0b8f2022-01-21 10:24:34 +0000219 mPreselectedImages = getResources().obtainTypedArray(R.array.avatar_images);
220 mUserIconColors = UserIcons.getUserIconColors(getResources());
221 mImageDrawables = buildDrawableList();
Oli Lan735747f2022-01-24 15:00:14 +0000222 mImageDescriptions = buildDescriptionsList();
Oli Lanf98ae932022-01-17 18:19:36 +0000223 }
224
225 @NonNull
226 @Override
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
234 public void onBindViewHolder(@NonNull AvatarViewHolder viewHolder, int position) {
Oli Lanb4751122022-01-18 10:05:36 +0000235 if (position == mTakePhotoPosition) {
236 viewHolder.setDrawable(getDrawable(R.drawable.avatar_take_photo_circled));
Oli Lan735747f2022-01-24 15:00:14 +0000237 viewHolder.setContentDescription(getString(R.string.user_image_take_photo));
Oli Lanb4751122022-01-18 10:05:36 +0000238
239 } else if (position == mChoosePhotoPosition) {
240 viewHolder.setDrawable(getDrawable(R.drawable.avatar_choose_photo_circled));
Oli Lan735747f2022-01-24 15:00:14 +0000241 viewHolder.setContentDescription(getString(R.string.user_image_choose_photo));
Oli Lanb4751122022-01-18 10:05:36 +0000242
243 } else if (position >= mPreselectedImageStartPosition) {
Oli Lan735747f2022-01-24 15:00:14 +0000244 int index = indexFromPosition(position);
Oli Lanb4751122022-01-18 10:05:36 +0000245 viewHolder.setSelected(position == mSelectedPosition);
Oli Lan735747f2022-01-24 15:00:14 +0000246 viewHolder.setDrawable(mImageDrawables.get(index));
Oli Lanf756b262023-06-14 11:26:32 +0100247 if (mImageDescriptions != null && index < mImageDescriptions.size()) {
Oli Lan735747f2022-01-24 15:00:14 +0000248 viewHolder.setContentDescription(mImageDescriptions.get(index));
249 } else {
Anna Bauza1ad4dc52023-04-21 16:17:15 +0000250 viewHolder.setContentDescription(getString(
251 R.string.default_user_icon_description));
Oli Lan735747f2022-01-24 15:00:14 +0000252 }
Oli Lanb4751122022-01-18 10:05:36 +0000253 }
Anna Bauza1ad4dc52023-04-21 16:17:15 +0000254 viewHolder.setClickListener(view -> onViewHolderSelected(position));
255 }
256
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
272 public void onAdapterResume() {
273 mLastSelectedPosition = NONE;
Oli Lanf98ae932022-01-17 18:19:36 +0000274 }
275
276 @Override
277 public int getItemCount() {
Oli Lan4f3a0b8f2022-01-21 10:24:34 +0000278 return mPreselectedImageStartPosition + mImageDrawables.size();
279 }
280
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;
Oli Lanf98ae932022-01-17 18:19:36 +0000301 }
302
Oli Lan735747f2022-01-24 15:00:14 +0000303 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
Oli Lanf98ae932022-01-17 18:19:36 +0000312 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
322 private int indexFromPosition(int position) {
Oli Lanb4751122022-01-18 10:05:36 +0000323 return position - mPreselectedImageStartPosition;
Oli Lanf98ae932022-01-17 18:19:36 +0000324 }
325
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
337 private void deselect(int position) {
338 mSelectedPosition = NONE;
339 notifyItemChanged(position);
340 mDoneButton.setEnabled(false);
341 }
Oli Lanb4751122022-01-18 10:05:36 +0000342
Oli Lan4f3a0b8f2022-01-21 10:24:34 +0000343 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]);
Oli Lanb4751122022-01-18 10:05:36 +0000354 }
Oli Lanb4751122022-01-18 10:05:36 +0000355 }
356
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 }
Oli Lanf98ae932022-01-17 18:19:36 +0000365 }
366
367 private static class AvatarViewHolder extends RecyclerView.ViewHolder {
368 private final ImageView mImageView;
369
370 AvatarViewHolder(View view) {
371 super(view);
372 mImageView = view.findViewById(R.id.avatar_image);
373 }
374
375 public void setDrawable(Drawable drawable) {
376 mImageView.setImageDrawable(drawable);
377 }
378
Oli Lan735747f2022-01-24 15:00:14 +0000379 public void setContentDescription(String desc) {
380 mImageView.setContentDescription(desc);
381 }
382
Oli Lanf98ae932022-01-17 18:19:36 +0000383 public void setClickListener(View.OnClickListener listener) {
384 mImageView.setOnClickListener(listener);
385 }
386
387 public void setSelected(boolean selected) {
388 mImageView.setSelected(selected);
389 }
390 }
391}