Extract the gallery from CategoryFragment into CategorySelectorFragment.

This is a preparation for the new behavior of clicking a category in the gallery will show the wallpapers in the current UI container (by replacing a fragment) instead of launching an activity.

Bug: 149670265
Change-Id: Ice62d946ca05eee012770bfd84f34549f62640ff
diff --git a/res/layout/fragment_category_picker.xml b/res/layout/fragment_category_picker.xml
index 558d6a3..9af83df 100755
--- a/res/layout/fragment_category_picker.xml
+++ b/res/layout/fragment_category_picker.xml
@@ -36,19 +36,11 @@
         android:layout_marginTop="?android:attr/actionBarSize"
         android:visibility="gone" />
 
-    <androidx.recyclerview.widget.RecyclerView
-        android:id="@+id/category_grid"
+    <FrameLayout
+        android:id="@+id/category_fragment_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:background="?android:colorPrimary"
-        android:clipToPadding="false"
-        android:fitsSystemWindows="true"
-        android:paddingHorizontal="@dimen/grid_edge_space"
-        android:paddingTop="@dimen/grid_padding"
-        android:scrollbarSize="@dimen/grid_padding"
-        android:scrollbarStyle="outsideOverlay"
-        android:scrollbarThumbVertical="@color/scrollbar_thumb_color_dark"
-        android:scrollbars="vertical"
         app:layout_behavior="@string/bottom_sheet_behavior" />
 
 </androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/res/layout/fragment_category_selector.xml b/res/layout/fragment_category_selector.xml
new file mode 100644
index 0000000..2e83774
--- /dev/null
+++ b/res/layout/fragment_category_selector.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+     Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/category_grid"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:clipToPadding="false"
+    android:fitsSystemWindows="true"
+    android:paddingHorizontal="@dimen/grid_edge_space"
+    android:paddingTop="@dimen/grid_padding"
+    android:scrollbarSize="@dimen/grid_padding"
+    android:scrollbarStyle="outsideOverlay"
+    android:scrollbarThumbVertical="@color/scrollbar_thumb_color_dark"
+    android:scrollbars="vertical"
+    app:layout_behavior="@string/bottom_sheet_behavior" />
diff --git a/src/com/android/wallpaper/picker/CategoryFragment.java b/src/com/android/wallpaper/picker/CategoryFragment.java
index 7093b09..58374ce 100755
--- a/src/com/android/wallpaper/picker/CategoryFragment.java
+++ b/src/com/android/wallpaper/picker/CategoryFragment.java
@@ -18,12 +18,10 @@
 import android.app.Activity;
 import android.app.ProgressDialog;
 import android.content.Context;
-import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.graphics.Point;
 import android.graphics.PorterDuff.Mode;
-import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
@@ -40,21 +38,16 @@
 import android.widget.FrameLayout;
 import android.widget.ImageButton;
 import android.widget.ImageView;
-import android.widget.ProgressBar;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.appcompat.app.AlertDialog;
 import androidx.cardview.widget.CardView;
-import androidx.recyclerview.widget.GridLayoutManager;
-import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup;
 import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 import androidx.viewpager.widget.PagerAdapter;
 
 import com.android.wallpaper.R;
-import com.android.wallpaper.asset.Asset;
 import com.android.wallpaper.config.Flags;
 import com.android.wallpaper.model.Category;
 import com.android.wallpaper.model.WallpaperInfo;
@@ -68,11 +61,11 @@
 import com.android.wallpaper.module.WallpaperPreferences.PresentationMode;
 import com.android.wallpaper.module.WallpaperRotationRefresher;
 import com.android.wallpaper.module.WallpaperRotationRefresher.Listener;
+import com.android.wallpaper.picker.CategorySelectorFragment.CategorySelectorFragmentHost;
 import com.android.wallpaper.picker.MyPhotosStarter.MyPhotosStarterProvider;
 import com.android.wallpaper.picker.MyPhotosStarter.PermissionChangedListener;
 import com.android.wallpaper.util.DisplayMetricsRetriever;
 import com.android.wallpaper.util.ScreenSizeCalculator;
-import com.android.wallpaper.util.TileSizeCalculator;
 import com.android.wallpaper.widget.PreviewPager;
 
 import com.bumptech.glide.Glide;
@@ -86,7 +79,7 @@
 /**
  * Displays the Main UI for picking a category of wallpapers to choose from.
  */
