blob: c82fc873f69c21c1e93b277f72f47c6566d1d56a [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 */
Chuck Liao58aca1c2021-03-17 01:20:55 +080062public class CategorySelectorFragment extends AppbarFragment {
Chuck Liao1e0501f2020-02-17 18:20:54 +080063
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
Chuck Liao58aca1c2021-03-17 01:20:55 +080088
Chuck Liao0088bca2020-05-28 16:03:23 +080089 /**
Chuck Liao58aca1c2021-03-17 01:20:55 +080090 * Indicates if the host has toolbar to show the title. If it does, we should set the title
91 * there.
92 */
93 boolean isHostToolbarShown();
94
95 /**
96 * Sets the title in the host's toolbar.
Chuck Liao0088bca2020-05-28 16:03:23 +080097 */
98 void setToolbarTitle(CharSequence title);
Chuck Liaof6b4b192020-08-07 02:31:32 +080099
100 /**
101 * Fetches the wallpaper categories.
102 */
103 void fetchCategories();
Chuck Liao81145b22020-09-03 09:52:25 +0800104
105 /**
Chuck Liao3abf15b2020-12-17 22:33:02 +0800106 * Cleans up the listeners which will be notified when there's a package event.
107 */
108 void cleanUp();
109
110 /**
Chuck Liao81145b22020-09-03 09:52:25 +0800111 * Hides the {@link com.android.wallpaper.widget.BottomActionBar}.
112 */
113 void hideBottomActionBar();
Chuck Liao1e0501f2020-02-17 18:20:54 +0800114 }
115
116 private RecyclerView mImageGrid;
117 private CategoryAdapter mAdapter;
118 private ArrayList<Category> mCategories = new ArrayList<>();
119 private Point mTileSizePx;
120 private boolean mAwaitingCategories;
121
122 public CategorySelectorFragment() {
123 mAdapter = new CategoryAdapter(mCategories);
124 }
125
126 @Nullable
127 @Override
128 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
129 @Nullable Bundle savedInstanceState) {
130 View view = inflater.inflate(R.layout.fragment_category_selector, container,
131 /* attachToRoot= */ false);
Chuck Liao1e0501f2020-02-17 18:20:54 +0800132 mImageGrid = view.findViewById(R.id.category_grid);
133 mImageGrid.addItemDecoration(new GridPaddingDecoration(
134 getResources().getDimensionPixelSize(R.dimen.grid_padding)));
135
Santiago Etchebehere53c63432020-05-07 18:55:35 -0700136 mTileSizePx = SizeCalculator.getCategoryTileSize(getActivity());
Chuck Liao1e0501f2020-02-17 18:20:54 +0800137
138 mImageGrid.setAdapter(mAdapter);
139
140 GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), getNumColumns());
141 mImageGrid.setLayoutManager(gridLayoutManager);
Wesley.CW Wang27ff4262020-06-12 19:19:57 +0800142 mImageGrid.setAccessibilityDelegateCompat(
Wesley.CW Wangdc68fde2020-06-15 19:12:33 +0800143 new WallpaperPickerRecyclerViewAccessibilityDelegate(
144 mImageGrid, (BottomSheetHost) getParentFragment(), getNumColumns()));
Chuck Liao58aca1c2021-03-17 01:20:55 +0800145
146 if (getCategorySelectorFragmentHost().isHostToolbarShown()) {
147 view.findViewById(R.id.header_bar).setVisibility(View.GONE);
148 getCategorySelectorFragmentHost().setToolbarTitle(getText(R.string.wallpaper_title));
149 } else {
150 setUpToolbar(view);
151 setTitle(getText(R.string.wallpaper_title));
152 }
Chuck Liao1e0501f2020-02-17 18:20:54 +0800153
Chuck Liaof6b4b192020-08-07 02:31:32 +0800154 if (!DeepLinkUtils.isDeepLink(getActivity().getIntent())) {
155 getCategorySelectorFragmentHost().fetchCategories();
156 }
157
Chuck Liao81145b22020-09-03 09:52:25 +0800158 getCategorySelectorFragmentHost().hideBottomActionBar();
159
Chuck Liao1e0501f2020-02-17 18:20:54 +0800160 return view;
161 }
162
Chuck Liao3abf15b2020-12-17 22:33:02 +0800163 @Override
164 public void onDestroyView() {
165 getCategorySelectorFragmentHost().cleanUp();
166 super.onDestroyView();
167 }
168
Chuck Liao1e0501f2020-02-17 18:20:54 +0800169 /**
170 * Inserts the given category into the categories list in priority order.
171 */
172 void addCategory(Category category, boolean loading) {
173 // If not previously waiting for categories, enter the waiting state by showing the loading
174 // indicator.
175 if (loading && !mAwaitingCategories) {
176 mAdapter.notifyItemChanged(getNumColumns());
177 mAdapter.notifyItemInserted(getNumColumns());
178 mAwaitingCategories = true;
179 }
180 // Not add existing category to category list
181 if (mCategories.indexOf(category) >= 0) {
182 updateCategory(category);
183 return;
184 }
185
186 int priority = category.getPriority();
187
188 int index = 0;
189 while (index < mCategories.size() && priority >= mCategories.get(index).getPriority()) {
190 index++;
191 }
192
193 mCategories.add(index, category);
194 if (mAdapter != null) {
195 // Offset the index because of the static metadata element at beginning of RecyclerView.
196 mAdapter.notifyItemInserted(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
197 }
198 }
199
200 void removeCategory(Category category) {
201 int index = mCategories.indexOf(category);
202 if (index >= 0) {
203 mCategories.remove(index);
204 mAdapter.notifyItemRemoved(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
205 }
206 }
207
208 void updateCategory(Category category) {
209 int index = mCategories.indexOf(category);
210 if (index >= 0) {
211 mCategories.remove(index);
212 mCategories.add(index, category);
213 mAdapter.notifyItemChanged(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
214 }
215 }
216
217 void clearCategories() {
218 mCategories.clear();
219 mAdapter.notifyDataSetChanged();
220 }
221
222 /**
223 * Notifies the CategoryFragment that no further categories are expected so it may hide
224 * the loading indicator.
225 */
226 void doneFetchingCategories() {
227 if (mAwaitingCategories) {
228 mAdapter.notifyItemRemoved(mAdapter.getItemCount() - 1);
229 mAwaitingCategories = false;
230 }
231 }
232
233 void notifyDataSetChanged() {
234 mAdapter.notifyDataSetChanged();
235 }
236
237 private int getNumColumns() {
238 Activity activity = getActivity();
Chuck Liaoe2fe0302020-06-29 21:15:35 +0800239 return activity == null ? 1 : SizeCalculator.getNumCategoryColumns(activity);
Chuck Liao1e0501f2020-02-17 18:20:54 +0800240 }
241
242
243 private CategorySelectorFragmentHost getCategorySelectorFragmentHost() {
Chuck Liao8cbe49f2021-03-15 20:28:04 +0800244 Fragment parentFragment = getParentFragment();
245 if (parentFragment != null) {
246 return (CategorySelectorFragmentHost) parentFragment;
247 } else {
248 return (CategorySelectorFragmentHost) getActivity();
249 }
Chuck Liao1e0501f2020-02-17 18:20:54 +0800250 }
251
252 /**
253 * ViewHolder subclass for a category tile in the RecyclerView.
254 */
255 private class CategoryHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
256 private Category mCategory;
257 private ImageView mImageView;
258 private ImageView mOverlayIconView;
259 private TextView mTitleView;
260
261 CategoryHolder(View itemView) {
262 super(itemView);
263 itemView.setOnClickListener(this);
264
265 mImageView = itemView.findViewById(R.id.image);
266 mOverlayIconView = itemView.findViewById(R.id.overlay_icon);
267 mTitleView = itemView.findViewById(R.id.category_title);
268
269 CardView categoryView = itemView.findViewById(R.id.category);
270 categoryView.getLayoutParams().height = mTileSizePx.y;
271 }
272
273 @Override
274 public void onClick(View view) {
275 final UserEventLogger eventLogger =
276 InjectorProvider.getInjector().getUserEventLogger(getActivity());
277 eventLogger.logCategorySelected(mCategory.getCollectionId());
278
279 if (mCategory.supportsCustomPhotos()) {
280 getCategorySelectorFragmentHost().requestCustomPhotoPicker(
281 new MyPhotosStarter.PermissionChangedListener() {
282 @Override
283 public void onPermissionsGranted() {
284 drawThumbnailAndOverlayIcon();
285 }
286
287 @Override
288 public void onPermissionsDenied(boolean dontAskAgain) {
289 // No-op
290 }
291 });
292 return;
293 }
294
Chuck Liaoa63f1bf2020-06-11 01:35:42 +0800295 getCategorySelectorFragmentHost().show(mCategory);
Chuck Liao1e0501f2020-02-17 18:20:54 +0800296 }
297
298 /**
299 * Binds the given category to this CategoryHolder.
300 */
301 private void bindCategory(Category category) {
302 mCategory = category;
303 mTitleView.setText(category.getTitle());
304 drawThumbnailAndOverlayIcon();
305 }
306
307 /**
308 * Draws the CategoryHolder's thumbnail and overlay icon.
309 */
310 private void drawThumbnailAndOverlayIcon() {
311 mOverlayIconView.setImageDrawable(mCategory.getOverlayIcon(
312 getActivity().getApplicationContext()));
313
314 // Size the overlay icon according to the category.
315 int overlayIconDimenDp = mCategory.getOverlayIconSizeDp();
316 DisplayMetrics metrics = DisplayMetricsRetriever.getInstance().getDisplayMetrics(
317 getResources(), getActivity().getWindowManager().getDefaultDisplay());
318 int overlayIconDimenPx = (int) (overlayIconDimenDp * metrics.density);
319 mOverlayIconView.getLayoutParams().width = overlayIconDimenPx;
320 mOverlayIconView.getLayoutParams().height = overlayIconDimenPx;
321
322 Asset thumbnail = mCategory.getThumbnail(getActivity().getApplicationContext());
323 if (thumbnail != null) {
324 thumbnail.loadDrawable(getActivity(), mImageView,
325 getResources().getColor(R.color.secondary_color));
326 } else {
327 // TODO(orenb): Replace this workaround for b/62584914 with a proper way of
328 // unloading the ImageView such that no incorrect image is improperly loaded upon
329 // rapid scroll.
330 Object nullObj = null;
331 Glide.with(getActivity())
332 .asDrawable()
333 .load(nullObj)
334 .into(mImageView);
335
336 }
337 }
338 }
339
340 /**
341 * ViewHolder subclass for the loading indicator ("spinner") shown when categories are being
342 * fetched.
343 */
344 private class LoadingIndicatorHolder extends RecyclerView.ViewHolder {
345 private LoadingIndicatorHolder(View view) {
346 super(view);
347 ProgressBar progressBar = view.findViewById(R.id.loading_indicator);
348 progressBar.getIndeterminateDrawable().setColorFilter(
349 getResources().getColor(R.color.accent_color), PorterDuff.Mode.SRC_IN);
350 }
351 }
352
353 /**
354 * RecyclerView Adapter subclass for the category tiles in the RecyclerView.
355 */
356 private class CategoryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
357 implements MyPhotosStarter.PermissionChangedListener {
358 private static final int ITEM_VIEW_TYPE_CATEGORY = 3;
359 private static final int ITEM_VIEW_TYPE_LOADING_INDICATOR = 4;
360 private List<Category> mCategories;
361
362 private CategoryAdapter(List<Category> categories) {
363 mCategories = categories;
364 }
365
366 @Override
367 public int getItemViewType(int position) {
368 if (mAwaitingCategories && position == getItemCount() - 1) {
369 return ITEM_VIEW_TYPE_LOADING_INDICATOR;
370 }
371
372 return ITEM_VIEW_TYPE_CATEGORY;
373 }
374
375 @Override
376 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
377 LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
378 View view;
379
380 switch (viewType) {
381 case ITEM_VIEW_TYPE_LOADING_INDICATOR:
382 view = layoutInflater.inflate(R.layout.grid_item_loading_indicator,
383 parent, /* attachToRoot= */ false);
384 return new LoadingIndicatorHolder(view);
385 case ITEM_VIEW_TYPE_CATEGORY:
386 view = layoutInflater.inflate(R.layout.grid_item_category,
387 parent, /* attachToRoot= */ false);
388 return new CategoryHolder(view);
389 default:
390 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
391 return null;
392 }
393 }
394
395 @Override
396 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
397 int viewType = getItemViewType(position);
398
399 switch (viewType) {
400 case ITEM_VIEW_TYPE_CATEGORY:
401 // Offset position to get category index to account for the non-category view
402 // holders.
403 Category category = mCategories.get(position - NUM_NON_CATEGORY_VIEW_HOLDERS);
404 ((CategoryHolder) holder).bindCategory(category);
405 break;
406 case ITEM_VIEW_TYPE_LOADING_INDICATOR:
407 // No op.
408 break;
409 default:
410 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
411 }
412 }
413
414 @Override
415 public int getItemCount() {
416 // Add to size of categories to account for the metadata related views.
417 // Add 1 more for the loading indicator if not yet done loading.
418 int size = mCategories.size() + NUM_NON_CATEGORY_VIEW_HOLDERS;
419 if (mAwaitingCategories) {
420 size += 1;
421 }
422
423 return size;
424 }
425
426 @Override
427 public void onPermissionsGranted() {
428 notifyDataSetChanged();
429 }
430
431 @Override
432 public void onPermissionsDenied(boolean dontAskAgain) {
433 if (!dontAskAgain) {
434 return;
435 }
436
437 String permissionNeededMessage =
438 getString(R.string.permission_needed_explanation_go_to_settings);
439 AlertDialog dialog = new AlertDialog.Builder(getActivity(), R.style.LightDialogTheme)
440 .setMessage(permissionNeededMessage)
441 .setPositiveButton(android.R.string.ok, null /* onClickListener */)
442 .setNegativeButton(
443 R.string.settings_button_label,
444 (dialogInterface, i) -> {
445 Intent appInfoIntent =
446 new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
447 Uri uri = Uri.fromParts("package",
448 getActivity().getPackageName(), /* fragment= */ null);
449 appInfoIntent.setData(uri);
450 startActivityForResult(
451 appInfoIntent, SETTINGS_APP_INFO_REQUEST_CODE);
452 })
453 .create();
454 dialog.show();
455 }
456 }
457
458 private class GridPaddingDecoration extends RecyclerView.ItemDecoration {
459
460 private int mPadding;
461
462 GridPaddingDecoration(int padding) {
463 mPadding = padding;
464 }
465
466 @Override
467 public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
468 RecyclerView.State state) {
469 int position = parent.getChildAdapterPosition(view) - NUM_NON_CATEGORY_VIEW_HOLDERS;
470 if (position >= 0) {
471 outRect.left = mPadding;
472 outRect.right = mPadding;
473 }
474 }
475 }
476
477 /**
478 * SpanSizeLookup subclass which provides that the item in the first position spans the number
479 * of columns in the RecyclerView and all other items only take up a single span.
480 */
481 private class CategorySpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
482 CategoryAdapter mAdapter;
483
484 private CategorySpanSizeLookup(CategoryAdapter adapter) {
485 mAdapter = adapter;
486 }
487
488 @Override
489 public int getSpanSize(int position) {
490 if (position < NUM_NON_CATEGORY_VIEW_HOLDERS
491 || mAdapter.getItemViewType(position)
492 == CategoryAdapter.ITEM_VIEW_TYPE_LOADING_INDICATOR) {
493 return getNumColumns();
494 }
495
496 return 1;
497 }
498 }
Chuck Liao1e0501f2020-02-17 18:20:54 +0800499}