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