blob: 0e42132a9933bd7f54ab21985ccd94923750fc2c [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;
Chuck Liaof646e052020-09-24 12:24:59 +080019import android.app.AlertDialog;
Chuck Liao1e0501f2020-02-17 18:20:54 +080020import android.content.Intent;
21import android.graphics.Point;
22import android.graphics.PorterDuff;
23import android.graphics.Rect;
24import android.net.Uri;
25import android.os.Bundle;
26import android.provider.Settings;
27import android.util.DisplayMetrics;
28import android.util.Log;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.widget.ImageView;
33import android.widget.ProgressBar;
34import android.widget.TextView;
35
36import androidx.annotation.NonNull;
37import androidx.annotation.Nullable;
Chuck Liao1e0501f2020-02-17 18:20:54 +080038import 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;
Chuck Liaof6b4b192020-08-07 02:31:32 +080048import com.android.wallpaper.util.DeepLinkUtils;
Chuck Liao1e0501f2020-02-17 18:20:54 +080049import com.android.wallpaper.util.DisplayMetricsRetriever;
Santiago Etchebehere53c63432020-05-07 18:55:35 -070050import com.android.wallpaper.util.SizeCalculator;
Wesley.CW Wangdc68fde2020-06-15 19:12:33 +080051import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate;
52import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate.BottomSheetHost;
Chuck Liao1e0501f2020-02-17 18:20:54 +080053
54import com.bumptech.glide.Glide;
55
56import java.util.ArrayList;
57import java.util.List;
58
59/**
60 * Displays the UI which contains the categories of the wallpaper.
61 */
62public class CategorySelectorFragment extends Fragment {
63
64 // The number of ViewHolders that don't pertain to category tiles.
65 // Currently 2: one for the metadata section and one for the "Select wallpaper" header.
66 private static final int NUM_NON_CATEGORY_VIEW_HOLDERS = 0;
67 private static final int SETTINGS_APP_INFO_REQUEST_CODE = 1;
68 private static final String TAG = "CategorySelectorFragment";
69
70 /**
71 * Interface to be implemented by an Fragment hosting a {@link CategorySelectorFragment}
72 */
73 public interface CategorySelectorFragmentHost {
74
75 /**
76 * Requests to show the Android custom photo picker for the sake of picking a photo
77 * to set as the device's wallpaper.
78 */
79 void requestCustomPhotoPicker(MyPhotosStarter.PermissionChangedListener listener);
80
81 /**
82 * Shows the wallpaper page of the specific category.
83 *
Chuck Liaoa63f1bf2020-06-11 01:35:42 +080084 * @param category the wallpaper's {@link Category}
Chuck Liao1e0501f2020-02-17 18:20:54 +080085 */
Chuck Liaoa63f1bf2020-06-11 01:35:42 +080086 void show(Category category);
Chuck Liao0088bca2020-05-28 16:03:23 +080087
88 /**
89 * Sets the title in the toolbar.
90 */
91 void setToolbarTitle(CharSequence title);
Chuck Liaof6b4b192020-08-07 02:31:32 +080092
93 /**
94 * Fetches the wallpaper categories.
95 */
96 void fetchCategories();
Chuck Liao81145b22020-09-03 09:52:25 +080097
98 /**
Chuck Liao3abf15b2020-12-17 22:33:02 +080099 * Cleans up the listeners which will be notified when there's a package event.
100 */
101 void cleanUp();
102
103 /**
Chuck Liao81145b22020-09-03 09:52:25 +0800104 * Hides the {@link com.android.wallpaper.widget.BottomActionBar}.
105 */
106 void hideBottomActionBar();
Chuck Liao1e0501f2020-02-17 18:20:54 +0800107 }
108
109 private RecyclerView mImageGrid;
110 private CategoryAdapter mAdapter;
111 private ArrayList<Category> mCategories = new ArrayList<>();
112 private Point mTileSizePx;
113 private boolean mAwaitingCategories;
114
115 public CategorySelectorFragment() {
116 mAdapter = new CategoryAdapter(mCategories);
117 }
118
119 @Nullable
120 @Override
121 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
122 @Nullable Bundle savedInstanceState) {
123 View view = inflater.inflate(R.layout.fragment_category_selector, container,
124 /* attachToRoot= */ false);
Chuck Liao1e0501f2020-02-17 18:20:54 +0800125 mImageGrid = view.findViewById(R.id.category_grid);
126 mImageGrid.addItemDecoration(new GridPaddingDecoration(
127 getResources().getDimensionPixelSize(R.dimen.grid_padding)));
128
Santiago Etchebehere53c63432020-05-07 18:55:35 -0700129 mTileSizePx = SizeCalculator.getCategoryTileSize(getActivity());
Chuck Liao1e0501f2020-02-17 18:20:54 +0800130
131 mImageGrid.setAdapter(mAdapter);
132
133 GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), getNumColumns());
134 mImageGrid.setLayoutManager(gridLayoutManager);
Wesley.CW Wang27ff4262020-06-12 19:19:57 +0800135 mImageGrid.setAccessibilityDelegateCompat(
Wesley.CW Wangdc68fde2020-06-15 19:12:33 +0800136 new WallpaperPickerRecyclerViewAccessibilityDelegate(
137 mImageGrid, (BottomSheetHost) getParentFragment(), getNumColumns()));
Chuck Liao0088bca2020-05-28 16:03:23 +0800138 getCategorySelectorFragmentHost().setToolbarTitle(getText(R.string.wallpaper_title));
Chuck Liao1e0501f2020-02-17 18:20:54 +0800139
Chuck Liaof6b4b192020-08-07 02:31:32 +0800140 if (!DeepLinkUtils.isDeepLink(getActivity().getIntent())) {
141 getCategorySelectorFragmentHost().fetchCategories();
142 }
143
Chuck Liao81145b22020-09-03 09:52:25 +0800144 getCategorySelectorFragmentHost().hideBottomActionBar();
145
Chuck Liao1e0501f2020-02-17 18:20:54 +0800146 return view;
147 }
148
Chuck Liao3abf15b2020-12-17 22:33:02 +0800149 @Override
150 public void onDestroyView() {
151 getCategorySelectorFragmentHost().cleanUp();
152 super.onDestroyView();
153 }
154
Chuck Liao1e0501f2020-02-17 18:20:54 +0800155 /**
156 * Inserts the given category into the categories list in priority order.
157 */
158 void addCategory(Category category, boolean loading) {
159 // If not previously waiting for categories, enter the waiting state by showing the loading
160 // indicator.
161 if (loading && !mAwaitingCategories) {
162 mAdapter.notifyItemChanged(getNumColumns());
163 mAdapter.notifyItemInserted(getNumColumns());
164 mAwaitingCategories = true;
165 }
166 // Not add existing category to category list
167 if (mCategories.indexOf(category) >= 0) {
168 updateCategory(category);
169 return;
170 }
171
172 int priority = category.getPriority();
173
174 int index = 0;
175 while (index < mCategories.size() && priority >= mCategories.get(index).getPriority()) {
176 index++;
177 }
178
179 mCategories.add(index, category);
180 if (mAdapter != null) {
181 // Offset the index because of the static metadata element at beginning of RecyclerView.
182 mAdapter.notifyItemInserted(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
183 }
184 }
185
186 void removeCategory(Category category) {
187 int index = mCategories.indexOf(category);
188 if (index >= 0) {
189 mCategories.remove(index);
190 mAdapter.notifyItemRemoved(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
191 }
192 }
193
194 void updateCategory(Category category) {
195 int index = mCategories.indexOf(category);
196 if (index >= 0) {
197 mCategories.remove(index);
198 mCategories.add(index, category);
199 mAdapter.notifyItemChanged(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
200 }
201 }
202
203 void clearCategories() {
204 mCategories.clear();
205 mAdapter.notifyDataSetChanged();
206 }
207
208 /**
209 * Notifies the CategoryFragment that no further categories are expected so it may hide
210 * the loading indicator.
211 */
212 void doneFetchingCategories() {
213 if (mAwaitingCategories) {
214 mAdapter.notifyItemRemoved(mAdapter.getItemCount() - 1);
215 mAwaitingCategories = false;
216 }
217 }
218
219 void notifyDataSetChanged() {
220 mAdapter.notifyDataSetChanged();
221 }
222
223 private int getNumColumns() {
224 Activity activity = getActivity();
Chuck Liaoe2fe0302020-06-29 21:15:35 +0800225 return activity == null ? 1 : SizeCalculator.getNumCategoryColumns(activity);
Chuck Liao1e0501f2020-02-17 18:20:54 +0800226 }
227
228
229 private CategorySelectorFragmentHost getCategorySelectorFragmentHost() {
Chuck Liao8cbe49f2021-03-15 20:28:04 +0800230 Fragment parentFragment = getParentFragment();
231 if (parentFragment != null) {
232 return (CategorySelectorFragmentHost) parentFragment;
233 } else {
234 return (CategorySelectorFragmentHost) getActivity();
235 }
Chuck Liao1e0501f2020-02-17 18:20:54 +0800236 }
237
238 /**
239 * ViewHolder subclass for a category tile in the RecyclerView.
240 */
241 private class CategoryHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
242 private Category mCategory;
243 private ImageView mImageView;
244 private ImageView mOverlayIconView;
245 private TextView mTitleView;
246
247 CategoryHolder(View itemView) {
248 super(itemView);
249 itemView.setOnClickListener(this);
250
251 mImageView = itemView.findViewById(R.id.image);
252 mOverlayIconView = itemView.findViewById(R.id.overlay_icon);
253 mTitleView = itemView.findViewById(R.id.category_title);
254
255 CardView categoryView = itemView.findViewById(R.id.category);
256 categoryView.getLayoutParams().height = mTileSizePx.y;
257 }
258
259 @Override
260 public void onClick(View view) {
261 final UserEventLogger eventLogger =
262 InjectorProvider.getInjector().getUserEventLogger(getActivity());
263 eventLogger.logCategorySelected(mCategory.getCollectionId());
264
265 if (mCategory.supportsCustomPhotos()) {
266 getCategorySelectorFragmentHost().requestCustomPhotoPicker(
267 new MyPhotosStarter.PermissionChangedListener() {
268 @Override
269 public void onPermissionsGranted() {
270 drawThumbnailAndOverlayIcon();
271 }
272
273 @Override
274 public void onPermissionsDenied(boolean dontAskAgain) {
275 // No-op
276 }
277 });
278 return;
279 }
280
Chuck Liaoa63f1bf2020-06-11 01:35:42 +0800281 getCategorySelectorFragmentHost().show(mCategory);
Chuck Liao1e0501f2020-02-17 18:20:54 +0800282 }
283
284 /**
285 * Binds the given category to this CategoryHolder.
286 */
287 private void bindCategory(Category category) {
288 mCategory = category;
289 mTitleView.setText(category.getTitle());
290 drawThumbnailAndOverlayIcon();
291 }
292
293 /**
294 * Draws the CategoryHolder's thumbnail and overlay icon.
295 */
296 private void drawThumbnailAndOverlayIcon() {
297 mOverlayIconView.setImageDrawable(mCategory.getOverlayIcon(
298 getActivity().getApplicationContext()));
299
300 // Size the overlay icon according to the category.
301 int overlayIconDimenDp = mCategory.getOverlayIconSizeDp();
302 DisplayMetrics metrics = DisplayMetricsRetriever.getInstance().getDisplayMetrics(
303 getResources(), getActivity().getWindowManager().getDefaultDisplay());
304 int overlayIconDimenPx = (int) (overlayIconDimenDp * metrics.density);
305 mOverlayIconView.getLayoutParams().width = overlayIconDimenPx;
306 mOverlayIconView.getLayoutParams().height = overlayIconDimenPx;
307
308 Asset thumbnail = mCategory.getThumbnail(getActivity().getApplicationContext());
309 if (thumbnail != null) {
310 thumbnail.loadDrawable(getActivity(), mImageView,
311 getResources().getColor(R.color.secondary_color));
312 } else {
313 // TODO(orenb): Replace this workaround for b/62584914 with a proper way of
314 // unloading the ImageView such that no incorrect image is improperly loaded upon
315 // rapid scroll.
316 Object nullObj = null;
317 Glide.with(getActivity())
318 .asDrawable()
319 .load(nullObj)
320 .into(mImageView);
321
322 }
323 }
324 }
325
326 /**
327 * ViewHolder subclass for the loading indicator ("spinner") shown when categories are being
328 * fetched.
329 */
330 private class LoadingIndicatorHolder extends RecyclerView.ViewHolder {
331 private LoadingIndicatorHolder(View view) {
332 super(view);
333 ProgressBar progressBar = view.findViewById(R.id.loading_indicator);
334 progressBar.getIndeterminateDrawable().setColorFilter(
335 getResources().getColor(R.color.accent_color), PorterDuff.Mode.SRC_IN);
336 }
337 }
338
339 /**
340 * RecyclerView Adapter subclass for the category tiles in the RecyclerView.
341 */
342 private class CategoryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
343 implements MyPhotosStarter.PermissionChangedListener {
344 private static final int ITEM_VIEW_TYPE_CATEGORY = 3;
345 private static final int ITEM_VIEW_TYPE_LOADING_INDICATOR = 4;
346 private List<Category> mCategories;
347
348 private CategoryAdapter(List<Category> categories) {
349 mCategories = categories;
350 }
351
352 @Override
353 public int getItemViewType(int position) {
354 if (mAwaitingCategories && position == getItemCount() - 1) {
355 return ITEM_VIEW_TYPE_LOADING_INDICATOR;
356 }
357
358 return ITEM_VIEW_TYPE_CATEGORY;
359 }
360
361 @Override
362 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
363 LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
364 View view;
365
366 switch (viewType) {
367 case ITEM_VIEW_TYPE_LOADING_INDICATOR:
368 view = layoutInflater.inflate(R.layout.grid_item_loading_indicator,
369 parent, /* attachToRoot= */ false);
370 return new LoadingIndicatorHolder(view);
371 case ITEM_VIEW_TYPE_CATEGORY:
372 view = layoutInflater.inflate(R.layout.grid_item_category,
373 parent, /* attachToRoot= */ false);
374 return new CategoryHolder(view);
375 default:
376 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
377 return null;
378 }
379 }
380
381 @Override
382 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
383 int viewType = getItemViewType(position);
384
385 switch (viewType) {
386 case ITEM_VIEW_TYPE_CATEGORY:
387 // Offset position to get category index to account for the non-category view
388 // holders.
389 Category category = mCategories.get(position - NUM_NON_CATEGORY_VIEW_HOLDERS);
390 ((CategoryHolder) holder).bindCategory(category);
391 break;
392 case ITEM_VIEW_TYPE_LOADING_INDICATOR:
393 // No op.
394 break;
395 default:
396 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
397 }
398 }
399
400 @Override
401 public int getItemCount() {
402 // Add to size of categories to account for the metadata related views.
403 // Add 1 more for the loading indicator if not yet done loading.
404 int size = mCategories.size() + NUM_NON_CATEGORY_VIEW_HOLDERS;
405 if (mAwaitingCategories) {
406 size += 1;
407 }
408
409 return size;
410 }
411
412 @Override
413 public void onPermissionsGranted() {
414 notifyDataSetChanged();
415 }
416
417 @Override
418 public void onPermissionsDenied(boolean dontAskAgain) {
419 if (!dontAskAgain) {
420 return;
421 }
422
423 String permissionNeededMessage =
424 getString(R.string.permission_needed_explanation_go_to_settings);
425 AlertDialog dialog = new AlertDialog.Builder(getActivity(), R.style.LightDialogTheme)
426 .setMessage(permissionNeededMessage)
427 .setPositiveButton(android.R.string.ok, null /* onClickListener */)
428 .setNegativeButton(
429 R.string.settings_button_label,
430 (dialogInterface, i) -> {
431 Intent appInfoIntent =
432 new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
433 Uri uri = Uri.fromParts("package",
434 getActivity().getPackageName(), /* fragment= */ null);
435 appInfoIntent.setData(uri);
436 startActivityForResult(
437 appInfoIntent, SETTINGS_APP_INFO_REQUEST_CODE);
438 })
439 .create();
440 dialog.show();
441 }
442 }
443
444 private class GridPaddingDecoration extends RecyclerView.ItemDecoration {
445
446 private int mPadding;
447
448 GridPaddingDecoration(int padding) {
449 mPadding = padding;
450 }
451
452 @Override
453 public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
454 RecyclerView.State state) {
455 int position = parent.getChildAdapterPosition(view) - NUM_NON_CATEGORY_VIEW_HOLDERS;
456 if (position >= 0) {
457 outRect.left = mPadding;
458 outRect.right = mPadding;
459 }
460 }
461 }
462
463 /**
464 * SpanSizeLookup subclass which provides that the item in the first position spans the number
465 * of columns in the RecyclerView and all other items only take up a single span.
466 */
467 private class CategorySpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
468 CategoryAdapter mAdapter;
469
470 private CategorySpanSizeLookup(CategoryAdapter adapter) {
471 mAdapter = adapter;
472 }
473
474 @Override
475 public int getSpanSize(int position) {
476 if (position < NUM_NON_CATEGORY_VIEW_HOLDERS
477 || mAdapter.getItemViewType(position)
478 == CategoryAdapter.ITEM_VIEW_TYPE_LOADING_INDICATOR) {
479 return getNumColumns();
480 }
481
482 return 1;
483 }
484 }
Chuck Liao1e0501f2020-02-17 18:20:54 +0800485}