blob: 733aeb1683ef5a4a450008e3ed93c4f52f1481cb [file] [log] [blame]
Chuck Liao1e0501f2020-02-17 18:20:54 +08001/*
2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.wallpaper.picker;
17
18import android.app.Activity;
19import android.content.Intent;
20import android.graphics.Point;
21import android.graphics.PorterDuff;
22import android.graphics.Rect;
23import android.net.Uri;
24import android.os.Bundle;
25import android.provider.Settings;
26import android.util.DisplayMetrics;
27import android.util.Log;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.ImageView;
32import android.widget.ProgressBar;
33import android.widget.TextView;
34
35import androidx.annotation.NonNull;
36import androidx.annotation.Nullable;
37import androidx.appcompat.app.AlertDialog;
38import androidx.cardview.widget.CardView;
39import androidx.fragment.app.Fragment;
40import androidx.recyclerview.widget.GridLayoutManager;
41import androidx.recyclerview.widget.RecyclerView;
42
43import com.android.wallpaper.R;
44import com.android.wallpaper.asset.Asset;
45import com.android.wallpaper.model.Category;
46import com.android.wallpaper.module.InjectorProvider;
47import com.android.wallpaper.module.UserEventLogger;
48import com.android.wallpaper.util.DisplayMetricsRetriever;
Santiago Etchebehere53c63432020-05-07 18:55:35 -070049import com.android.wallpaper.util.SizeCalculator;
Chuck Liao1e0501f2020-02-17 18:20:54 +080050
51import com.bumptech.glide.Glide;
52
53import java.util.ArrayList;
54import java.util.List;
55
56/**
57 * Displays the UI which contains the categories of the wallpaper.
58 */
59public class CategorySelectorFragment extends Fragment {
60
61 // The number of ViewHolders that don't pertain to category tiles.
62 // Currently 2: one for the metadata section and one for the "Select wallpaper" header.
63 private static final int NUM_NON_CATEGORY_VIEW_HOLDERS = 0;
64 private static final int SETTINGS_APP_INFO_REQUEST_CODE = 1;
65 private static final String TAG = "CategorySelectorFragment";
66
67 /**
68 * Interface to be implemented by an Fragment hosting a {@link CategorySelectorFragment}
69 */
70 public interface CategorySelectorFragmentHost {
71
72 /**
73 * Requests to show the Android custom photo picker for the sake of picking a photo
74 * to set as the device's wallpaper.
75 */
76 void requestCustomPhotoPicker(MyPhotosStarter.PermissionChangedListener listener);
77
78 /**
79 * Shows the wallpaper page of the specific category.
80 *
81 * @param collectionId the id of the category
82 */
83 void show(String collectionId);
84 }
85
86 private RecyclerView mImageGrid;
87 private CategoryAdapter mAdapter;
88 private ArrayList<Category> mCategories = new ArrayList<>();
89 private Point mTileSizePx;
90 private boolean mAwaitingCategories;
91
92 public CategorySelectorFragment() {
93 mAdapter = new CategoryAdapter(mCategories);
94 }
95
96 @Nullable
97 @Override
98 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
99 @Nullable Bundle savedInstanceState) {
100 View view = inflater.inflate(R.layout.fragment_category_selector, container,
101 /* attachToRoot= */ false);
102
103 mImageGrid = view.findViewById(R.id.category_grid);
104 mImageGrid.addItemDecoration(new GridPaddingDecoration(
105 getResources().getDimensionPixelSize(R.dimen.grid_padding)));
106
Santiago Etchebehere53c63432020-05-07 18:55:35 -0700107 mTileSizePx = SizeCalculator.getCategoryTileSize(getActivity());
Chuck Liao1e0501f2020-02-17 18:20:54 +0800108
109 mImageGrid.setAdapter(mAdapter);
110
111 GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), getNumColumns());
112 mImageGrid.setLayoutManager(gridLayoutManager);
113
114 return view;
115 }
116
117 /**
118 * Inserts the given category into the categories list in priority order.
119 */
120 void addCategory(Category category, boolean loading) {
121 // If not previously waiting for categories, enter the waiting state by showing the loading
122 // indicator.
123 if (loading && !mAwaitingCategories) {
124 mAdapter.notifyItemChanged(getNumColumns());
125 mAdapter.notifyItemInserted(getNumColumns());
126 mAwaitingCategories = true;
127 }
128 // Not add existing category to category list
129 if (mCategories.indexOf(category) >= 0) {
130 updateCategory(category);
131 return;
132 }
133
134 int priority = category.getPriority();
135
136 int index = 0;
137 while (index < mCategories.size() && priority >= mCategories.get(index).getPriority()) {
138 index++;
139 }
140
141 mCategories.add(index, category);
142 if (mAdapter != null) {
143 // Offset the index because of the static metadata element at beginning of RecyclerView.
144 mAdapter.notifyItemInserted(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
145 }
146 }
147
148 void removeCategory(Category category) {
149 int index = mCategories.indexOf(category);
150 if (index >= 0) {
151 mCategories.remove(index);
152 mAdapter.notifyItemRemoved(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
153 }
154 }
155
156 void updateCategory(Category category) {
157 int index = mCategories.indexOf(category);
158 if (index >= 0) {
159 mCategories.remove(index);
160 mCategories.add(index, category);
161 mAdapter.notifyItemChanged(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
162 }
163 }
164
165 void clearCategories() {
166 mCategories.clear();
167 mAdapter.notifyDataSetChanged();
168 }
169
170 /**
171 * Notifies the CategoryFragment that no further categories are expected so it may hide
172 * the loading indicator.
173 */
174 void doneFetchingCategories() {
175 if (mAwaitingCategories) {
176 mAdapter.notifyItemRemoved(mAdapter.getItemCount() - 1);
177 mAwaitingCategories = false;
178 }
179 }
180
181 void notifyDataSetChanged() {
182 mAdapter.notifyDataSetChanged();
183 }
184
185 private int getNumColumns() {
186 Activity activity = getActivity();
Santiago Etchebehere53c63432020-05-07 18:55:35 -0700187 return activity == null ? 0 : SizeCalculator.getNumCategoryColumns(activity);
Chuck Liao1e0501f2020-02-17 18:20:54 +0800188 }
189
190
191 private CategorySelectorFragmentHost getCategorySelectorFragmentHost() {
192 return (CategorySelectorFragmentHost) getParentFragment();
193 }
194
195 /**
196 * ViewHolder subclass for a category tile in the RecyclerView.
197 */
198 private class CategoryHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
199 private Category mCategory;
200 private ImageView mImageView;
201 private ImageView mOverlayIconView;
202 private TextView mTitleView;
203
204 CategoryHolder(View itemView) {
205 super(itemView);
206 itemView.setOnClickListener(this);
207
208 mImageView = itemView.findViewById(R.id.image);
209 mOverlayIconView = itemView.findViewById(R.id.overlay_icon);
210 mTitleView = itemView.findViewById(R.id.category_title);
211
212 CardView categoryView = itemView.findViewById(R.id.category);
213 categoryView.getLayoutParams().height = mTileSizePx.y;
214 }
215
216 @Override
217 public void onClick(View view) {
218 final UserEventLogger eventLogger =
219 InjectorProvider.getInjector().getUserEventLogger(getActivity());
220 eventLogger.logCategorySelected(mCategory.getCollectionId());
221
222 if (mCategory.supportsCustomPhotos()) {
223 getCategorySelectorFragmentHost().requestCustomPhotoPicker(
224 new MyPhotosStarter.PermissionChangedListener() {
225 @Override
226 public void onPermissionsGranted() {
227 drawThumbnailAndOverlayIcon();
228 }
229
230 @Override
231 public void onPermissionsDenied(boolean dontAskAgain) {
232 // No-op
233 }
234 });
235 return;
236 }
237
238 getCategorySelectorFragmentHost().show(mCategory.getCollectionId());
239 }
240
241 /**
242 * Binds the given category to this CategoryHolder.
243 */
244 private void bindCategory(Category category) {
245 mCategory = category;
246 mTitleView.setText(category.getTitle());
247 drawThumbnailAndOverlayIcon();
248 }
249
250 /**
251 * Draws the CategoryHolder's thumbnail and overlay icon.
252 */
253 private void drawThumbnailAndOverlayIcon() {
254 mOverlayIconView.setImageDrawable(mCategory.getOverlayIcon(
255 getActivity().getApplicationContext()));
256
257 // Size the overlay icon according to the category.
258 int overlayIconDimenDp = mCategory.getOverlayIconSizeDp();
259 DisplayMetrics metrics = DisplayMetricsRetriever.getInstance().getDisplayMetrics(
260 getResources(), getActivity().getWindowManager().getDefaultDisplay());
261 int overlayIconDimenPx = (int) (overlayIconDimenDp * metrics.density);
262 mOverlayIconView.getLayoutParams().width = overlayIconDimenPx;
263 mOverlayIconView.getLayoutParams().height = overlayIconDimenPx;
264
265 Asset thumbnail = mCategory.getThumbnail(getActivity().getApplicationContext());
266 if (thumbnail != null) {
267 thumbnail.loadDrawable(getActivity(), mImageView,
268 getResources().getColor(R.color.secondary_color));
269 } else {
270 // TODO(orenb): Replace this workaround for b/62584914 with a proper way of
271 // unloading the ImageView such that no incorrect image is improperly loaded upon
272 // rapid scroll.
273 Object nullObj = null;
274 Glide.with(getActivity())
275 .asDrawable()
276 .load(nullObj)
277 .into(mImageView);
278
279 }
280 }
281 }
282
283 /**
284 * ViewHolder subclass for the loading indicator ("spinner") shown when categories are being
285 * fetched.
286 */
287 private class LoadingIndicatorHolder extends RecyclerView.ViewHolder {
288 private LoadingIndicatorHolder(View view) {
289 super(view);
290 ProgressBar progressBar = view.findViewById(R.id.loading_indicator);
291 progressBar.getIndeterminateDrawable().setColorFilter(
292 getResources().getColor(R.color.accent_color), PorterDuff.Mode.SRC_IN);
293 }
294 }
295
296 /**
297 * RecyclerView Adapter subclass for the category tiles in the RecyclerView.
298 */
299 private class CategoryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
300 implements MyPhotosStarter.PermissionChangedListener {
301 private static final int ITEM_VIEW_TYPE_CATEGORY = 3;
302 private static final int ITEM_VIEW_TYPE_LOADING_INDICATOR = 4;
303 private List<Category> mCategories;
304
305 private CategoryAdapter(List<Category> categories) {
306 mCategories = categories;
307 }
308
309 @Override
310 public int getItemViewType(int position) {
311 if (mAwaitingCategories && position == getItemCount() - 1) {
312 return ITEM_VIEW_TYPE_LOADING_INDICATOR;
313 }
314
315 return ITEM_VIEW_TYPE_CATEGORY;
316 }
317
318 @Override
319 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
320 LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
321 View view;
322
323 switch (viewType) {
324 case ITEM_VIEW_TYPE_LOADING_INDICATOR:
325 view = layoutInflater.inflate(R.layout.grid_item_loading_indicator,
326 parent, /* attachToRoot= */ false);
327 return new LoadingIndicatorHolder(view);
328 case ITEM_VIEW_TYPE_CATEGORY:
329 view = layoutInflater.inflate(R.layout.grid_item_category,
330 parent, /* attachToRoot= */ false);
331 return new CategoryHolder(view);
332 default:
333 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
334 return null;
335 }
336 }
337
338 @Override
339 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
340 int viewType = getItemViewType(position);
341
342 switch (viewType) {
343 case ITEM_VIEW_TYPE_CATEGORY:
344 // Offset position to get category index to account for the non-category view
345 // holders.
346 Category category = mCategories.get(position - NUM_NON_CATEGORY_VIEW_HOLDERS);
347 ((CategoryHolder) holder).bindCategory(category);
348 break;
349 case ITEM_VIEW_TYPE_LOADING_INDICATOR:
350 // No op.
351 break;
352 default:
353 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
354 }
355 }
356
357 @Override
358 public int getItemCount() {
359 // Add to size of categories to account for the metadata related views.
360 // Add 1 more for the loading indicator if not yet done loading.
361 int size = mCategories.size() + NUM_NON_CATEGORY_VIEW_HOLDERS;
362 if (mAwaitingCategories) {
363 size += 1;
364 }
365
366 return size;
367 }
368
369 @Override
370 public void onPermissionsGranted() {
371 notifyDataSetChanged();
372 }
373
374 @Override
375 public void onPermissionsDenied(boolean dontAskAgain) {
376 if (!dontAskAgain) {
377 return;
378 }
379
380 String permissionNeededMessage =
381 getString(R.string.permission_needed_explanation_go_to_settings);
382 AlertDialog dialog = new AlertDialog.Builder(getActivity(), R.style.LightDialogTheme)
383 .setMessage(permissionNeededMessage)
384 .setPositiveButton(android.R.string.ok, null /* onClickListener */)
385 .setNegativeButton(
386 R.string.settings_button_label,
387 (dialogInterface, i) -> {
388 Intent appInfoIntent =
389 new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
390 Uri uri = Uri.fromParts("package",
391 getActivity().getPackageName(), /* fragment= */ null);
392 appInfoIntent.setData(uri);
393 startActivityForResult(
394 appInfoIntent, SETTINGS_APP_INFO_REQUEST_CODE);
395 })
396 .create();
397 dialog.show();
398 }
399 }
400
401 private class GridPaddingDecoration extends RecyclerView.ItemDecoration {
402
403 private int mPadding;
404
405 GridPaddingDecoration(int padding) {
406 mPadding = padding;
407 }
408
409 @Override
410 public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
411 RecyclerView.State state) {
412 int position = parent.getChildAdapterPosition(view) - NUM_NON_CATEGORY_VIEW_HOLDERS;
413 if (position >= 0) {
414 outRect.left = mPadding;
415 outRect.right = mPadding;
416 }
417 }
418 }
419
420 /**
421 * SpanSizeLookup subclass which provides that the item in the first position spans the number
422 * of columns in the RecyclerView and all other items only take up a single span.
423 */
424 private class CategorySpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
425 CategoryAdapter mAdapter;
426
427 private CategorySpanSizeLookup(CategoryAdapter adapter) {
428 mAdapter = adapter;
429 }
430
431 @Override
432 public int getSpanSize(int position) {
433 if (position < NUM_NON_CATEGORY_VIEW_HOLDERS
434 || mAdapter.getItemViewType(position)
435 == CategoryAdapter.ITEM_VIEW_TYPE_LOADING_INDICATOR) {
436 return getNumColumns();
437 }
438
439 return 1;
440 }
441 }
442}