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;
+ }
+ }
+}