blob: 0121c4f19bd728f1fc22d56ef3dd6575b07aee17 [file] [log] [blame]
Chuck Liao1e0501f2020-02-17 18:20:54 +08001/*
2 * Copyright (C) 2020 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 */
16package com.android.wallpaper.picker;
17
18import android.app.Activity;
19import android.content.Intent;
20import android.graphics.Point;
21import android.graphics.PorterDuff;
22import android.graphics.Rect;
23import android.net.Uri;
24import android.os.Bundle;
25import android.provider.Settings;
26import android.util.DisplayMetrics;
27import android.util.Log;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.ImageView;
32import android.widget.ProgressBar;
33import android.widget.TextView;
34
35import androidx.annotation.NonNull;
36import androidx.annotation.Nullable;
37import androidx.appcompat.app.AlertDialog;
38import androidx.cardview.widget.CardView;
39import androidx.fragment.app.Fragment;
40import androidx.recyclerview.widget.GridLayoutManager;
41import androidx.recyclerview.widget.RecyclerView;
42
43import com.android.wallpaper.R;
44import com.android.wallpaper.asset.Asset;
45import com.android.wallpaper.model.Category;
46import com.android.wallpaper.module.InjectorProvider;
47import com.android.wallpaper.module.UserEventLogger;
48import com.android.wallpaper.util.DisplayMetricsRetriever;
Santiago Etchebehere53c63432020-05-07 18:55:35 -070049import com.android.wallpaper.util.SizeCalculator;
Wesley.CW Wangdc68fde2020-06-15 19:12:33 +080050import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate;
51import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate.BottomSheetHost;
Chuck Liao1e0501f2020-02-17 18:20:54 +080052
53import com.bumptech.glide.Glide;
54
55import java.util.ArrayList;
56import java.util.List;
57
58/**
59 * Displays the UI which contains the categories of the wallpaper.
60 */
61public class CategorySelectorFragment extends Fragment {
62
63 // The number of ViewHolders that don't pertain to category tiles.
64 // Currently 2: one for the metadata section and one for the "Select wallpaper" header.
65 private static final int NUM_NON_CATEGORY_VIEW_HOLDERS = 0;
66 private static final int SETTINGS_APP_INFO_REQUEST_CODE = 1;
67 private static final String TAG = "CategorySelectorFragment";
68
69 /**
70 * Interface to be implemented by an Fragment hosting a {@link CategorySelectorFragment}
71 */
72 public interface CategorySelectorFragmentHost {
73
74 /**
75 * Requests to show the Android custom photo picker for the sake of picking a photo
76 * to set as the device's wallpaper.
77 */
78 void requestCustomPhotoPicker(MyPhotosStarter.PermissionChangedListener listener);
79
80 /**
81 * Shows the wallpaper page of the specific category.
82 *
Chuck Liaoa63f1bf2020-06-11 01:35:42 +080083 * @param category the wallpaper's {@link Category}
Chuck Liao1e0501f2020-02-17 18:20:54 +080084 */
Chuck Liaoa63f1bf2020-06-11 01:35:42 +080085 void show(Category category);
Chuck Liao0088bca2020-05-28 16:03:23 +080086
87 /**
88 * Sets the title in the toolbar.
89 */
90 void setToolbarTitle(CharSequence title);
Chuck Liao1e0501f2020-02-17 18:20:54 +080091 }
92
93 private RecyclerView mImageGrid;
94 private CategoryAdapter mAdapter;
95 private ArrayList<Category> mCategories = new ArrayList<>();
96 private Point mTileSizePx;
97 private boolean mAwaitingCategories;
98
99 public CategorySelectorFragment() {
100 mAdapter = new CategoryAdapter(mCategories);
101 }
102
103 @Nullable
104 @Override
105 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
106 @Nullable Bundle savedInstanceState) {
107 View view = inflater.inflate(R.layout.fragment_category_selector, container,
108 /* attachToRoot= */ false);
Chuck Liao1e0501f2020-02-17 18:20:54 +0800109 mImageGrid = view.findViewById(R.id.category_grid);
110 mImageGrid.addItemDecoration(new GridPaddingDecoration(
111 getResources().getDimensionPixelSize(R.dimen.grid_padding)));
112
Santiago Etchebehere53c63432020-05-07 18:55:35 -0700113 mTileSizePx = SizeCalculator.getCategoryTileSize(getActivity());
Chuck Liao1e0501f2020-02-17 18:20:54 +0800114
115 mImageGrid.setAdapter(mAdapter);
116
117 GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), getNumColumns());
118 mImageGrid.setLayoutManager(gridLayoutManager);
Chuck Liaoca97c572020-05-08 17:13:25 +0800119 mImageGrid.setOnApplyWindowInsetsListener((v, windowInsets) -> {
120 v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(),
121 windowInsets.getSystemWindowInsetBottom());
122 return windowInsets;
123 });
Wesley.CW Wang27ff4262020-06-12 19:19:57 +0800124 mImageGrid.setAccessibilityDelegateCompat(
Wesley.CW Wangdc68fde2020-06-15 19:12:33 +0800125 new WallpaperPickerRecyclerViewAccessibilityDelegate(
126 mImageGrid, (BottomSheetHost) getParentFragment(), getNumColumns()));
Chuck Liao0088bca2020-05-28 16:03:23 +0800127 getCategorySelectorFragmentHost().setToolbarTitle(getText(R.string.wallpaper_title));
Chuck Liao1e0501f2020-02-17 18:20:54 +0800128
129 return view;
130 }
131
132 /**
133 * Inserts the given category into the categories list in priority order.
134 */
135 void addCategory(Category category, boolean loading) {
136 // If not previously waiting for categories, enter the waiting state by showing the loading
137 // indicator.
138 if (loading && !mAwaitingCategories) {
139 mAdapter.notifyItemChanged(getNumColumns());
140 mAdapter.notifyItemInserted(getNumColumns());
141 mAwaitingCategories = true;
142 }
143 // Not add existing category to category list
144 if (mCategories.indexOf(category) >= 0) {
145 updateCategory(category);
146 return;
147 }
148
149 int priority = category.getPriority();
150
151 int index = 0;
152 while (index < mCategories.size() && priority >= mCategories.get(index).getPriority()) {
153 index++;
154 }
155
156 mCategories.add(index, category);
157 if (mAdapter != null) {
158 // Offset the index because of the static metadata element at beginning of RecyclerView.
159 mAdapter.notifyItemInserted(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
160 }
161 }
162
163 void removeCategory(Category category) {
164 int index = mCategories.indexOf(category);
165 if (index >= 0) {
166 mCategories.remove(index);
167 mAdapter.notifyItemRemoved(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
168 }
169 }
170
171 void updateCategory(Category category) {
172 int index = mCategories.indexOf(category);
173 if (index >= 0) {
174 mCategories.remove(index);
175 mCategories.add(index, category);
176 mAdapter.notifyItemChanged(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
177 }
178 }
179
180 void clearCategories() {
181 mCategories.clear();
182 mAdapter.notifyDataSetChanged();
183 }
184
185 /**
186 * Notifies the CategoryFragment that no further categories are expected so it may hide
187 * the loading indicator.
188 */
189 void doneFetchingCategories() {
190 if (mAwaitingCategories) {
191 mAdapter.notifyItemRemoved(mAdapter.getItemCount() - 1);
192 mAwaitingCategories = false;
193 }
194 }
195
196 void notifyDataSetChanged() {
197 mAdapter.notifyDataSetChanged();
198 }
199
200 private int getNumColumns() {
201 Activity activity = getActivity();
Santiago Etchebehere53c63432020-05-07 18:55:35 -0700202 return activity == null ? 0 : SizeCalculator.getNumCategoryColumns(activity);
Chuck Liao1e0501f2020-02-17 18:20:54 +0800203 }
204
205
206 private CategorySelectorFragmentHost getCategorySelectorFragmentHost() {
207 return (CategorySelectorFragmentHost) getParentFragment();
208 }
209
210 /**
211 * ViewHolder subclass for a category tile in the RecyclerView.
212 */
213 private class CategoryHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
214 private Category mCategory;
215 private ImageView mImageView;
216 private ImageView mOverlayIconView;
217 private TextView mTitleView;
218
219 CategoryHolder(View itemView) {
220 super(itemView);
221 itemView.setOnClickListener(this);
222
223 mImageView = itemView.findViewById(R.id.image);
224 mOverlayIconView = itemView.findViewById(R.id.overlay_icon);
225 mTitleView = itemView.findViewById(R.id.category_title);
226
227 CardView categoryView = itemView.findViewById(R.id.category);
228 categoryView.getLayoutParams().height = mTileSizePx.y;
229 }
230
231 @Override
232 public void onClick(View view) {
233 final UserEventLogger eventLogger =
234 InjectorProvider.getInjector().getUserEventLogger(getActivity());
235 eventLogger.logCategorySelected(mCategory.getCollectionId());
236
237 if (mCategory.supportsCustomPhotos()) {
238 getCategorySelectorFragmentHost().requestCustomPhotoPicker(
239 new MyPhotosStarter.PermissionChangedListener() {
240 @Override
241 public void onPermissionsGranted() {
242 drawThumbnailAndOverlayIcon();
243 }
244
245 @Override
246 public void onPermissionsDenied(boolean dontAskAgain) {
247 // No-op
248 }
249 });
250 return;
251 }
252
Chuck Liaoa63f1bf2020-06-11 01:35:42 +0800253 getCategorySelectorFragmentHost().show(mCategory);
Chuck Liao0088bca2020-05-28 16:03:23 +0800254 getCategorySelectorFragmentHost().setToolbarTitle(mCategory.getTitle());
Chuck Liao1e0501f2020-02-17 18:20:54 +0800255 }
256
257 /**
258 * Binds the given category to this CategoryHolder.
259 */
260 private void bindCategory(Category category) {
261 mCategory = category;
262 mTitleView.setText(category.getTitle());
263 drawThumbnailAndOverlayIcon();
264 }
265
266 /**
267 * Draws the CategoryHolder's thumbnail and overlay icon.
268 */
269 private void drawThumbnailAndOverlayIcon() {
270 mOverlayIconView.setImageDrawable(mCategory.getOverlayIcon(
271 getActivity().getApplicationContext()));
272
273 // Size the overlay icon according to the category.
274 int overlayIconDimenDp = mCategory.getOverlayIconSizeDp();
275 DisplayMetrics metrics = DisplayMetricsRetriever.getInstance().getDisplayMetrics(
276 getResources(), getActivity().getWindowManager().getDefaultDisplay());
277 int overlayIconDimenPx = (int) (overlayIconDimenDp * metrics.density);
278 mOverlayIconView.getLayoutParams().width = overlayIconDimenPx;
279 mOverlayIconView.getLayoutParams().height = overlayIconDimenPx;
280
281 Asset thumbnail = mCategory.getThumbnail(getActivity().getApplicationContext());
282 if (thumbnail != null) {
283 thumbnail.loadDrawable(getActivity(), mImageView,
284 getResources().getColor(R.color.secondary_color));
285 } else {
286 // TODO(orenb): Replace this workaround for b/62584914 with a proper way of
287 // unloading the ImageView such that no incorrect image is improperly loaded upon
288 // rapid scroll.
289 Object nullObj = null;
290 Glide.with(getActivity())
291 .asDrawable()
292 .load(nullObj)
293 .into(mImageView);
294
295 }
296 }
297 }
298
299 /**
300 * ViewHolder subclass for the loading indicator ("spinner") shown when categories are being
301 * fetched.
302 */
303 private class LoadingIndicatorHolder extends RecyclerView.ViewHolder {
304 private LoadingIndicatorHolder(View view) {
305 super(view);
306 ProgressBar progressBar = view.findViewById(R.id.loading_indicator);
307 progressBar.getIndeterminateDrawable().setColorFilter(
308 getResources().getColor(R.color.accent_color), PorterDuff.Mode.SRC_IN);
309 }
310 }
311
312 /**
313 * RecyclerView Adapter subclass for the category tiles in the RecyclerView.
314 */
315 private class CategoryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
316 implements MyPhotosStarter.PermissionChangedListener {
317 private static final int ITEM_VIEW_TYPE_CATEGORY = 3;
318 private static final int ITEM_VIEW_TYPE_LOADING_INDICATOR = 4;
319 private List<Category> mCategories;
320
321 private CategoryAdapter(List<Category> categories) {
322 mCategories = categories;
323 }
324
325 @Override
326 public int getItemViewType(int position) {
327 if (mAwaitingCategories && position == getItemCount() - 1) {
328 return ITEM_VIEW_TYPE_LOADING_INDICATOR;
329 }
330
331 return ITEM_VIEW_TYPE_CATEGORY;
332 }
333
334 @Override
335 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
336 LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
337 View view;
338
339 switch (viewType) {
340 case ITEM_VIEW_TYPE_LOADING_INDICATOR:
341 view = layoutInflater.inflate(R.layout.grid_item_loading_indicator,
342 parent, /* attachToRoot= */ false);
343 return new LoadingIndicatorHolder(view);
344 case ITEM_VIEW_TYPE_CATEGORY:
345 view = layoutInflater.inflate(R.layout.grid_item_category,
346 parent, /* attachToRoot= */ false);
347 return new CategoryHolder(view);
348 default:
349 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
350 return null;
351 }
352 }
353
354 @Override
355 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
356 int viewType = getItemViewType(position);
357
358 switch (viewType) {
359 case ITEM_VIEW_TYPE_CATEGORY:
360 // Offset position to get category index to account for the non-category view
361 // holders.
362 Category category = mCategories.get(position - NUM_NON_CATEGORY_VIEW_HOLDERS);
363 ((CategoryHolder) holder).bindCategory(category);
364 break;
365 case ITEM_VIEW_TYPE_LOADING_INDICATOR:
366 // No op.
367 break;
368 default:
369 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
370 }
371 }
372
373 @Override
374 public int getItemCount() {
375 // Add to size of categories to account for the metadata related views.
376 // Add 1 more for the loading indicator if not yet done loading.
377 int size = mCategories.size() + NUM_NON_CATEGORY_VIEW_HOLDERS;
378 if (mAwaitingCategories) {
379 size += 1;
380 }
381
382 return size;
383 }
384
385 @Override
386 public void onPermissionsGranted() {
387 notifyDataSetChanged();
388 }
389
390 @Override
391 public void onPermissionsDenied(boolean dontAskAgain) {
392 if (!dontAskAgain) {
393 return;
394 }
395
396 String permissionNeededMessage =
397 getString(R.string.permission_needed_explanation_go_to_settings);
398 AlertDialog dialog = new AlertDialog.Builder(getActivity(), R.style.LightDialogTheme)
399 .setMessage(permissionNeededMessage)
400 .setPositiveButton(android.R.string.ok, null /* onClickListener */)
401 .setNegativeButton(
402 R.string.settings_button_label,
403 (dialogInterface, i) -> {
404 Intent appInfoIntent =
405 new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
406 Uri uri = Uri.fromParts("package",
407 getActivity().getPackageName(), /* fragment= */ null);
408 appInfoIntent.setData(uri);
409 startActivityForResult(
410 appInfoIntent, SETTINGS_APP_INFO_REQUEST_CODE);
411 })
412 .create();
413 dialog.show();
414 }
415 }
416
417 private class GridPaddingDecoration extends RecyclerView.ItemDecoration {
418
419 private int mPadding;
420
421 GridPaddingDecoration(int padding) {
422 mPadding = padding;
423 }
424
425 @Override
426 public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
427 RecyclerView.State state) {
428 int position = parent.getChildAdapterPosition(view) - NUM_NON_CATEGORY_VIEW_HOLDERS;
429 if (position >= 0) {
430 outRect.left = mPadding;
431 outRect.right = mPadding;
432 }
433 }
434 }
435
436 /**
437 * SpanSizeLookup subclass which provides that the item in the first position spans the number
438 * of columns in the RecyclerView and all other items only take up a single span.
439 */
440 private class CategorySpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
441 CategoryAdapter mAdapter;
442
443 private CategorySpanSizeLookup(CategoryAdapter adapter) {
444 mAdapter = adapter;
445 }
446
447 @Override
448 public int getSpanSize(int position) {
449 if (position < NUM_NON_CATEGORY_VIEW_HOLDERS
450 || mAdapter.getItemViewType(position)
451 == CategoryAdapter.ITEM_VIEW_TYPE_LOADING_INDICATOR) {
452 return getNumColumns();
453 }
454
455 return 1;
456 }
457 }
Chuck Liao1e0501f2020-02-17 18:20:54 +0800458}