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