1 /*
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  */
16 package com.android.wallpaper.picker;
17 
18 import static com.android.wallpaper.picker.WallpaperPickerDelegate.PREVIEW_LIVE_WALLPAPER_REQUEST_CODE;
19 import static com.android.wallpaper.picker.WallpaperPickerDelegate.PREVIEW_WALLPAPER_REQUEST_CODE;
20 
21 import android.app.Activity;
22 import android.app.AlertDialog;
23 import android.content.Intent;
24 import android.content.res.TypedArray;
25 import android.graphics.Color;
26 import android.graphics.Point;
27 import android.graphics.Rect;
28 import android.net.Uri;
29 import android.os.Bundle;
30 import android.provider.Settings;
31 import android.util.DisplayMetrics;
32 import android.util.Log;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.widget.ImageView;
37 import android.widget.LinearLayout;
38 import android.widget.ProgressBar;
39 import android.widget.TextView;
40 
41 import androidx.annotation.NonNull;
42 import androidx.annotation.Nullable;
43 import androidx.cardview.widget.CardView;
44 import androidx.core.content.ContextCompat;
45 import androidx.core.view.AccessibilityDelegateCompat;
46 import androidx.core.view.ViewCompat;
47 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
48 import androidx.fragment.app.Fragment;
49 import androidx.recyclerview.widget.GridLayoutManager;
50 import androidx.recyclerview.widget.RecyclerView;
51 
52 import com.android.wallpaper.R;
53 import com.android.wallpaper.asset.Asset;
54 import com.android.wallpaper.effects.EffectsController;
55 import com.android.wallpaper.model.Category;
56 import com.android.wallpaper.model.CategoryProvider;
57 import com.android.wallpaper.model.LiveWallpaperInfo;
58 import com.android.wallpaper.model.WallpaperInfo;
59 import com.android.wallpaper.module.InjectorProvider;
60 import com.android.wallpaper.util.DeepLinkUtils;
61 import com.android.wallpaper.util.DisplayMetricsRetriever;
62 import com.android.wallpaper.util.ResourceUtils;
63 import com.android.wallpaper.util.SizeCalculator;
64 import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate;
65 import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate.BottomSheetHost;
66 
67 import com.bumptech.glide.Glide;
68 import com.google.android.material.snackbar.Snackbar;
69 
70 import java.util.ArrayList;
71 import java.util.List;
72 
73 /**
74  * Displays the UI which contains the categories of the wallpaper.
75  */
76 public class CategorySelectorFragment extends AppbarFragment {
77 
78     // The number of ViewHolders that don't pertain to category tiles.
79     // Currently 2: one for the metadata section and one for the "Select wallpaper" header.
80     private static final int NUM_NON_CATEGORY_VIEW_HOLDERS = 0;
81     private static final int SETTINGS_APP_INFO_REQUEST_CODE = 1;
82     private static final String TAG = "CategorySelectorFragment";
83     private static final String IMAGE_WALLPAPER_COLLECTION_ID = "image_wallpapers";
84     private static final int CREATIVE_CATEGORY_ROW_INDEX = 0;
85 
86     /**
87      * Interface to be implemented by an Fragment hosting a {@link CategorySelectorFragment}
88      */
89     public interface CategorySelectorFragmentHost {
90 
91         /**
92          * Requests to show the Android custom photo picker for the sake of picking a photo
93          * to set as the device's wallpaper.
94          */
requestCustomPhotoPicker(MyPhotosStarter.PermissionChangedListener listener)95         void requestCustomPhotoPicker(MyPhotosStarter.PermissionChangedListener listener);
96 
97         /**
98          * Shows the wallpaper page of the specific category.
99          *
100          * @param category the wallpaper's {@link Category}
101          */
show(Category category)102         void show(Category category);
103 
104 
105         /**
106          * Indicates if the host has toolbar to show the title. If it does, we should set the title
107          * there.
108          */
isHostToolbarShown()109         boolean isHostToolbarShown();
110 
111         /**
112          * Sets the title in the host's toolbar.
113          */
setToolbarTitle(CharSequence title)114         void setToolbarTitle(CharSequence title);
115 
116         /**
117          * Fetches the wallpaper categories.
118          */
fetchCategories()119         void fetchCategories();
120 
121         /**
122          * Cleans up the listeners which will be notified when there's a package event.
123          */
cleanUp()124         void cleanUp();
125     }
126 
127     private RecyclerView mImageGrid;
128     private CategoryAdapter mAdapter;
129     private GroupedCategoryAdapter mGroupedCategoryAdapter;
130     private CategoryProvider mCategoryProvider;
131     private ArrayList<Category> mCategories = new ArrayList<>();
132     private Point mTileSizePx;
133     private boolean mAwaitingCategories;
134     private ProgressBar mLoadingIndicator;
135     private ArrayList<Category> mCreativeCategories = new ArrayList<>();
136     private boolean mIsFeaturedCollectionAvailable;
137     private boolean mIsCreativeCategoryCollectionAvailable;
138     private boolean mIsCreativeWallpaperEnabled = false;
139 
140     @Override
onCreate(@ullable Bundle savedInstanceState)141     public void onCreate(@Nullable Bundle savedInstanceState) {
142         super.onCreate(savedInstanceState);
143         mCategoryProvider = InjectorProvider.getInjector().getCategoryProvider(requireContext());
144         mIsCreativeWallpaperEnabled = InjectorProvider.getInjector()
145             .getFlags().isAIWallpaperEnabled(requireContext());
146         if (mIsCreativeWallpaperEnabled) {
147             mGroupedCategoryAdapter = new GroupedCategoryAdapter(mCategories);
148         } else {
149             mAdapter = new CategoryAdapter(mCategories);
150         }
151     }
152 
153     @Nullable
154     @Override
onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)155     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
156                              @Nullable Bundle savedInstanceState) {
157         View view = inflater.inflate(R.layout.fragment_category_selector, container,
158                 /* attachToRoot= */ false);
159         mImageGrid = view.findViewById(R.id.category_grid);
160         mImageGrid.addItemDecoration(new GridPaddingDecoration(getResources().getDimensionPixelSize(
161                 R.dimen.grid_item_category_padding_horizontal)));
162         mTileSizePx = SizeCalculator.getCategoryTileSize(getActivity());
163         // In case CreativeWallpapers are enabled, it means we want to show the new view
164         // in the picker for which we have made a new adaptor
165         if (mIsCreativeWallpaperEnabled) {
166             mImageGrid.setAdapter(mGroupedCategoryAdapter);
167             GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(),
168                     getNumColumns()
169                             * GroupedCategorySpanSizeLookup.DEFAULT_CATEGORY_SPAN_SIZE);
170             gridLayoutManager.setSpanSizeLookup(new
171                     GroupedCategorySpanSizeLookup(mGroupedCategoryAdapter));
172             mImageGrid.setLayoutManager(gridLayoutManager);
173             //TODO (b/290267060): To be fixed when re-factoring of loading categories is done
174             mImageGrid.setItemAnimator(null);
175         } else {
176             mImageGrid.setAdapter(mAdapter);
177             GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(),
178                     getNumColumns() * CategorySpanSizeLookup.DEFAULT_CATEGORY_SPAN_SIZE);
179             gridLayoutManager.setSpanSizeLookup(new CategorySpanSizeLookup(mAdapter));
180             mImageGrid.setLayoutManager(gridLayoutManager);
181         }
182 
183         mLoadingIndicator = view.findViewById(R.id.loading_indicator);
184         mLoadingIndicator.setVisibility(View.VISIBLE);
185         mImageGrid.setVisibility(View.INVISIBLE);
186         mImageGrid.setAccessibilityDelegateCompat(
187                 new WallpaperPickerRecyclerViewAccessibilityDelegate(
188                         mImageGrid, (BottomSheetHost) getParentFragment(), getNumColumns()));
189 
190         if (getCategorySelectorFragmentHost().isHostToolbarShown()) {
191             view.findViewById(R.id.header_bar).setVisibility(View.GONE);
192             getCategorySelectorFragmentHost().setToolbarTitle(getText(R.string.wallpaper_title));
193         } else {
194             setUpToolbar(view);
195             setTitle(getText(R.string.wallpaper_title));
196         }
197 
198         if (!DeepLinkUtils.isDeepLink(getActivity().getIntent())) {
199             getCategorySelectorFragmentHost().fetchCategories();
200         }
201 
202         // For nav bar edge-to-edge effect.
203         mImageGrid.setOnApplyWindowInsetsListener((v, windowInsets) -> {
204             v.setPadding(
205                     v.getPaddingLeft(),
206                     v.getPaddingTop(),
207                     v.getPaddingRight(),
208                     windowInsets.getSystemWindowInsetBottom());
209             return windowInsets.consumeSystemWindowInsets();
210         });
211         return view;
212     }
213 
214     @Override
onDestroyView()215     public void onDestroyView() {
216         getCategorySelectorFragmentHost().cleanUp();
217         super.onDestroyView();
218     }
219 
220     /**
221      * Inserts the given category into the categories list in priority order.
222      */
addCategory(Category category, boolean loading)223     void addCategory(Category category, boolean loading) {
224         // If not previously waiting for categories, enter the waiting state by showing the loading
225         // indicator.
226         if (mIsCreativeWallpaperEnabled) {
227             if (loading && !mAwaitingCategories) {
228                 mAwaitingCategories = true;
229             }
230             // Not add existing category to category list
231             if (mCategories.indexOf(category) >= 0) {
232                 updateCategory(category);
233                 return;
234             }
235 
236             int priority = category.getPriority();
237             if (category.supportsUserCreatedWallpapers()) {
238                 mCreativeCategories.add(category);
239             }
240 
241             int index = 0;
242             while (index < mCategories.size() && priority >= mCategories.get(index).getPriority()) {
243                 index++;
244             }
245 
246             mCategories.add(index, category);
247         } else {
248             if (loading && !mAwaitingCategories) {
249                 mAdapter.notifyItemChanged(getNumColumns());
250                 mAdapter.notifyItemInserted(getNumColumns());
251                 mAwaitingCategories = true;
252             }
253             // Not add existing category to category list
254             if (mCategories.indexOf(category) >= 0) {
255                 updateCategory(category);
256                 return;
257             }
258 
259             int priority = category.getPriority();
260 
261             int index = 0;
262             while (index < mCategories.size() && priority >= mCategories.get(index).getPriority()) {
263                 index++;
264             }
265 
266             mCategories.add(index, category);
267             if (mAdapter != null) {
268                 // Offset the index because of the static metadata element
269                 // at beginning of RecyclerView.
270                 mAdapter.notifyItemInserted(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
271             }
272         }
273     }
274 
removeCategory(Category category)275     void removeCategory(Category category) {
276         int index = mCategories.indexOf(category);
277         if (index >= 0) {
278             mCategories.remove(index);
279             if (mIsCreativeWallpaperEnabled) {
280                 int indexCreativeCategory = mCreativeCategories.indexOf(category);
281                 if (indexCreativeCategory >= 0) {
282                     mCreativeCategories.remove(indexCreativeCategory);
283                 }
284             } else {
285                 mAdapter.notifyItemRemoved(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
286             }
287         }
288     }
289 
updateCategory(Category category)290     void updateCategory(Category category) {
291         int index = mCategories.indexOf(category);
292         if (index >= 0) {
293             mCategories.set(index, category);
294             if (mIsCreativeWallpaperEnabled) {
295                 int indexCreativeCategory = mCreativeCategories.indexOf(category);
296                 if (indexCreativeCategory >= 0) {
297                     mCreativeCategories.set(indexCreativeCategory, category);
298                 }
299             } else {
300                 mAdapter.notifyItemChanged(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
301             }
302         }
303     }
304 
clearCategories()305     void clearCategories() {
306         mCategories.clear();
307         if (mIsCreativeWallpaperEnabled) {
308             mCreativeCategories.clear();
309             mGroupedCategoryAdapter.notifyDataSetChanged();
310         } else {
311             mAdapter.notifyDataSetChanged();
312         }
313     }
314 
315     /**
316      * Notifies that no further categories are expected.
317      */
doneFetchingCategories()318     void doneFetchingCategories() {
319         notifyDataSetChanged();
320         mLoadingIndicator.setVisibility(View.INVISIBLE);
321         mImageGrid.setVisibility(View.VISIBLE);
322         mAwaitingCategories = false;
323         mIsFeaturedCollectionAvailable = mCategoryProvider.isFeaturedCollectionAvailable();
324         mIsCreativeCategoryCollectionAvailable = mCategoryProvider.isCreativeCategoryAvailable();
325     }
326 
notifyDataSetChanged()327     void notifyDataSetChanged() {
328         if (mIsCreativeWallpaperEnabled) {
329             mGroupedCategoryAdapter.notifyDataSetChanged();
330         } else {
331             mAdapter.notifyDataSetChanged();
332         }
333     }
334 
getNumColumns()335     private int getNumColumns() {
336         Activity activity = getActivity();
337         return activity == null ? 1 : SizeCalculator.getNumCategoryColumns(activity);
338     }
339 
340 
getCategorySelectorFragmentHost()341     private CategorySelectorFragmentHost getCategorySelectorFragmentHost() {
342         Fragment parentFragment = getParentFragment();
343         if (parentFragment != null) {
344             return (CategorySelectorFragmentHost) parentFragment;
345         } else {
346             return (CategorySelectorFragmentHost) getActivity();
347         }
348     }
349 
350     /**
351      * ViewHolder subclass for a category tile in the RecyclerView.
352      */
353     private class CategoryHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
354         private Category mCategory;
355         private ImageView mImageView;
356         private ImageView mOverlayIconView;
357         private TextView mTitleView;
358 
CategoryHolder(View itemView)359         CategoryHolder(View itemView) {
360             super(itemView);
361             itemView.setOnClickListener(this);
362 
363             mImageView = itemView.findViewById(R.id.image);
364             mOverlayIconView = itemView.findViewById(R.id.overlay_icon);
365             mTitleView = itemView.findViewById(R.id.category_title);
366 
367             CardView categoryView = itemView.findViewById(R.id.category);
368             categoryView.getLayoutParams().height = mTileSizePx.y;
369             categoryView.setRadius(getResources().getDimension(R.dimen.grid_item_all_radius_small));
370         }
371 
372         @Override
onClick(View view)373         public void onClick(View view) {
374             Activity activity = getActivity();
375 
376             if (mCategory.supportsCustomPhotos()) {
377                 EffectsController effectsController =
378                         InjectorProvider.getInjector().getEffectsController(getContext());
379                 if (effectsController != null && !effectsController.isEffectTriggered()) {
380                     effectsController.triggerEffect(getContext());
381                 }
382                 getCategorySelectorFragmentHost().requestCustomPhotoPicker(
383                         new MyPhotosStarter.PermissionChangedListener() {
384                             @Override
385                             public void onPermissionsGranted() {
386                                 drawThumbnailAndOverlayIcon();
387                             }
388 
389                             @Override
390                             public void onPermissionsDenied(boolean dontAskAgain) {
391                                 if (dontAskAgain) {
392                                     showPermissionSnackbar();
393                                 }
394                             }
395                         });
396                 return;
397             }
398 
399             if (mCategory.isSingleWallpaperCategory()) {
400                 WallpaperInfo wallpaper = mCategory.getSingleWallpaper();
401 
402                 InjectorProvider.getInjector().getWallpaperPersister(activity)
403                         .setWallpaperInfoInPreview(wallpaper);
404                 wallpaper.showPreview(activity,
405                         InjectorProvider.getInjector().getPreviewActivityIntentFactory(),
406                         wallpaper instanceof LiveWallpaperInfo ? PREVIEW_LIVE_WALLPAPER_REQUEST_CODE
407                                 : PREVIEW_WALLPAPER_REQUEST_CODE, true);
408                 return;
409             }
410 
411             getCategorySelectorFragmentHost().show(mCategory);
412         }
413 
414         /**
415          * Binds the given category to this CategoryHolder.
416          */
bindCategory(Category category)417         private void bindCategory(Category category) {
418             mCategory = category;
419             mTitleView.setText(category.getTitle());
420             drawThumbnailAndOverlayIcon();
421         }
422 
423         /**
424          * Draws the CategoryHolder's thumbnail and overlay icon.
425          */
drawThumbnailAndOverlayIcon()426         private void drawThumbnailAndOverlayIcon() {
427             mOverlayIconView.setImageDrawable(mCategory.getOverlayIcon(
428                     getActivity().getApplicationContext()));
429             Asset thumbnail = mCategory.getThumbnail(getActivity().getApplicationContext());
430             if (thumbnail != null) {
431                 // Size the overlay icon according to the category.
432                 int overlayIconDimenDp = mCategory.getOverlayIconSizeDp();
433                 DisplayMetrics metrics = DisplayMetricsRetriever.getInstance().getDisplayMetrics(
434                         getResources(), getActivity().getWindowManager().getDefaultDisplay());
435                 int overlayIconDimenPx = (int) (overlayIconDimenDp * metrics.density);
436                 mOverlayIconView.getLayoutParams().width = overlayIconDimenPx;
437                 mOverlayIconView.getLayoutParams().height = overlayIconDimenPx;
438                 thumbnail.loadDrawable(getActivity(), mImageView,
439                         ResourceUtils.getColorAttr(
440                                 getActivity(),
441                                 android.R.attr.colorSecondary
442                         ));
443             } else {
444                 // TODO(orenb): Replace this workaround for b/62584914 with a proper way of
445                 //  unloading the ImageView such that no incorrect image is improperly loaded upon
446                 //  rapid scroll.
447                 mImageView.setBackgroundColor(
448                         getResources().getColor(R.color.myphoto_background_color));
449                 Object nullObj = null;
450                 Glide.with(getActivity())
451                         .asDrawable()
452                         .load(nullObj)
453                         .into(mImageView);
454 
455             }
456         }
457     }
458 
showPermissionSnackbar()459     private void showPermissionSnackbar() {
460         Snackbar snackbar = Snackbar.make(getView(), R.string.settings_snackbar_description,
461                 Snackbar.LENGTH_LONG);
462         Snackbar.SnackbarLayout layout = (Snackbar.SnackbarLayout) snackbar.getView();
463         TextView textView = (TextView) layout.findViewById(
464                 com.google.android.material.R.id.snackbar_text);
465         layout.setBackgroundResource(R.drawable.snackbar_background);
466         TypedArray typedArray = getContext().obtainStyledAttributes(
467                 new int[]{android.R.attr.textColorPrimary,
468                         com.android.internal.R.attr.materialColorPrimaryContainer});
469         textView.setTextColor(typedArray.getColor(0, Color.TRANSPARENT));
470         snackbar.setActionTextColor(typedArray.getColor(1, Color.TRANSPARENT));
471         typedArray.recycle();
472 
473         snackbar.setAction(getContext().getString(R.string.settings_snackbar_enable),
474                 new View.OnClickListener() {
475                     @Override
476                     public void onClick(View view) {
477                         startSettings(SETTINGS_APP_INFO_REQUEST_CODE);
478                     }
479                 });
480         snackbar.show();
481     }
482 
startSettings(int resultCode)483     private void startSettings(int resultCode) {
484         Activity activity = getActivity();
485         if (activity == null) {
486             return;
487         }
488         Intent appInfoIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
489         Uri uri = Uri.fromParts("package", activity.getPackageName(), /* fragment= */ null);
490         appInfoIntent.setData(uri);
491         startActivityForResult(appInfoIntent, resultCode);
492     }
493 
494     /*
495        This is for FeaturedCategories and only present in CategoryAdaptor
496      */
497     private class FeaturedCategoryHolder extends CategoryHolder {
498 
FeaturedCategoryHolder(View itemView)499         FeaturedCategoryHolder(View itemView) {
500             super(itemView);
501             CardView categoryView = itemView.findViewById(R.id.category);
502             categoryView.getLayoutParams().height =
503                     SizeCalculator.getFeaturedCategoryTileSize(getActivity()).y;
504             categoryView.setRadius(getResources().getDimension(R.dimen.grid_item_all_radius));
505         }
506     }
507 
508     /*
509        This is re-used between both GroupedCategoryAdaptor and CategoryAdaptor
510      */
511     private class MyPhotosCategoryHolder extends CategoryHolder {
512 
MyPhotosCategoryHolder(View itemView)513         MyPhotosCategoryHolder(View itemView) {
514             super(itemView);
515             // Reuse the height of featured category since My Photos category & featured category
516             // have the same height in current UI design.
517             CardView categoryView = itemView.findViewById(R.id.category);
518             int height = SizeCalculator.getFeaturedCategoryTileSize(getActivity()).y;
519             categoryView.getLayoutParams().height = height;
520             // Use the height as the card corner radius for the "My photos" category
521             // for a stadium border.
522             categoryView.setRadius(height);
523             // We do this since itemView here refers to the broader LinearLayout defined in
524             // the My Photos xml, which includes the section title. Doing this allows us to make
525             // sure that the onClickListener is configured only on the My Photos grid item.
526             if (mIsCreativeWallpaperEnabled) {
527                 itemView.setOnClickListener(null);
528                 itemView.setClickable(false);
529                 itemView.findViewById(R.id.tile).setOnClickListener(this);
530             }
531         }
532     }
533 
534     private class GroupCategoryHolder extends RecyclerView.ViewHolder {
535         private static final float INDIVIDUAL_TILE_WEIGHT = 1.0f;
536         LayoutInflater mLayoutInflater = LayoutInflater.from(getActivity());
537         private ArrayList<Category> mCategories = new ArrayList<>();
538         private ArrayList<ImageView> mImageViews = new ArrayList<>();
539         private ArrayList<ImageView> mOverlayIconViews = new ArrayList<>();
540         private ArrayList<TextView> mTextViews = new ArrayList<>();
541 
GroupCategoryHolder(View itemView, int mCreativeCategoriesSize)542         GroupCategoryHolder(View itemView, int mCreativeCategoriesSize) {
543             super(itemView);
544             LinearLayout linearLayout = itemView.findViewById(R.id.linear_layout_for_cards);
545             for (int i = 0; i < mCreativeCategoriesSize; i++) {
546                 LinearLayout gridItemCategory = (LinearLayout)
547                         mLayoutInflater.inflate(R.layout.grid_item_category, null);
548                 if (gridItemCategory != null) {
549                     int position = i; //Used in onClickListener
550                     mImageViews.add(gridItemCategory.findViewById(R.id.image));
551                     mOverlayIconViews.add(gridItemCategory.findViewById(R.id.overlay_icon));
552                     mTextViews.add(gridItemCategory.findViewById(R.id.category_title));
553                     setLayoutParams(gridItemCategory);
554                     linearLayout.addView(gridItemCategory);
555                     gridItemCategory.setOnClickListener(view -> {
556                         onClickListenerForCreativeCategory(position);
557                     });
558                     // Make sure the column number is announced when there is more than 1 creative
559                     // category.
560                     if (mCreativeCategoriesSize > 1) {
561                         ViewCompat.setAccessibilityDelegate(gridItemCategory,
562                                 new AccessibilityDelegateCompat() {
563                                     @Override
564                                     public void onInitializeAccessibilityNodeInfo(View host,
565                                             AccessibilityNodeInfoCompat info) {
566                                         super.onInitializeAccessibilityNodeInfo(host, info);
567                                         info.setCollectionItemInfo(
568                                                 AccessibilityNodeInfoCompat.CollectionItemInfoCompat
569                                                         .obtain(
570                                                             /* rowIndex= */
571                                                                 CREATIVE_CATEGORY_ROW_INDEX,
572                                                             /* rowSpan= */ 1,
573                                                             /* columnIndex= */ position,
574                                                             /* columnSpan= */ 1,
575                                                             /* heading= */ false));
576                                     }
577                                 });
578                     }
579                 }
580             }
581         }
582 
onClickListenerForCreativeCategory(int position)583         private void onClickListenerForCreativeCategory(int position) {
584             Activity activity = getActivity();
585             if (mCategories.get(position).supportsCustomPhotos()) {
586                 getCategorySelectorFragmentHost().requestCustomPhotoPicker(
587                         new MyPhotosStarter.PermissionChangedListener() {
588                             @Override
589                             public void onPermissionsGranted() {
590                                 drawThumbnailAndOverlayIcon(
591                                         mOverlayIconViews.get(position),
592                                         mCategories.get(position),
593                                         mImageViews.get(position));
594                             }
595 
596                             @Override
597                             public void onPermissionsDenied(boolean dontAskAgain) {
598                                 if (dontAskAgain) {
599                                     showPermissionSnackbar();
600                                 }
601                             }
602                         });
603                 return;
604             }
605 
606             if (mCategories.get(position).isSingleWallpaperCategory()) {
607                 WallpaperInfo wallpaper = mCategories.get(position)
608                         .getSingleWallpaper();
609 
610                 InjectorProvider.getInjector().getWallpaperPersister(activity)
611                         .setWallpaperInfoInPreview(wallpaper);
612                 wallpaper.showPreview(activity,
613                         InjectorProvider.getInjector().getPreviewActivityIntentFactory(),
614                         wallpaper instanceof LiveWallpaperInfo
615                                 ? PREVIEW_LIVE_WALLPAPER_REQUEST_CODE
616                                 : PREVIEW_WALLPAPER_REQUEST_CODE, true);
617                 return;
618             }
619 
620             getCategorySelectorFragmentHost().show(mCategories.get(position));
621         }
622 
setLayoutParams(LinearLayout gridItemCategory)623         private void setLayoutParams(LinearLayout gridItemCategory) {
624             LinearLayout.LayoutParams params =
625                     (LinearLayout.LayoutParams) gridItemCategory.getLayoutParams();
626             if (params == null) {
627                 params =
628                         new LinearLayout.LayoutParams(
629                                 LinearLayout.LayoutParams.MATCH_PARENT,
630                                 LinearLayout.LayoutParams.WRAP_CONTENT);
631             }
632             params.setMargins(
633                     (int) getResources().getDimension(
634                             R.dimen.creative_category_grid_padding_horizontal),
635                     (int) getResources().getDimension(
636                             R.dimen.creative_category_grid_padding_vertical),
637                     (int) getResources().getDimension(
638                             R.dimen.creative_category_grid_padding_horizontal),
639                     (int) getResources().getDimension(
640                             R.dimen.creative_category_grid_padding_vertical));
641             CardView cardView = gridItemCategory.findViewById(R.id.category);
642             cardView.getLayoutParams().height = SizeCalculator
643                     .getFeaturedCategoryTileSize(getActivity()).y / 2;
644             cardView.setRadius(getResources().getDimension(R.dimen.grid_item_all_radius));
645             params.weight = INDIVIDUAL_TILE_WEIGHT;
646             gridItemCategory.setLayoutParams(params);
647         }
648 
drawThumbnailAndOverlayIcon(ImageView mOverlayIconView, Category mCategory, ImageView mImageView)649         private void drawThumbnailAndOverlayIcon(ImageView mOverlayIconView,
650                 Category mCategory, ImageView mImageView) {
651             mOverlayIconView.setImageDrawable(mCategory.getOverlayIcon(
652                     getActivity().getApplicationContext()));
653             Asset thumbnail = mCategory.getThumbnail(getActivity().getApplicationContext());
654             if (thumbnail != null) {
655                 // Size the overlay icon according to the category.
656                 int overlayIconDimenDp = mCategory.getOverlayIconSizeDp();
657                 DisplayMetrics metrics = DisplayMetricsRetriever.getInstance().getDisplayMetrics(
658                         getResources(), getActivity().getWindowManager().getDefaultDisplay());
659                 int overlayIconDimenPx = (int) (overlayIconDimenDp * metrics.density);
660                 mOverlayIconView.getLayoutParams().width = overlayIconDimenPx;
661                 mOverlayIconView.getLayoutParams().height = overlayIconDimenPx;
662                 thumbnail.loadDrawable(getActivity(), mImageView,
663                         ResourceUtils.getColorAttr(
664                                 getActivity(),
665                                 android.R.attr.colorSecondary
666                         ));
667             } else {
668                 mImageView.setBackgroundColor(
669                         getResources().getColor(R.color.myphoto_background_color));
670                 Object nullObj = null;
671                 Glide.with(getActivity())
672                         .asDrawable()
673                         .load(nullObj)
674                         .into(mImageView);
675             }
676         }
677 
bindCategory(ArrayList<Category> creativeCategories)678         private void bindCategory(ArrayList<Category> creativeCategories) {
679             for (int i = 0; i < creativeCategories.size(); i++) {
680                 mCategories.add(creativeCategories.get(i));
681                 mTextViews.get(i).setText(creativeCategories.get(i).getTitle());
682                 drawThumbnailAndOverlayIcon(mOverlayIconViews.get(i), mCategories.get(i),
683                         mImageViews.get(i));
684             }
685         }
686     }
687 
688     /**
689      * RecyclerView Adapter subclass for the category tiles in the RecyclerView. This excludes
690      * CreativeCategory and has FeaturedCategory
691      */
692     private class CategoryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
693             implements MyPhotosStarter.PermissionChangedListener {
694         private static final int ITEM_VIEW_TYPE_MY_PHOTOS = 1;
695         private static final int ITEM_VIEW_TYPE_FEATURED_CATEGORY = 2;
696         private static final int ITEM_VIEW_TYPE_CATEGORY = 3;
697         private List<Category> mCategories;
698 
CategoryAdapter(List<Category> categories)699         private CategoryAdapter(List<Category> categories) {
700             mCategories = categories;
701         }
702 
703         @Override
getItemViewType(int position)704         public int getItemViewType(int position) {
705             if (position == 0) {
706                 return ITEM_VIEW_TYPE_MY_PHOTOS;
707             }
708 
709             if (mIsFeaturedCollectionAvailable && (position == 1 || position == 2)) {
710                 return ITEM_VIEW_TYPE_FEATURED_CATEGORY;
711             }
712 
713             return ITEM_VIEW_TYPE_CATEGORY;
714         }
715 
716         @Override
onCreateViewHolder(ViewGroup parent, int viewType)717         public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
718             LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
719             View view;
720 
721             switch (viewType) {
722                 case ITEM_VIEW_TYPE_MY_PHOTOS:
723                     view = layoutInflater.inflate(R.layout.grid_item_category,
724                             parent, /* attachToRoot= */ false);
725                     return new MyPhotosCategoryHolder(view);
726                 case ITEM_VIEW_TYPE_FEATURED_CATEGORY:
727                     view = layoutInflater.inflate(R.layout.grid_item_category,
728                             parent, /* attachToRoot= */ false);
729                     return new FeaturedCategoryHolder(view);
730                 case ITEM_VIEW_TYPE_CATEGORY:
731                     view = layoutInflater.inflate(R.layout.grid_item_category,
732                             parent, /* attachToRoot= */ false);
733                     return new CategoryHolder(view);
734                 default:
735                     Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
736                     return null;
737             }
738         }
739 
740         @Override
onBindViewHolder(RecyclerView.ViewHolder holder, int position)741         public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
742             int viewType = getItemViewType(position);
743             switch (viewType) {
744                 case ITEM_VIEW_TYPE_MY_PHOTOS:
745                 case ITEM_VIEW_TYPE_FEATURED_CATEGORY:
746                 case ITEM_VIEW_TYPE_CATEGORY:
747                     // Offset position to get category index to account for the non-category view
748                     // holders.
749                     Category category = mCategories.get(position - NUM_NON_CATEGORY_VIEW_HOLDERS);
750                     ((CategoryHolder) holder).bindCategory(category);
751                     break;
752                 default:
753                     Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
754             }
755         }
756 
757         @Override
getItemCount()758         public int getItemCount() {
759             // Add to size of categories to account for the metadata related views.
760             int size = mCategories.size() + NUM_NON_CATEGORY_VIEW_HOLDERS;
761 
762             return size;
763         }
764 
765         @Override
onPermissionsGranted()766         public void onPermissionsGranted() {
767             notifyDataSetChanged();
768         }
769 
770         @Override
onPermissionsDenied(boolean dontAskAgain)771         public void onPermissionsDenied(boolean dontAskAgain) {
772             if (!dontAskAgain) {
773                 return;
774             }
775 
776             String permissionNeededMessage =
777                     getString(R.string.permission_needed_explanation_go_to_settings);
778             AlertDialog dialog = new AlertDialog.Builder(getActivity(), R.style.LightDialogTheme)
779                     .setMessage(permissionNeededMessage)
780                     .setPositiveButton(android.R.string.ok, null /* onClickListener */)
781                     .setNegativeButton(
782                             R.string.settings_button_label,
783                             (dialogInterface, i) -> {
784                                 startSettings(SETTINGS_APP_INFO_REQUEST_CODE);
785                             })
786                     .create();
787             dialog.show();
788         }
789     }
790 
791     /**
792      * RecyclerView GroupedCategoryAdaptor subclass for the category tiles in the RecyclerView.
793      * This removes FeaturedCategory and adds CreativeCategory with a slightly different layout
794      */
795     private class GroupedCategoryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
796             implements MyPhotosStarter.PermissionChangedListener {
797         private static final int ITEM_VIEW_TYPE_MY_PHOTOS = 1;
798         private static final int ITEM_VIEW_TYPE_CREATIVE_CATEGORY = 2;
799         private static final int ITEM_VIEW_TYPE_CATEGORY = 3;
800         private List<Category> mCategories;
801 
GroupedCategoryAdapter(List<Category> categories)802         private GroupedCategoryAdapter(List<Category> categories) {
803             mCategories = categories;
804         }
805 
806         @Override
getItemViewType(int position)807         public int getItemViewType(int position) {
808             if (mCategories.stream().anyMatch(Category::supportsUserCreatedWallpapers)) {
809                 if (position == CREATIVE_CATEGORY_ROW_INDEX) {
810                     return ITEM_VIEW_TYPE_CREATIVE_CATEGORY;
811                 }
812                 if (position == 1) {
813                     return ITEM_VIEW_TYPE_MY_PHOTOS;
814                 }
815             } else {
816                 if (position == 0) {
817                     return ITEM_VIEW_TYPE_MY_PHOTOS;
818                 }
819             }
820             return ITEM_VIEW_TYPE_CATEGORY;
821         }
822 
823         @Override
onCreateViewHolder(ViewGroup parent, int viewType)824         public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
825             LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
826 
827             switch (viewType) {
828                 case ITEM_VIEW_TYPE_MY_PHOTOS:
829                     View view = layoutInflater.inflate(R.layout.my_photos,
830                             parent, /* attachToRoot= */ false);
831                     return new MyPhotosCategoryHolder(view);
832                 case ITEM_VIEW_TYPE_CREATIVE_CATEGORY:
833                     view = layoutInflater.inflate(R.layout.creative_wallpaper,
834                             parent, /* attachToRoot= */ false);
835                     return new GroupCategoryHolder(view, mCreativeCategories.size());
836                 case ITEM_VIEW_TYPE_CATEGORY:
837                     view = layoutInflater.inflate(R.layout.grid_item_category,
838                             parent, /* attachToRoot= */ false);
839                     return new CategoryHolder(view);
840                 default:
841                     Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
842                     return null;
843             }
844         }
845 
846         @Override
onBindViewHolder(RecyclerView.ViewHolder holder, int position)847         public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
848             int viewType = getItemViewType(position);
849             switch (viewType) {
850                 case ITEM_VIEW_TYPE_CREATIVE_CATEGORY:
851                     ((GroupCategoryHolder) holder).bindCategory(mCreativeCategories);
852                     break;
853                 case ITEM_VIEW_TYPE_MY_PHOTOS:
854                     holder.setIsRecyclable(false);
855                 case ITEM_VIEW_TYPE_CATEGORY:
856                     // Offset position to get category index to account for the non-category view
857                     // holders.
858                     if (mIsCreativeCategoryCollectionAvailable) {
859                         int numCreativeCategories = mCreativeCategories.size();
860                         int positionRelativeToCreativeCategory = position + numCreativeCategories
861                                 - 1;
862                         Category category = mCategories.get(
863                                 positionRelativeToCreativeCategory - NUM_NON_CATEGORY_VIEW_HOLDERS);
864                         ((CategoryHolder) holder).bindCategory(category);
865                     } else {
866                         Category category = mCategories.get(position
867                                 - NUM_NON_CATEGORY_VIEW_HOLDERS);
868                         ((CategoryHolder) holder).bindCategory(category);
869                     }
870                     break;
871                 default:
872                     Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
873             }
874         }
875 
876         @Override
getItemCount()877         public int getItemCount() {
878             // Add to size of categories to account for the metadata related views.
879             int size = mCategories.size() + NUM_NON_CATEGORY_VIEW_HOLDERS;
880             // This is done to make sure all CreativeCategories are accounted for
881             // in one single block, therefore subtracted the size of CreativeCategories
882             // from total count
883             if (mCreativeCategories.size() >= 2) {
884                 size = size - (mCreativeCategories.size() - 1);
885             }
886             return size;
887         }
888 
889         @Override
onPermissionsGranted()890         public void onPermissionsGranted() {
891             notifyDataSetChanged();
892         }
893 
894         @Override
onPermissionsDenied(boolean dontAskAgain)895         public void onPermissionsDenied(boolean dontAskAgain) {
896             if (!dontAskAgain) {
897                 return;
898             }
899 
900             String permissionNeededMessage =
901                     getString(R.string.permission_needed_explanation_go_to_settings);
902             AlertDialog dialog = new AlertDialog.Builder(getActivity(), R.style.LightDialogTheme)
903                     .setMessage(permissionNeededMessage)
904                     .setPositiveButton(android.R.string.ok, null /* onClickListener */)
905                     .setNegativeButton(
906                             R.string.settings_button_label,
907                             (dialogInterface, i) -> {
908                                 startSettings(SETTINGS_APP_INFO_REQUEST_CODE);
909                             })
910                     .create();
911             dialog.show();
912         }
913     }
914 
915     private class GridPaddingDecoration extends RecyclerView.ItemDecoration {
916 
917         private final int mPadding;
918 
GridPaddingDecoration(int padding)919         GridPaddingDecoration(int padding) {
920             mPadding = padding;
921         }
922 
923         @Override
getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)924         public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
925                                    RecyclerView.State state) {
926             int position = parent.getChildAdapterPosition(view) - NUM_NON_CATEGORY_VIEW_HOLDERS;
927             if (position >= 0) {
928                 outRect.left = mPadding;
929                 outRect.right = mPadding;
930             }
931 
932             RecyclerView.ViewHolder viewHolder = parent.getChildViewHolder(view);
933             if (viewHolder instanceof MyPhotosCategoryHolder
934                     || viewHolder instanceof GroupCategoryHolder
935                     || viewHolder instanceof FeaturedCategoryHolder) {
936                 outRect.bottom = getResources().getDimensionPixelSize(
937                         R.dimen.grid_item_featured_category_padding_bottom);
938             } else {
939                 outRect.bottom = getResources().getDimensionPixelSize(
940                         R.dimen.grid_item_category_padding_bottom);
941             }
942         }
943     }
944     @Override
onActivityResult(int requestCode, int resultCode, Intent data)945     public void onActivityResult(int requestCode, int resultCode, Intent data) {
946         if (requestCode == SETTINGS_APP_INFO_REQUEST_CODE) {
947             notifyDataSetChanged();
948         }
949     }
950 
951     /**
952      * SpanSizeLookup subclass which works with CategoryAdaptor and provides that the item in the
953      * first position spans the number of columns in the RecyclerView and all other items only
954      * take up a single span.
955      */
956     private class CategorySpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
957         private static final int DEFAULT_CATEGORY_SPAN_SIZE = 2;
958 
959         CategoryAdapter mAdapter;
960 
CategorySpanSizeLookup(CategoryAdapter adapter)961         private CategorySpanSizeLookup(CategoryAdapter adapter) {
962             mAdapter = adapter;
963         }
964 
965         @Override
getSpanSize(int position)966         public int getSpanSize(int position) {
967             if (position < NUM_NON_CATEGORY_VIEW_HOLDERS || mAdapter.getItemViewType(
968                     position) == CategoryAdapter.ITEM_VIEW_TYPE_MY_PHOTOS) {
969                 return getNumColumns() * DEFAULT_CATEGORY_SPAN_SIZE;
970             }
971 
972             if (mAdapter.getItemViewType(position)
973                     == CategoryAdapter.ITEM_VIEW_TYPE_FEATURED_CATEGORY) {
974                 return getNumColumns() * DEFAULT_CATEGORY_SPAN_SIZE / 2;
975             }
976             return DEFAULT_CATEGORY_SPAN_SIZE;
977         }
978     }
979 
980     /**
981      * SpanSizeLookup subclass which works with GroupCategoryAdaptor and provides that
982      * item of type photos and items of type CreativeCategory spans the number of columns in the
983      * RecyclerView and all other items only take up a single span.
984      */
985     private class GroupedCategorySpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
986         private static final int DEFAULT_CATEGORY_SPAN_SIZE = 1;
987 
988         GroupedCategoryAdapter mAdapter;
989 
GroupedCategorySpanSizeLookup(GroupedCategoryAdapter adapter)990         private GroupedCategorySpanSizeLookup(GroupedCategoryAdapter adapter) {
991             mAdapter = adapter;
992         }
993 
994         @Override
getSpanSize(int position)995         public int getSpanSize(int position) {
996             if (position < NUM_NON_CATEGORY_VIEW_HOLDERS || mAdapter.getItemViewType(
997                     position) == GroupedCategoryAdapter.ITEM_VIEW_TYPE_MY_PHOTOS) {
998                 return getNumColumns() * DEFAULT_CATEGORY_SPAN_SIZE;
999             }
1000 
1001             if (mAdapter.getItemViewType(position)
1002                     == GroupedCategoryAdapter.ITEM_VIEW_TYPE_CREATIVE_CATEGORY) {
1003                 return getNumColumns() * DEFAULT_CATEGORY_SPAN_SIZE;
1004             }
1005             return DEFAULT_CATEGORY_SPAN_SIZE;
1006         }
1007     }
1008 
1009     @Override
getToolbarTextColor()1010     protected int getToolbarTextColor() {
1011         return ContextCompat.getColor(requireContext(), R.color.system_on_surface);
1012     }
1013 }
1014