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