-public class CategoryFragment extends ToolbarFragment {
+public class CategoryFragment extends ToolbarFragment implements CategorySelectorFragmentHost {
 
     /**
      * Interface to be implemented by an Activity hosting a {@link CategoryFragment}
@@ -114,36 +107,21 @@
     // Currently 2: one for the metadata section and one for the "Select wallpaper" header.
     private static final int NUM_NON_CATEGORY_VIEW_HOLDERS = 0;
 
-    /**
-     * The fixed RecyclerView.Adapter position of the ViewHolder for the initial item in the grid --
-     * usually the wallpaper metadata, or a "permission needed" warning UI.
-     */
-    private static final int INITIAL_HOLDER_ADAPTER_POSITION = 0;
-
     private static final int SETTINGS_APP_INFO_REQUEST_CODE = 1;
 
     private static final String PERMISSION_READ_WALLPAPER_INTERNAL =
             "android.permission.READ_WALLPAPER_INTERNAL";
 
-    private RecyclerView mImageGrid;
-    private CategoryAdapter mAdapter;
-    private ArrayList<Category> mCategories = new ArrayList<>();
-    private Point mTileSizePx;
-    private boolean mAwaitingCategories;
     private ProgressDialog mRefreshWallpaperProgressDialog;
     private boolean mTestingMode;
     private ImageView mHomePreview;
     private ImageView mLockscreenPreview;
     private PreviewPager mPreviewPager;
     private List<View> mWallPaperPreviews;
+    private CategorySelectorFragment mCategorySelectorFragment;
 
     public CategoryFragment() {
-    }
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        mAdapter = new CategoryAdapter(mCategories);
+        mCategorySelectorFragment = new CategorySelectorFragment();
     }
 
     @Override
@@ -171,31 +149,28 @@
         mPreviewPager.setAdapter(new PreviewPagerAdapter(mWallPaperPreviews));
         setupCurrentWallpaperPreview(view);
 
-        mImageGrid = view.findViewById(R.id.category_grid);
-        mImageGrid.addItemDecoration(new GridPaddingDecoration(
-                getResources().getDimensionPixelSize(R.dimen.grid_padding)));
         view.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
             @Override
             public void onLayoutChange(View view, int left, int top, int right, int bottom,
                                        int oldLeft, int oldTop, int oldRight, int oldBottom) {
                 View toolBar = view.findViewById(R.id.toolbar);
-                int gridCollapsedHeight = view.getHeight()
+                View fragmentContainer = view.findViewById(R.id.category_fragment_container);
+                int minimumHeight = view.getHeight()
                         - toolBar.getHeight()
                         - getResources().getDimensionPixelOffset(R.dimen.preview_pager_height);
-                BottomSheetBehavior.from(mImageGrid).setPeekHeight(gridCollapsedHeight);
-                mImageGrid.setMinimumHeight(gridCollapsedHeight);
-                mImageGrid.getLayoutParams().height = view.getHeight() - toolBar.getHeight();
+                BottomSheetBehavior.from(fragmentContainer).setPeekHeight(minimumHeight);
+                fragmentContainer.setMinimumHeight(minimumHeight);
+                fragmentContainer.getLayoutParams().height = view.getHeight() - toolBar.getHeight();
                 view.removeOnLayoutChangeListener(this);
             }
         });
 
-        mTileSizePx = TileSizeCalculator.getCategoryTileSize(getActivity());
-
-        mImageGrid.setAdapter(mAdapter);
-
-        GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), getNumColumns());
-        mImageGrid.setLayoutManager(gridLayoutManager);
         setUpToolbar(view);
+
+        getChildFragmentManager()
+                .beginTransaction()
+                .replace(R.id.category_fragment_container, mCategorySelectorFragment)
+                .commitNow();
         return view;
     }
 
@@ -215,17 +190,9 @@
         // PreviewFragment.
         Glide.get(getActivity()).setMemoryCategory(MemoryCategory.NORMAL);
 
-        // Refresh metadata since it may have changed since the activity was paused.
-        ViewHolder initialViewHolder =
-                mImageGrid.findViewHolderForAdapterPosition(INITIAL_HOLDER_ADAPTER_POSITION);
-        MetadataHolder metadataHolder = null;
-        if (initialViewHolder instanceof MetadataHolder) {
-            metadataHolder = (MetadataHolder) initialViewHolder;
-        }
-
         // The wallpaper may have been set while this fragment was paused, so force refresh the current
         // wallpapers and presentation mode.
-        refreshCurrentWallpapers(metadataHolder, true /* forceRefresh */);
+        refreshCurrentWallpapers(/* MetadataHolder= */ null, /* forceRefresh= */ true);
     }
 
     @Override
@@ -239,72 +206,45 @@
     @Override
     public void onActivityResult(int requestCode, int resultCode, Intent data) {
         if (requestCode == SETTINGS_APP_INFO_REQUEST_CODE) {
-            mAdapter.notifyDataSetChanged();
+            mCategorySelectorFragment.notifyDataSetChanged();
         }
     }
 
