blob: c803231b65c71a93ebd3a17061250d2e7bf6c467 [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;
Wesley.CW Wang27ff4262020-06-12 19:19:57 +080031import android.view.accessibility.AccessibilityEvent;
Chuck Liao1e0501f2020-02-17 18:20:54 +080032import android.widget.ImageView;
33import android.widget.ProgressBar;
34import android.widget.TextView;
35
36import androidx.annotation.NonNull;
37import androidx.annotation.Nullable;
38import androidx.appcompat.app.AlertDialog;
39import androidx.cardview.widget.CardView;
40import androidx.fragment.app.Fragment;
41import androidx.recyclerview.widget.GridLayoutManager;
42import androidx.recyclerview.widget.RecyclerView;
Wesley.CW Wang27ff4262020-06-12 19:19:57 +080043import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
Chuck Liao1e0501f2020-02-17 18:20:54 +080044
45import com.android.wallpaper.R;
46import com.android.wallpaper.asset.Asset;
47import com.android.wallpaper.model.Category;
48import com.android.wallpaper.module.InjectorProvider;
49import com.android.wallpaper.module.UserEventLogger;
50import com.android.wallpaper.util.DisplayMetricsRetriever;
Santiago Etchebehere53c63432020-05-07 18:55:35 -070051import com.android.wallpaper.util.SizeCalculator;
Chuck Liao1e0501f2020-02-17 18:20:54 +080052
53import com.bumptech.glide.Glide;
Wesley.CW Wang27ff4262020-06-12 19:19:57 +080054import com.google.android.material.bottomsheet.BottomSheetBehavior;
Chuck Liao1e0501f2020-02-17 18:20:54 +080055
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 *
84 * @param collectionId the id of the category
85 */
86 void show(String collectionId);
Chuck Liao0088bca2020-05-28 16:03:23 +080087
88 /**
89 * Sets the title in the toolbar.
90 */
91 void setToolbarTitle(CharSequence title);
Wesley.CW Wang27ff4262020-06-12 19:19:57 +080092
93 /**
94 * Expand the bottom sheet if it's not expanded.
95 */
96 void expandBottomSheet();
97
98 /**
99 * Get bottom sheet current state.
100 */
101 int getBottomSheetState();
Chuck Liao1e0501f2020-02-17 18:20:54 +0800102 }
103
104 private RecyclerView mImageGrid;
105 private CategoryAdapter mAdapter;
106 private ArrayList<Category> mCategories = new ArrayList<>();
107 private Point mTileSizePx;
108 private boolean mAwaitingCategories;
109
110 public CategorySelectorFragment() {
111 mAdapter = new CategoryAdapter(mCategories);
112 }
113
114 @Nullable
115 @Override
116 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
117 @Nullable Bundle savedInstanceState) {
118 View view = inflater.inflate(R.layout.fragment_category_selector, container,
119 /* attachToRoot= */ false);
Chuck Liao1e0501f2020-02-17 18:20:54 +0800120 mImageGrid = view.findViewById(R.id.category_grid);
121 mImageGrid.addItemDecoration(new GridPaddingDecoration(
122 getResources().getDimensionPixelSize(R.dimen.grid_padding)));
123
Santiago Etchebehere53c63432020-05-07 18:55:35 -0700124 mTileSizePx = SizeCalculator.getCategoryTileSize(getActivity());
Chuck Liao1e0501f2020-02-17 18:20:54 +0800125
126 mImageGrid.setAdapter(mAdapter);
127
128 GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), getNumColumns());
129 mImageGrid.setLayoutManager(gridLayoutManager);
Chuck Liaoca97c572020-05-08 17:13:25 +0800130 mImageGrid.setOnApplyWindowInsetsListener((v, windowInsets) -> {
131 v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(),
132 windowInsets.getSystemWindowInsetBottom());
133 return windowInsets;
134 });
Wesley.CW Wang27ff4262020-06-12 19:19:57 +0800135 mImageGrid.setAccessibilityDelegateCompat(
136 new CategoryRecyclerViewAccessibilityDelegate(mImageGrid));
Chuck Liao0088bca2020-05-28 16:03:23 +0800137 getCategorySelectorFragmentHost().setToolbarTitle(getText(R.string.wallpaper_title));
Chuck Liao1e0501f2020-02-17 18:20:54 +0800138
139 return view;
140 }
141
142 /**
143 * Inserts the given category into the categories list in priority order.
144 */
145 void addCategory(Category category, boolean loading) {
146 // If not previously waiting for categories, enter the waiting state by showing the loading
147 // indicator.
148 if (loading && !mAwaitingCategories) {
149 mAdapter.notifyItemChanged(getNumColumns());
150 mAdapter.notifyItemInserted(getNumColumns());
151 mAwaitingCategories = true;
152 }
153 // Not add existing category to category list
154 if (mCategories.indexOf(category) >= 0) {
155 updateCategory(category);
156 return;
157 }
158
159 int priority = category.getPriority();
160
161 int index = 0;
162 while (index < mCategories.size() && priority >= mCategories.get(index).getPriority()) {
163 index++;
164 }
165
166 mCategories.add(index, category);
167 if (mAdapter != null) {
168 // Offset the index because of the static metadata element at beginning of RecyclerView.
169 mAdapter.notifyItemInserted(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
170 }
171 }
172
173 void removeCategory(Category category) {
174 int index = mCategories.indexOf(category);
175 if (index >= 0) {
176 mCategories.remove(index);
177 mAdapter.notifyItemRemoved(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
178 }
179 }
180
181 void updateCategory(Category category) {
182 int index = mCategories.indexOf(category);
183 if (index >= 0) {
184 mCategories.remove(index);
185 mCategories.add(index, category);
186 mAdapter.notifyItemChanged(index + NUM_NON_CATEGORY_VIEW_HOLDERS);
187 }
188 }
189
190 void clearCategories() {
191 mCategories.clear();
192 mAdapter.notifyDataSetChanged();
193 }
194
195 /**
196 * Notifies the CategoryFragment that no further categories are expected so it may hide
197 * the loading indicator.
198 */
199 void doneFetchingCategories() {
200 if (mAwaitingCategories) {
201 mAdapter.notifyItemRemoved(mAdapter.getItemCount() - 1);
202 mAwaitingCategories = false;
203 }
204 }
205
206 void notifyDataSetChanged() {
207 mAdapter.notifyDataSetChanged();
208 }
209
210 private int getNumColumns() {
211 Activity activity = getActivity();
Santiago Etchebehere53c63432020-05-07 18:55:35 -0700212 return activity == null ? 0 : SizeCalculator.getNumCategoryColumns(activity);
Chuck Liao1e0501f2020-02-17 18:20:54 +0800213 }
214
215
216 private CategorySelectorFragmentHost getCategorySelectorFragmentHost() {
217 return (CategorySelectorFragmentHost) getParentFragment();
218 }
219
220 /**
221 * ViewHolder subclass for a category tile in the RecyclerView.
222 */
223 private class CategoryHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
224 private Category mCategory;
225 private ImageView mImageView;
226 private ImageView mOverlayIconView;
227 private TextView mTitleView;
228
229 CategoryHolder(View itemView) {
230 super(itemView);
231 itemView.setOnClickListener(this);
232
233 mImageView = itemView.findViewById(R.id.image);
234 mOverlayIconView = itemView.findViewById(R.id.overlay_icon);
235 mTitleView = itemView.findViewById(R.id.category_title);
236
237 CardView categoryView = itemView.findViewById(R.id.category);
238 categoryView.getLayoutParams().height = mTileSizePx.y;
239 }
240
241 @Override
242 public void onClick(View view) {
243 final UserEventLogger eventLogger =
244 InjectorProvider.getInjector().getUserEventLogger(getActivity());
245 eventLogger.logCategorySelected(mCategory.getCollectionId());
246
247 if (mCategory.supportsCustomPhotos()) {
248 getCategorySelectorFragmentHost().requestCustomPhotoPicker(
249 new MyPhotosStarter.PermissionChangedListener() {
250 @Override
251 public void onPermissionsGranted() {
252 drawThumbnailAndOverlayIcon();
253 }
254
255 @Override
256 public void onPermissionsDenied(boolean dontAskAgain) {
257 // No-op
258 }
259 });
260 return;
261 }
262
263 getCategorySelectorFragmentHost().show(mCategory.getCollectionId());
Chuck Liao0088bca2020-05-28 16:03:23 +0800264 getCategorySelectorFragmentHost().setToolbarTitle(mCategory.getTitle());
Chuck Liao1e0501f2020-02-17 18:20:54 +0800265 }
266
267 /**
268 * Binds the given category to this CategoryHolder.
269 */
270 private void bindCategory(Category category) {
271 mCategory = category;
272 mTitleView.setText(category.getTitle());
273 drawThumbnailAndOverlayIcon();
274 }
275
276 /**
277 * Draws the CategoryHolder's thumbnail and overlay icon.
278 */
279 private void drawThumbnailAndOverlayIcon() {
280 mOverlayIconView.setImageDrawable(mCategory.getOverlayIcon(
281 getActivity().getApplicationContext()));
282
283 // Size the overlay icon according to the category.
284 int overlayIconDimenDp = mCategory.getOverlayIconSizeDp();
285 DisplayMetrics metrics = DisplayMetricsRetriever.getInstance().getDisplayMetrics(
286 getResources(), getActivity().getWindowManager().getDefaultDisplay());
287 int overlayIconDimenPx = (int) (overlayIconDimenDp * metrics.density);
288 mOverlayIconView.getLayoutParams().width = overlayIconDimenPx;
289 mOverlayIconView.getLayoutParams().height = overlayIconDimenPx;
290
291 Asset thumbnail = mCategory.getThumbnail(getActivity().getApplicationContext());
292 if (thumbnail != null) {
293 thumbnail.loadDrawable(getActivity(), mImageView,
294 getResources().getColor(R.color.secondary_color));
295 } else {
296 // TODO(orenb): Replace this workaround for b/62584914 with a proper way of
297 // unloading the ImageView such that no incorrect image is improperly loaded upon
298 // rapid scroll.
299 Object nullObj = null;
300 Glide.with(getActivity())
301 .asDrawable()
302 .load(nullObj)
303 .into(mImageView);
304
305 }
306 }
307 }
308
309 /**
310 * ViewHolder subclass for the loading indicator ("spinner") shown when categories are being
311 * fetched.
312 */
313 private class LoadingIndicatorHolder extends RecyclerView.ViewHolder {
314 private LoadingIndicatorHolder(View view) {
315 super(view);
316 ProgressBar progressBar = view.findViewById(R.id.loading_indicator);
317 progressBar.getIndeterminateDrawable().setColorFilter(
318 getResources().getColor(R.color.accent_color), PorterDuff.Mode.SRC_IN);
319 }
320 }
321
322 /**
323 * RecyclerView Adapter subclass for the category tiles in the RecyclerView.
324 */
325 private class CategoryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
326 implements MyPhotosStarter.PermissionChangedListener {
327 private static final int ITEM_VIEW_TYPE_CATEGORY = 3;
328 private static final int ITEM_VIEW_TYPE_LOADING_INDICATOR = 4;
329 private List<Category> mCategories;
330
331 private CategoryAdapter(List<Category> categories) {
332 mCategories = categories;
333 }
334
335 @Override
336 public int getItemViewType(int position) {
337 if (mAwaitingCategories && position == getItemCount() - 1) {
338 return ITEM_VIEW_TYPE_LOADING_INDICATOR;
339 }
340
341 return ITEM_VIEW_TYPE_CATEGORY;
342 }
343
344 @Override
345 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
346 LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
347 View view;
348
349 switch (viewType) {
350 case ITEM_VIEW_TYPE_LOADING_INDICATOR:
351 view = layoutInflater.inflate(R.layout.grid_item_loading_indicator,
352 parent, /* attachToRoot= */ false);
353 return new LoadingIndicatorHolder(view);
354 case ITEM_VIEW_TYPE_CATEGORY:
355 view = layoutInflater.inflate(R.layout.grid_item_category,
356 parent, /* attachToRoot= */ false);
357 return new CategoryHolder(view);
358 default:
359 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
360 return null;
361 }
362 }
363
364 @Override
365 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
366 int viewType = getItemViewType(position);
367
368 switch (viewType) {
369 case ITEM_VIEW_TYPE_CATEGORY:
370 // Offset position to get category index to account for the non-category view
371 // holders.
372 Category category = mCategories.get(position - NUM_NON_CATEGORY_VIEW_HOLDERS);
373 ((CategoryHolder) holder).bindCategory(category);
374 break;
375 case ITEM_VIEW_TYPE_LOADING_INDICATOR:
376 // No op.
377 break;
378 default:
379 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter");
380 }
381 }
382
383 @Override
384 public int getItemCount() {
385 // Add to size of categories to account for the metadata related views.
386 // Add 1 more for the loading indicator if not yet done loading.
387 int size = mCategories.size() + NUM_NON_CATEGORY_VIEW_HOLDERS;
388 if (mAwaitingCategories) {
389 size += 1;
390 }
391
392 return size;
393 }
394
395 @Override
396 public void onPermissionsGranted() {
397 notifyDataSetChanged();
398 }
399
400 @Override
401 public void onPermissionsDenied(boolean dontAskAgain) {
402 if (!dontAskAgain) {
403 return;
404 }
405
406 String permissionNeededMessage =
407 getString(R.string.permission_needed_explanation_go_to_settings);
408 AlertDialog dialog = new AlertDialog.Builder(getActivity(), R.style.LightDialogTheme)
409 .setMessage(permissionNeededMessage)
410 .setPositiveButton(android.R.string.ok, null /* onClickListener */)
411 .setNegativeButton(
412 R.string.settings_button_label,
413 (dialogInterface, i) -> {
414 Intent appInfoIntent =
415 new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
416 Uri uri = Uri.fromParts("package",
417 getActivity().getPackageName(), /* fragment= */ null);
418 appInfoIntent.setData(uri);
419 startActivityForResult(
420 appInfoIntent, SETTINGS_APP_INFO_REQUEST_CODE);
421 })
422 .create();
423 dialog.show();
424 }
425 }
426
427 private class GridPaddingDecoration extends RecyclerView.ItemDecoration {
428
429 private int mPadding;
430
431 GridPaddingDecoration(int padding) {
432 mPadding = padding;
433 }
434
435 @Override
436 public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
437 RecyclerView.State state) {
438 int position = parent.getChildAdapterPosition(view) - NUM_NON_CATEGORY_VIEW_HOLDERS;
439 if (position >= 0) {
440 outRect.left = mPadding;
441 outRect.right = mPadding;
442 }
443 }
444 }
445
446 /**
447 * SpanSizeLookup subclass which provides that the item in the first position spans the number
448 * of columns in the RecyclerView and all other items only take up a single span.
449 */
450 private class CategorySpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
451 CategoryAdapter mAdapter;
452
453 private CategorySpanSizeLookup(CategoryAdapter adapter) {
454 mAdapter = adapter;
455 }
456
457 @Override
458 public int getSpanSize(int position) {
459 if (position < NUM_NON_CATEGORY_VIEW_HOLDERS
460 || mAdapter.getItemViewType(position)
461 == CategoryAdapter.ITEM_VIEW_TYPE_LOADING_INDICATOR) {
462 return getNumColumns();
463 }
464
465 return 1;
466 }
467 }
Wesley.CW Wang27ff4262020-06-12 19:19:57 +0800468
469 private class CategoryRecyclerViewAccessibilityDelegate
470 extends RecyclerViewAccessibilityDelegate {
471
472 CategoryRecyclerViewAccessibilityDelegate(@NonNull RecyclerView recyclerView) {
473 super(recyclerView);
474 }
475
476 @Override
477 public boolean onRequestSendAccessibilityEvent(
478 ViewGroup host, View child, AccessibilityEvent event) {
479 int itemPos;
480 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
481 itemPos = mImageGrid.getChildLayoutPosition(child);
482
483 // Expand the bottom sheet when TB travel to second column.
484 if (getCategorySelectorFragmentHost() != null
485 && (getCategorySelectorFragmentHost()).getBottomSheetState()
486 != BottomSheetBehavior.STATE_EXPANDED && itemPos >= getNumColumns()) {
487 (getCategorySelectorFragmentHost()).expandBottomSheet();
488 }
489 }
490 return super.onRequestSendAccessibilityEvent(host, child, event);
491 }
492 }
Chuck Liao1e0501f2020-02-17 18:20:54 +0800493}