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