+    @Override
+    public void requestCustomPhotoPicker(PermissionChangedListener listener) {
+        getFragmentHost().getMyPhotosStarter().requestCustomPhotoPicker(listener);
+    }
+
+    @Override
+    public void show(String collectionId) {
+        getFragmentHost().show(collectionId);
+    }
+
     /**
      * Inserts the given category into the categories list in priority order.
      */
-    public void addCategory(Category category, boolean loading) {
-        // If not previously waiting for categories, enter the waiting state by showing the loading
-        // indicator.
-        if (loading && !mAwaitingCategories) {
-            mAdapter.notifyItemChanged(getNumColumns());
-            mAdapter.notifyItemInserted(getNumColumns());
-            mAwaitingCategories = true;
-        }
-        // Not add existing category to category list
-        if (mCategories.indexOf(category) >= 0) {
-            updateCategory(category);
-            return;
-        }
-
-        int priority = category.getPriority();
-
-        int index = 0;
-        while (index < mCategories.size() && priority >= mCategories.get(index).getPriority()) {
-            index++;
-        }
-
-        mCategories.add(index, category);
-        if (mAdapter != null) {
-            // Offset the index because of the static metadata element at beginning of RecyclerView.
-            mAdapter.notifyItemInserted(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
-        }
+    void addCategory(Category category, boolean loading) {
+        mCategorySelectorFragment.addCategory(category, loading);
     }
 
-    public void removeCategory(Category category) {
-        int index = mCategories.indexOf(category);
-        if (index >= 0) {
-            mCategories.remove(index);
-            mAdapter.notifyItemRemoved(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
-        }
+    void removeCategory(Category category) {
+        mCategorySelectorFragment.removeCategory(category);
     }
 
-    public void updateCategory(Category category) {
-        int index = mCategories.indexOf(category);
-        if (index >= 0) {
-            mCategories.remove(index);
-            mCategories.add(index, category);
-            mAdapter.notifyItemChanged(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
-        }
+    void updateCategory(Category category) {
+        mCategorySelectorFragment.updateCategory(category);
     }
 
-    public void clearCategories() {
-        mCategories.clear();
-        mAdapter.notifyDataSetChanged();
+    void clearCategories() {
+        mCategorySelectorFragment.clearCategories();
     }
 
     /**
      * Notifies the CategoryFragment that no further categories are expected so it may hide
      * the loading indicator.
      */
-    public void doneFetchingCategories() {
-        if (mAwaitingCategories) {
-            mAdapter.notifyItemRemoved(mAdapter.getItemCount() - 1);
-            mAwaitingCategories = false;
-        }
+    void doneFetchingCategories() {
+        mCategorySelectorFragment.doneFetchingCategories();
     }
 
     /**
@@ -349,7 +289,7 @@
                                 @Override
                                 public void onPermissionsGranted() {
                                     showCurrentWallpaper(rootView, true);
-                                    mAdapter.notifyDataSetChanged();
+                                    mCategorySelectorFragment.notifyDataSetChanged();
                                 }
 
                                 @Override
@@ -457,11 +397,6 @@
         }, forceRefresh);
     }
 
-    private int getNumColumns() {
-        Activity activity = getActivity();
-        return activity == null ? 0 : TileSizeCalculator.getNumCategoryColumns(activity);
-    }
-
     /**
      * Returns the width to use for the home screen wallpaper in the "single metadata" configuration.
      */
@@ -509,14 +444,6 @@
                 if (mRefreshWallpaperProgressDialog != null) {
                     mRefreshWallpaperProgressDialog.dismiss();
                 }
-
-                ViewHolder initialViewHolder =
-                        mImageGrid.findViewHolderForAdapterPosition(INITIAL_HOLDER_ADAPTER_POSITION);
-                if (initialViewHolder instanceof MetadataHolder) {
-                    MetadataHolder metadataHolder = (MetadataHolder) initialViewHolder;
-                    // Update the metadata pane since we know now the UI there is stale.
-                    refreshCurrentWallpapers(metadataHolder, true /* forceRefresh */);
-                }
             }
 
             @Override
@@ -567,29 +494,6 @@
     }
 
     /**
-     * SpanSizeLookup subclass which provides that the item in the first position spans the number of
-     * columns in the RecyclerView and all other items only take up a single span.
-     */
-    private class CategorySpanSizeLookup extends SpanSizeLookup {
-        CategoryAdapter mAdapter;
-
-        public CategorySpanSizeLookup(CategoryAdapter adapter) {
-            mAdapter = adapter;
-        }
-
-        @Override
-        public int getSpanSize(int position) {
-            if (position < NUM_NON_CATEGORY_VIEW_HOLDERS
-                    || mAdapter.getItemViewType(position)
-                    == CategoryAdapter.ITEM_VIEW_TYPE_LOADING_INDICATOR) {
-                return getNumColumns();
-            }
-
-            return 1;
-        }
-    }
-
-    /**
      * ViewHolder subclass for a metadata "card" at the beginning of the RecyclerView.
      */
     private class SingleWallpaperMetadataHolder extends RecyclerView.ViewHolder
@@ -963,256 +867,6 @@
         }
     }
 
-    /**
-     * ViewHolder subclass for a category tile in the RecyclerView.
-     */
-    private class CategoryHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
-        private Category mCategory;
-        private CardView mCategoryView;
-        private ImageView mImageView;
-        private ImageView mOverlayIconView;
-        private TextView mTitleView;
-
-        public CategoryHolder(View itemView) {
-            super(itemView);
-            itemView.setOnClickListener(this);
-
-            mCategoryView = itemView.findViewById(R.id.category);
-            mImageView = itemView.findViewById(R.id.image);
-            mOverlayIconView = itemView.findViewById(R.id.overlay_icon);
-            mTitleView = itemView.findViewById(R.id.category_title);
-
-            mCategoryView.getLayoutParams().height = mTileSizePx.y;
-        }
-
-        @Override
-        public void onClick(View view) {
-            final UserEventLogger eventLogger =
-                    InjectorProvider.getInjector().getUserEventLogger(getActivity());
-            eventLogger.logCategorySelected(mCategory.getCollectionId());
-
-            if (mCategory.supportsCustomPhotos()) {
-                getFragmentHost().getMyPhotosStarter().requestCustomPhotoPicker(
-                        new PermissionChangedListener() {
-                            @Override
-                            public void onPermissionsGranted() {
-                                drawThumbnailAndOverlayIcon();
-                            }
-
-                            @Override
-                            public void onPermissionsDenied(boolean dontAskAgain) {
-                                // No-op
-                            }
-                        });
-                return;
-            }
-
-            getFragmentHost().show(mCategory.getCollectionId());
-        }
-
-        /**
-         * Binds the given category to this CategoryHolder.
-         */
-        public void bindCategory(Category category) {
-            mCategory = category;
-            mTitleView.setText(category.getTitle());
-            drawThumbnailAndOverlayIcon();
-        }
-
-        /**
-         * Draws the CategoryHolder's thumbnail and overlay icon.
-         */
-        public void drawThumbnailAndOverlayIcon() {
-            mOverlayIconView.setImageDrawable(mCategory.getOverlayIcon(
-                    getActivity().getApplicationContext()));
-
-            // Size the overlay icon according to the category.
-            int overlayIconDimenDp = mCategory.getOverlayIconSizeDp();
-            DisplayMetrics metrics = DisplayMetricsRetriever.getInstance().getDisplayMetrics(
-                    getResources(), getActivity().getWindowManager().getDefaultDisplay());
-            int overlayIconDimenPx = (int) (overlayIconDimenDp * metrics.density);
-            mOverlayIconView.getLayoutParams().width = overlayIconDimenPx;
-            mOverlayIconView.getLayoutParams().height = overlayIconDimenPx;
-
-            Asset thumbnail = mCategory.getThumbnail(getActivity().getApplicationContext());
-            if (thumbnail != null) {
-                thumbnail.loadDrawable(getActivity(), mImageView,
-                        getResources().getColor(R.color.secondary_color));
-            } else {
-                // TODO(orenb): Replace this workaround for b/62584914 with a proper way of unloading the
-                // ImageView such that no incorrect image is improperly loaded upon rapid scroll.
-                Object nullObj = null;
-                Glide.with(getActivity())
-                        .asDrawable()
-                        .load(nullObj)
-                        .into(mImageView);
-
-            }
-        }
-    }
-
-    /**
-     * ViewHolder subclass for the loading indicator ("spinner") shown when categories are being
-     * fetched.
-     */
-    private class LoadingIndicatorHolder extends RecyclerView.ViewHolder {
-        public LoadingIndicatorHolder(View view) {
-            super(view);
-            ProgressBar progressBar = view.findViewById(R.id.loading_indicator);
-            progressBar.getIndeterminateDrawable().setColorFilter(
-                    getResources().getColor(R.color.accent_color), Mode.SRC_IN);
-        }
-    }
-
-    /**
-     * ViewHolder subclass for a "card" at the beginning of the RecyclerView showing the app needs the
-     * user to grant the storage permission to show the currently set wallpaper.
-     */
-    private class PermissionNeededHolder extends RecyclerView.ViewHolder {
-        private Button mAllowAccessButton;
-
-        public PermissionNeededHolder(View view) {
-            super(view);
-
-            mAllowAccessButton = view.findViewById(R.id.permission_needed_allow_access_button);
-            mAllowAccessButton.setOnClickListener((View v) -> {
-                getFragmentHost().requestExternalStoragePermission(mAdapter);
-            });
-
-            // Replace explanation text with text containing the Wallpapers app name which replaces the
-            // placeholder.
-            String appName = getString(R.string.app_name);
-            String explanation = getString(R.string.permission_needed_explanation, appName);
-            TextView explanationTextView = view.findViewById(R.id.permission_needed_explanation);
-            explanationTextView.setText(explanation);
-        }
-    }
-
-    /**
-     * RecyclerView Adapter subclass for the category tiles in the RecyclerView.
-     */
-    private class CategoryAdapter extends RecyclerView.Adapter<ViewHolder>
-            implements PermissionChangedListener {
-        private static final int ITEM_VIEW_TYPE_CATEGORY = 3;
-        private static final int ITEM_VIEW_TYPE_LOADING_INDICATOR = 4;
-        private List<Category> mCategories;
-
-        public CategoryAdapter(List<Category> categories) {
-            mCategories = categories;
-        }
-
-        @Override
-        public int getItemViewType(int position) {
-            if (mAwaitingCategories && position == getItemCount() - 1) {
-                return ITEM_VIEW_TYPE_LOADING_INDICATOR;
-            }
-
-            return ITEM_VIEW_TYPE_CATEGORY;
-        }
-
-        @Override
-        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-            LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
-            View view;
-
-            switch (viewType) {
-                case ITEM_VIEW_TYPE_LOADING_INDICATOR:
-                    view = layoutInflater.inflate(
-                            R.layout.grid_item_loading_indicator, parent, /* attachToRoot */ false);
-                    return new LoadingIndicatorHolder(view);
-                case ITEM_VIEW_TYPE_CATEGORY:
-                    view = layoutInflater.inflate(
-                            R.layout.grid_item_category, parent, /* attachToRoot */ false);
-                    return new CategoryHolder(view);
-                default:
-                    Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
-                    return null;
-            }
-        }
-
-        @Override
-        public void onBindViewHolder(ViewHolder holder, int position) {
-            int viewType = getItemViewType(position);
-
-            switch (viewType) {
-                case ITEM_VIEW_TYPE_CATEGORY:
-                    // Offset position to get category index to account for the non-category view holders.
-                    Category category = mCategories.get(position - NUM_NON_CATEGORY_VIEW_HOLDERS);
-                    ((CategoryHolder) holder).bindCategory(category);
-                    break;
-                case ITEM_VIEW_TYPE_LOADING_INDICATOR:
-                    // No op.
-                    break;
-                default:
-                    Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
-            }
-        }
-
-        @Override
-        public int getItemCount() {
-            // Add to size of categories to account for the metadata related views.
-            // Add 1 more for the loading indicator if not yet done loading.
-            int size = mCategories.size() + NUM_NON_CATEGORY_VIEW_HOLDERS;
-            if (mAwaitingCategories) {
-                size += 1;
-            }
-
-            return size;
-        }
-
-        @Override
-        public void onPermissionsGranted() {
-            notifyDataSetChanged();
-        }
-
-        @Override
-        public void onPermissionsDenied(boolean dontAskAgain) {
-            if (!dontAskAgain) {
-                return;
-            }
-
-            String permissionNeededMessage =
-                    getString(R.string.permission_needed_explanation_go_to_settings);
-            AlertDialog dialog = new AlertDialog.Builder(getActivity(), R.style.LightDialogTheme)
-                    .setMessage(permissionNeededMessage)
-                    .setPositiveButton(android.R.string.ok, null /* onClickListener */)
-                    .setNegativeButton(
-                            R.string.settings_button_label,
-                            new DialogInterface.OnClickListener() {
-                                @Override
-                                public void onClick(DialogInterface dialogInterface, int i) {
-                                    Intent appInfoIntent = new Intent();
-                                    appInfoIntent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
-                                    Uri uri = Uri.fromParts(
-                                            "package", getActivity().getPackageName(), null /* fragment */);
-                                    appInfoIntent.setData(uri);
-                                    startActivityForResult(appInfoIntent, SETTINGS_APP_INFO_REQUEST_CODE);
-                                }
-                            })
-                    .create();
-            dialog.show();
-        }
-    }
-
-    private class GridPaddingDecoration extends RecyclerView.ItemDecoration {
-
-        private int mPadding;
-
-        GridPaddingDecoration(int padding) {
-            mPadding = padding;
-        }
-
-        @Override
-        public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
-                                   RecyclerView.State state) {
-            int position = parent.getChildAdapterPosition(view) - NUM_NON_CATEGORY_VIEW_HOLDERS;
-            if (position >= 0) {
-                outRect.left = mPadding;
-                outRect.right = mPadding;
-            }
-        }
-    }
-
     private class PreviewPagerAdapter extends PagerAdapter {
 
         private List<View> mPages;
diff --git a/src/com/android/wallpaper/picker/CategorySelectorFragment.java b/src/com/android/wallpaper/picker/CategorySelectorFragment.java
new file mode 100644
index 0000000..4094cda
--- /dev/null
+++ b/src/com/android/wallpaper/picker/CategorySelectorFragment.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wallpaper.picker;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.cardview.widget.CardView;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.wallpaper.R;
+import com.android.wallpaper.asset.Asset;
+import com.android.wallpaper.model.Category;
+import com.android.wallpaper.module.InjectorProvider;
+import com.android.wallpaper.module.UserEventLogger;
+import com.android.wallpaper.util.DisplayMetricsRetriever;
+import com.android.wallpaper.util.TileSizeCalculator;
+
+import com.bumptech.glide.Glide;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Displays the UI which contains the categories of the wallpaper.
+ */
+public class CategorySelectorFragment extends Fragment {
+
+    // The number of ViewHolders that don't pertain to category tiles.
+    // Currently 2: one for the metadata section and one for the "Select wallpaper" header.
+    private static final int NUM_NON_CATEGORY_VIEW_HOLDERS = 0;
+    private static final int SETTINGS_APP_INFO_REQUEST_CODE = 1;
+    private static final String TAG = "CategorySelectorFragment";
+
+    /**
+     * Interface to be implemented by an Fragment hosting a {@link CategorySelectorFragment}
+     */
+    public interface CategorySelectorFragmentHost {
+
+        /**
+         * Requests to show the Android custom photo picker for the sake of picking a photo
+         * to set as the device's wallpaper.
+         */
+        void requestCustomPhotoPicker(MyPhotosStarter.PermissionChangedListener listener);
+
+        /**
+         * Shows the wallpaper page of the specific category.
+         *
+         * @param collectionId the id of the category
+         */
+        void show(String collectionId);
+    }
+
+    private RecyclerView mImageGrid;
+    private CategoryAdapter mAdapter;
+    private ArrayList<Category> mCategories = new ArrayList<>();
+    private Point mTileSizePx;
+    private boolean mAwaitingCategories;
+
+    public CategorySelectorFragment() {
+        mAdapter = new CategoryAdapter(mCategories);
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+                             @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.fragment_category_selector, container,
+                /* attachToRoot= */ false);
+
+        mImageGrid = view.findViewById(R.id.category_grid);
+        mImageGrid.addItemDecoration(new GridPaddingDecoration(
+                getResources().getDimensionPixelSize(R.dimen.grid_padding)));
+
+        mTileSizePx = TileSizeCalculator.getCategoryTileSize(getActivity());
+
+        mImageGrid.setAdapter(mAdapter);
+
+        GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), getNumColumns());
+        mImageGrid.setLayoutManager(gridLayoutManager);
+
+        return view;
+    }
+
+    /**
+     * Inserts the given category into the categories list in priority order.
+     */
+    void addCategory(Category category, boolean loading) {
+        // If not previously waiting for categories, enter the waiting state by showing the loading
+        // indicator.
+        if (loading && !mAwaitingCategories) {
+            mAdapter.notifyItemChanged(getNumColumns());
+            mAdapter.notifyItemInserted(getNumColumns());
+            mAwaitingCategories = true;
+        }
+        // Not add existing category to category list
+        if (mCategories.indexOf(category) >= 0) {
+            updateCategory(category);
+            return;
+        }
+
+        int priority = category.getPriority();
+
+        int index = 0;
+        while (index < mCategories.size() && priority >= mCategories.get(index).getPriority()) {
+            index++;
+        }
+
+        mCategories.add(index, category);
+        if (mAdapter != null) {
+            // Offset the index because of the static metadata element at beginning of RecyclerView.
+            mAdapter.notifyItemInserted(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
+        }
+    }
+
+    void removeCategory(Category category) {
+        int index = mCategories.indexOf(category);
+        if (index >= 0) {
+            mCategories.remove(index);
+            mAdapter.notifyItemRemoved(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
+        }
+    }
+
+    void updateCategory(Category category) {
+        int index = mCategories.indexOf(category);
+        if (index >= 0) {
+            mCategories.remove(index);
+            mCategories.add(index, category);
+            mAdapter.notifyItemChanged(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
+        }
+    }
+
+    void clearCategories() {
+        mCategories.clear();
+        mAdapter.notifyDataSetChanged();
+    }
+
+    /**
+     * Notifies the CategoryFragment that no further categories are expected so it may hide
+     * the loading indicator.
+     */
+    void doneFetchingCategories() {
+        if (mAwaitingCategories) {
+            mAdapter.notifyItemRemoved(mAdapter.getItemCount() - 1);
+            mAwaitingCategories = false;
+        }
+    }
+
+    void notifyDataSetChanged() {
+        mAdapter.notifyDataSetChanged();
+    }
+
+    private int getNumColumns() {
+        Activity activity = getActivity();
+        return activity == null ? 0 : TileSizeCalculator.getNumCategoryColumns(activity);
+    }
+
+
+    private CategorySelectorFragmentHost getCategorySelectorFragmentHost() {
+        return (CategorySelectorFragmentHost) getParentFragment();
+    }
+
+    /**
+     * ViewHolder subclass for a category tile in the RecyclerView.
+     */
+    private class CategoryHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
+        private Category mCategory;
+        private ImageView mImageView;
+        private ImageView mOverlayIconView;
+        private TextView mTitleView;
+
+        CategoryHolder(View itemView) {
+            super(itemView);
+            itemView.setOnClickListener(this);
+
+            mImageView = itemView.findViewById(R.id.image);
+            mOverlayIconView = itemView.findViewById(R.id.overlay_icon);
+            mTitleView = itemView.findViewById(R.id.category_title);
+
+            CardView categoryView = itemView.findViewById(R.id.category);
+            categoryView.getLayoutParams().height = mTileSizePx.y;
+        }
+
+        @Override
+        public void onClick(View view) {
+            final UserEventLogger eventLogger =
+                    InjectorProvider.getInjector().getUserEventLogger(getActivity());
+            eventLogger.logCategorySelected(mCategory.getCollectionId());
+
+            if (mCategory.supportsCustomPhotos()) {
+                getCategorySelectorFragmentHost().requestCustomPhotoPicker(
+                        new MyPhotosStarter.PermissionChangedListener() {
+                            @Override
+                            public void onPermissionsGranted() {
+                                drawThumbnailAndOverlayIcon();
+                            }
+
+                            @Override
+                            public void onPermissionsDenied(boolean dontAskAgain) {
+                                // No-op
+                            }
+                        });
+                return;
+            }
+
+            getCategorySelectorFragmentHost().show(mCategory.getCollectionId());
+        }
+
+        /**
+         * Binds the given category to this CategoryHolder.
+         */
+        private void bindCategory(Category category) {
+            mCategory = category;
+            mTitleView.setText(category.getTitle());
+            drawThumbnailAndOverlayIcon();
+        }
+
+        /**
+         * Draws the CategoryHolder's thumbnail and overlay icon.
+         */
+        private void drawThumbnailAndOverlayIcon() {
+            mOverlayIconView.setImageDrawable(mCategory.getOverlayIcon(
+                    getActivity().getApplicationContext()));
+
+            // Size the overlay icon according to the category.
+            int overlayIconDimenDp = mCategory.getOverlayIconSizeDp();
+            DisplayMetrics metrics = DisplayMetricsRetriever.getInstance().getDisplayMetrics(
+                    getResources(), getActivity().getWindowManager().getDefaultDisplay());
+            int overlayIconDimenPx = (int) (overlayIconDimenDp * metrics.density);
+            mOverlayIconView.getLayoutParams().width = overlayIconDimenPx;
+            mOverlayIconView.getLayoutParams().height = overlayIconDimenPx;
+
+            Asset thumbnail = mCategory.getThumbnail(getActivity().getApplicationContext());
+            if (thumbnail != null) {
+                thumbnail.loadDrawable(getActivity(), mImageView,
+                        getResources().getColor(R.color.secondary_color));
+            } else {
+                // TODO(orenb): Replace this workaround for b/62584914 with a proper way of
+                //  unloading the ImageView such that no incorrect image is improperly loaded upon
+                //  rapid scroll.
+                Object nullObj = null;
+                Glide.with(getActivity())
+                        .asDrawable()
+                        .load(nullObj)
+                        .into(mImageView);
+
+            }
+        }
+    }
+
+    /**
+     * ViewHolder subclass for the loading indicator ("spinner") shown when categories are being
+     * fetched.
+     */
+    private class LoadingIndicatorHolder extends RecyclerView.ViewHolder {
+        private LoadingIndicatorHolder(View view) {
+            super(view);
+            ProgressBar progressBar = view.findViewById(R.id.loading_indicator);
+            progressBar.getIndeterminateDrawable().setColorFilter(
+                    getResources().getColor(R.color.accent_color), PorterDuff.Mode.SRC_IN);
+        }
+    }
+
+    /**
+     * RecyclerView Adapter subclass for the category tiles in the RecyclerView.
+     */
+    private class CategoryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
+            implements MyPhotosStarter.PermissionChangedListener {
+        private static final int ITEM_VIEW_TYPE_CATEGORY = 3;
+        private static final int ITEM_VIEW_TYPE_LOADING_INDICATOR = 4;
+        private List<Category> mCategories;
+
+        private CategoryAdapter(List<Category> categories) {
+            mCategories = categories;
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            if (mAwaitingCategories && position == getItemCount() - 1) {
+                return ITEM_VIEW_TYPE_LOADING_INDICATOR;
+            }
+
+            return ITEM_VIEW_TYPE_CATEGORY;
+        }
+
+        @Override
+        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+            LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
+            View view;
+
+            switch (viewType) {
+                case ITEM_VIEW_TYPE_LOADING_INDICATOR:
+                    view = layoutInflater.inflate(R.layout.grid_item_loading_indicator,
+                            parent, /* attachToRoot= */ false);
+                    return new LoadingIndicatorHolder(view);
+                case ITEM_VIEW_TYPE_CATEGORY:
+                    view = layoutInflater.inflate(R.layout.grid_item_category,
+                            parent, /* attachToRoot= */ false);
+                    return new CategoryHolder(view);
+                default:
+                    Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
+                    return null;
+            }
+        }
+
+        @Override
+        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+            int viewType = getItemViewType(position);
+
+            switch (viewType) {
+                case ITEM_VIEW_TYPE_CATEGORY:
+                    // Offset position to get category index to account for the non-category view
+                    // holders.
+                    Category category = mCategories.get(position - NUM_NON_CATEGORY_VIEW_HOLDERS);
+                    ((CategoryHolder) holder).bindCategory(category);
+                    break;
+                case ITEM_VIEW_TYPE_LOADING_INDICATOR:
+                    // No op.
+                    break;
+                default:
+                    Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            // Add to size of categories to account for the metadata related views.
+            // Add 1 more for the loading indicator if not yet done loading.
+            int size = mCategories.size() + NUM_NON_CATEGORY_VIEW_HOLDERS;
+            if (mAwaitingCategories) {
+                size += 1;
+            }
+
+            return size;
+        }
+
+        @Override
+        public void onPermissionsGranted() {
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public void onPermissionsDenied(boolean dontAskAgain) {
+            if (!dontAskAgain) {
+                return;
+            }
+
+            String permissionNeededMessage =
+                    getString(R.string.permission_needed_explanation_go_to_settings);
+            AlertDialog dialog = new AlertDialog.Builder(getActivity(), R.style.LightDialogTheme)
+                    .setMessage(permissionNeededMessage)
+                    .setPositiveButton(android.R.string.ok, null /* onClickListener */)
+                    .setNegativeButton(
+                            R.string.settings_button_label,
+                            (dialogInterface, i) -> {
+                                Intent appInfoIntent =
+                                        new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+                                Uri uri = Uri.fromParts("package",
+                                        getActivity().getPackageName(), /* fragment= */ null);
+                                appInfoIntent.setData(uri);
+                                startActivityForResult(
+                                        appInfoIntent, SETTINGS_APP_INFO_REQUEST_CODE);
+                            })
+                    .create();
+            dialog.show();
+        }
+    }
+
+    private class GridPaddingDecoration extends RecyclerView.ItemDecoration {
+
+        private int mPadding;
+
+        GridPaddingDecoration(int padding) {
+            mPadding = padding;
+        }
+
+        @Override
+        public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+                                   RecyclerView.State state) {
+            int position = parent.getChildAdapterPosition(view) - NUM_NON_CATEGORY_VIEW_HOLDERS;
+            if (position >= 0) {
+                outRect.left = mPadding;
+                outRect.right = mPadding;
+            }
+        }
+    }
+
+    /**
+     * SpanSizeLookup subclass which provides that the item in the first position spans the number
+     * of columns in the RecyclerView and all other items only take up a single span.
+     */
+    private class CategorySpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
+        CategoryAdapter mAdapter;
+
+        private CategorySpanSizeLookup(CategoryAdapter adapter) {
+            mAdapter = adapter;
+        }
+
+        @Override
+        public int getSpanSize(int position) {
+            if (position < NUM_NON_CATEGORY_VIEW_HOLDERS
+                    || mAdapter.getItemViewType(position)
+                    == CategoryAdapter.ITEM_VIEW_TYPE_LOADING_INDICATOR) {
+                return getNumColumns();
+            }
+
+            return 1;
+        }
+    }
+}