1 /* 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 */ 16 package com.android.wallpaper.picker; 17 18 import static com.android.wallpaper.picker.WallpaperPickerDelegate.PREVIEW_LIVE_WALLPAPER_REQUEST_CODE; 19 import static com.android.wallpaper.picker.WallpaperPickerDelegate.PREVIEW_WALLPAPER_REQUEST_CODE; 20 21 import android.app.Activity; 22 import android.app.AlertDialog; 23 import android.content.Intent; 24 import android.content.res.TypedArray; 25 import android.graphics.Color; 26 import android.graphics.Point; 27 import android.graphics.Rect; 28 import android.net.Uri; 29 import android.os.Bundle; 30 import android.provider.Settings; 31 import android.util.DisplayMetrics; 32 import android.util.Log; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.widget.ImageView; 37 import android.widget.LinearLayout; 38 import android.widget.ProgressBar; 39 import android.widget.TextView; 40 41 import androidx.annotation.NonNull; 42 import androidx.annotation.Nullable; 43 import androidx.cardview.widget.CardView; 44 import androidx.core.content.ContextCompat; 45 import androidx.core.view.AccessibilityDelegateCompat; 46 import androidx.core.view.ViewCompat; 47 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 48 import androidx.fragment.app.Fragment; 49 import androidx.recyclerview.widget.GridLayoutManager; 50 import androidx.recyclerview.widget.RecyclerView; 51 52 import com.android.wallpaper.R; 53 import com.android.wallpaper.asset.Asset; 54 import com.android.wallpaper.effects.EffectsController; 55 import com.android.wallpaper.model.Category; 56 import com.android.wallpaper.model.CategoryProvider; 57 import com.android.wallpaper.model.LiveWallpaperInfo; 58 import com.android.wallpaper.model.WallpaperInfo; 59 import com.android.wallpaper.module.InjectorProvider; 60 import com.android.wallpaper.util.DeepLinkUtils; 61 import com.android.wallpaper.util.DisplayMetricsRetriever; 62 import com.android.wallpaper.util.ResourceUtils; 63 import com.android.wallpaper.util.SizeCalculator; 64 import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate; 65 import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate.BottomSheetHost; 66 67 import com.bumptech.glide.Glide; 68 import com.google.android.material.snackbar.Snackbar; 69 70 import java.util.ArrayList; 71 import java.util.List; 72 73 /** 74 * Displays the UI which contains the categories of the wallpaper. 75 */ 76 public class CategorySelectorFragment extends AppbarFragment { 77 78 // The number of ViewHolders that don't pertain to category tiles. 79 // Currently 2: one for the metadata section and one for the "Select wallpaper" header. 80 private static final int NUM_NON_CATEGORY_VIEW_HOLDERS = 0; 81 private static final int SETTINGS_APP_INFO_REQUEST_CODE = 1; 82 private static final String TAG = "CategorySelectorFragment"; 83 private static final String IMAGE_WALLPAPER_COLLECTION_ID = "image_wallpapers"; 84 private static final int CREATIVE_CATEGORY_ROW_INDEX = 0; 85 86 /** 87 * Interface to be implemented by an Fragment hosting a {@link CategorySelectorFragment} 88 */ 89 public interface CategorySelectorFragmentHost { 90 91 /** 92 * Requests to show the Android custom photo picker for the sake of picking a photo 93 * to set as the device's wallpaper. 94 */ requestCustomPhotoPicker(MyPhotosStarter.PermissionChangedListener listener)95 void requestCustomPhotoPicker(MyPhotosStarter.PermissionChangedListener listener); 96 97 /** 98 * Shows the wallpaper page of the specific category. 99 * 100 * @param category the wallpaper's {@link Category} 101 */ show(Category category)102 void show(Category category); 103 104 105 /** 106 * Indicates if the host has toolbar to show the title. If it does, we should set the title 107 * there. 108 */ isHostToolbarShown()109 boolean isHostToolbarShown(); 110 111 /** 112 * Sets the title in the host's toolbar. 113 */ setToolbarTitle(CharSequence title)114 void setToolbarTitle(CharSequence title); 115 116 /** 117 * Fetches the wallpaper categories. 118 */ fetchCategories()119 void fetchCategories(); 120 121 /** 122 * Cleans up the listeners which will be notified when there's a package event. 123 */ cleanUp()124 void cleanUp(); 125 } 126 127 private RecyclerView mImageGrid; 128 private CategoryAdapter mAdapter; 129 private GroupedCategoryAdapter mGroupedCategoryAdapter; 130 private CategoryProvider mCategoryProvider; 131 private ArrayList<Category> mCategories = new ArrayList<>(); 132 private Point mTileSizePx; 133 private boolean mAwaitingCategories; 134 private ProgressBar mLoadingIndicator; 135 private ArrayList<Category> mCreativeCategories = new ArrayList<>(); 136 private boolean mIsFeaturedCollectionAvailable; 137 private boolean mIsCreativeCategoryCollectionAvailable; 138 private boolean mIsCreativeWallpaperEnabled = false; 139 140 @Override onCreate(@ullable Bundle savedInstanceState)141 public void onCreate(@Nullable Bundle savedInstanceState) { 142 super.onCreate(savedInstanceState); 143 mCategoryProvider = InjectorProvider.getInjector().getCategoryProvider(requireContext()); 144 mIsCreativeWallpaperEnabled = InjectorProvider.getInjector() 145 .getFlags().isAIWallpaperEnabled(requireContext()); 146 if (mIsCreativeWallpaperEnabled) { 147 mGroupedCategoryAdapter = new GroupedCategoryAdapter(mCategories); 148 } else { 149 mAdapter = new CategoryAdapter(mCategories); 150 } 151 } 152 153 @Nullable 154 @Override onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)155 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 156 @Nullable Bundle savedInstanceState) { 157 View view = inflater.inflate(R.layout.fragment_category_selector, container, 158 /* attachToRoot= */ false); 159 mImageGrid = view.findViewById(R.id.category_grid); 160 mImageGrid.addItemDecoration(new GridPaddingDecoration(getResources().getDimensionPixelSize( 161 R.dimen.grid_item_category_padding_horizontal))); 162 mTileSizePx = SizeCalculator.getCategoryTileSize(getActivity()); 163 // In case CreativeWallpapers are enabled, it means we want to show the new view 164 // in the picker for which we have made a new adaptor 165 if (mIsCreativeWallpaperEnabled) { 166 mImageGrid.setAdapter(mGroupedCategoryAdapter); 167 GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), 168 getNumColumns() 169 * GroupedCategorySpanSizeLookup.DEFAULT_CATEGORY_SPAN_SIZE); 170 gridLayoutManager.setSpanSizeLookup(new 171 GroupedCategorySpanSizeLookup(mGroupedCategoryAdapter)); 172 mImageGrid.setLayoutManager(gridLayoutManager); 173 //TODO (b/290267060): To be fixed when re-factoring of loading categories is done 174 mImageGrid.setItemAnimator(null); 175 } else { 176 mImageGrid.setAdapter(mAdapter); 177 GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), 178 getNumColumns() * CategorySpanSizeLookup.DEFAULT_CATEGORY_SPAN_SIZE); 179 gridLayoutManager.setSpanSizeLookup(new CategorySpanSizeLookup(mAdapter)); 180 mImageGrid.setLayoutManager(gridLayoutManager); 181 } 182 183 mLoadingIndicator = view.findViewById(R.id.loading_indicator); 184 mLoadingIndicator.setVisibility(View.VISIBLE); 185 mImageGrid.setVisibility(View.INVISIBLE); 186 mImageGrid.setAccessibilityDelegateCompat( 187 new WallpaperPickerRecyclerViewAccessibilityDelegate( 188 mImageGrid, (BottomSheetHost) getParentFragment(), getNumColumns())); 189 190 if (getCategorySelectorFragmentHost().isHostToolbarShown()) { 191 view.findViewById(R.id.header_bar).setVisibility(View.GONE); 192 getCategorySelectorFragmentHost().setToolbarTitle(getText(R.string.wallpaper_title)); 193 } else { 194 setUpToolbar(view); 195 setTitle(getText(R.string.wallpaper_title)); 196 } 197 198 if (!DeepLinkUtils.isDeepLink(getActivity().getIntent())) { 199 getCategorySelectorFragmentHost().fetchCategories(); 200 } 201 202 // For nav bar edge-to-edge effect. 203 mImageGrid.setOnApplyWindowInsetsListener((v, windowInsets) -> { 204 v.setPadding( 205 v.getPaddingLeft(), 206 v.getPaddingTop(), 207 v.getPaddingRight(), 208 windowInsets.getSystemWindowInsetBottom()); 209 return windowInsets.consumeSystemWindowInsets(); 210 }); 211 return view; 212 } 213 214 @Override onDestroyView()215 public void onDestroyView() { 216 getCategorySelectorFragmentHost().cleanUp(); 217 super.onDestroyView(); 218 } 219 220 /** 221 * Inserts the given category into the categories list in priority order. 222 */ addCategory(Category category, boolean loading)223 void addCategory(Category category, boolean loading) { 224 // If not previously waiting for categories, enter the waiting state by showing the loading 225 // indicator. 226 if (mIsCreativeWallpaperEnabled) { 227 if (loading && !mAwaitingCategories) { 228 mAwaitingCategories = true; 229 } 230 // Not add existing category to category list 231 if (mCategories.indexOf(category) >= 0) { 232 updateCategory(category); 233 return; 234 } 235 236 int priority = category.getPriority(); 237 if (category.supportsUserCreatedWallpapers()) { 238 mCreativeCategories.add(category); 239 } 240 241 int index = 0; 242 while (index < mCategories.size() && priority >= mCategories.get(index).getPriority()) { 243 index++; 244 } 245 246 mCategories.add(index, category); 247 } else { 248 if (loading && !mAwaitingCategories) { 249 mAdapter.notifyItemChanged(getNumColumns()); 250 mAdapter.notifyItemInserted(getNumColumns()); 251 mAwaitingCategories = true; 252 } 253 // Not add existing category to category list 254 if (mCategories.indexOf(category) >= 0) { 255 updateCategory(category); 256 return; 257 } 258 259 int priority = category.getPriority(); 260 261 int index = 0; 262 while (index < mCategories.size() && priority >= mCategories.get(index).getPriority()) { 263 index++; 264 } 265 266 mCategories.add(index, category); 267 if (mAdapter != null) { 268 // Offset the index because of the static metadata element 269 // at beginning of RecyclerView. 270 mAdapter.notifyItemInserted(index + NUM_NON_CATEGORY_VIEW_HOLDERS); 271 } 272 } 273 } 274 removeCategory(Category category)275 void removeCategory(Category category) { 276 int index = mCategories.indexOf(category); 277 if (index >= 0) { 278 mCategories.remove(index); 279 if (mIsCreativeWallpaperEnabled) { 280 int indexCreativeCategory = mCreativeCategories.indexOf(category); 281 if (indexCreativeCategory >= 0) { 282 mCreativeCategories.remove(indexCreativeCategory); 283 } 284 } else { 285 mAdapter.notifyItemRemoved(index + NUM_NON_CATEGORY_VIEW_HOLDERS); 286 } 287 } 288 } 289 updateCategory(Category category)290 void updateCategory(Category category) { 291 int index = mCategories.indexOf(category); 292 if (index >= 0) { 293 mCategories.set(index, category); 294 if (mIsCreativeWallpaperEnabled) { 295 int indexCreativeCategory = mCreativeCategories.indexOf(category); 296 if (indexCreativeCategory >= 0) { 297 mCreativeCategories.set(indexCreativeCategory, category); 298 } 299 } else { 300 mAdapter.notifyItemChanged(index + NUM_NON_CATEGORY_VIEW_HOLDERS); 301 } 302 } 303 } 304 clearCategories()305 void clearCategories() { 306 mCategories.clear(); 307 if (mIsCreativeWallpaperEnabled) { 308 mCreativeCategories.clear(); 309 mGroupedCategoryAdapter.notifyDataSetChanged(); 310 } else { 311 mAdapter.notifyDataSetChanged(); 312 } 313 } 314 315 /** 316 * Notifies that no further categories are expected. 317 */ doneFetchingCategories()318 void doneFetchingCategories() { 319 notifyDataSetChanged(); 320 mLoadingIndicator.setVisibility(View.INVISIBLE); 321 mImageGrid.setVisibility(View.VISIBLE); 322 mAwaitingCategories = false; 323 mIsFeaturedCollectionAvailable = mCategoryProvider.isFeaturedCollectionAvailable(); 324 mIsCreativeCategoryCollectionAvailable = mCategoryProvider.isCreativeCategoryAvailable(); 325 } 326 notifyDataSetChanged()327 void notifyDataSetChanged() { 328 if (mIsCreativeWallpaperEnabled) { 329 mGroupedCategoryAdapter.notifyDataSetChanged(); 330 } else { 331 mAdapter.notifyDataSetChanged(); 332 } 333 } 334 getNumColumns()335 private int getNumColumns() { 336 Activity activity = getActivity(); 337 return activity == null ? 1 : SizeCalculator.getNumCategoryColumns(activity); 338 } 339 340 getCategorySelectorFragmentHost()341 private CategorySelectorFragmentHost getCategorySelectorFragmentHost() { 342 Fragment parentFragment = getParentFragment(); 343 if (parentFragment != null) { 344 return (CategorySelectorFragmentHost) parentFragment; 345 } else { 346 return (CategorySelectorFragmentHost) getActivity(); 347 } 348 } 349 350 /** 351 * ViewHolder subclass for a category tile in the RecyclerView. 352 */ 353 private class CategoryHolder extends RecyclerView.ViewHolder implements View.OnClickListener { 354 private Category mCategory; 355 private ImageView mImageView; 356 private ImageView mOverlayIconView; 357 private TextView mTitleView; 358 CategoryHolder(View itemView)359 CategoryHolder(View itemView) { 360 super(itemView); 361 itemView.setOnClickListener(this); 362 363 mImageView = itemView.findViewById(R.id.image); 364 mOverlayIconView = itemView.findViewById(R.id.overlay_icon); 365 mTitleView = itemView.findViewById(R.id.category_title); 366 367 CardView categoryView = itemView.findViewById(R.id.category); 368 categoryView.getLayoutParams().height = mTileSizePx.y; 369 categoryView.setRadius(getResources().getDimension(R.dimen.grid_item_all_radius_small)); 370 } 371 372 @Override onClick(View view)373 public void onClick(View view) { 374 Activity activity = getActivity(); 375 376 if (mCategory.supportsCustomPhotos()) { 377 EffectsController effectsController = 378 InjectorProvider.getInjector().getEffectsController(getContext()); 379 if (effectsController != null && !effectsController.isEffectTriggered()) { 380 effectsController.triggerEffect(getContext()); 381 } 382 getCategorySelectorFragmentHost().requestCustomPhotoPicker( 383 new MyPhotosStarter.PermissionChangedListener() { 384 @Override 385 public void onPermissionsGranted() { 386 drawThumbnailAndOverlayIcon(); 387 } 388 389 @Override 390 public void onPermissionsDenied(boolean dontAskAgain) { 391 if (dontAskAgain) { 392 showPermissionSnackbar(); 393 } 394 } 395 }); 396 return; 397 } 398 399 if (mCategory.isSingleWallpaperCategory()) { 400 WallpaperInfo wallpaper = mCategory.getSingleWallpaper(); 401 402 InjectorProvider.getInjector().getWallpaperPersister(activity) 403 .setWallpaperInfoInPreview(wallpaper); 404 wallpaper.showPreview(activity, 405 InjectorProvider.getInjector().getPreviewActivityIntentFactory(), 406 wallpaper instanceof LiveWallpaperInfo ? PREVIEW_LIVE_WALLPAPER_REQUEST_CODE 407 : PREVIEW_WALLPAPER_REQUEST_CODE, true); 408 return; 409 } 410 411 getCategorySelectorFragmentHost().show(mCategory); 412 } 413 414 /** 415 * Binds the given category to this CategoryHolder. 416 */ bindCategory(Category category)417 private void bindCategory(Category category) { 418 mCategory = category; 419 mTitleView.setText(category.getTitle()); 420 drawThumbnailAndOverlayIcon(); 421 } 422 423 /** 424 * Draws the CategoryHolder's thumbnail and overlay icon. 425 */ drawThumbnailAndOverlayIcon()426 private void drawThumbnailAndOverlayIcon() { 427 mOverlayIconView.setImageDrawable(mCategory.getOverlayIcon( 428 getActivity().getApplicationContext())); 429 Asset thumbnail = mCategory.getThumbnail(getActivity().getApplicationContext()); 430 if (thumbnail != null) { 431 // Size the overlay icon according to the category. 432 int overlayIconDimenDp = mCategory.getOverlayIconSizeDp(); 433 DisplayMetrics metrics = DisplayMetricsRetriever.getInstance().getDisplayMetrics( 434 getResources(), getActivity().getWindowManager().getDefaultDisplay()); 435 int overlayIconDimenPx = (int) (overlayIconDimenDp * metrics.density); 436 mOverlayIconView.getLayoutParams().width = overlayIconDimenPx; 437 mOverlayIconView.getLayoutParams().height = overlayIconDimenPx; 438 thumbnail.loadDrawable(getActivity(), mImageView, 439 ResourceUtils.getColorAttr( 440 getActivity(), 441 android.R.attr.colorSecondary 442 )); 443 } else { 444 // TODO(orenb): Replace this workaround for b/62584914 with a proper way of 445 // unloading the ImageView such that no incorrect image is improperly loaded upon 446 // rapid scroll. 447 mImageView.setBackgroundColor( 448 getResources().getColor(R.color.myphoto_background_color)); 449 Object nullObj = null; 450 Glide.with(getActivity()) 451 .asDrawable() 452 .load(nullObj) 453 .into(mImageView); 454 455 } 456 } 457 } 458 showPermissionSnackbar()459 private void showPermissionSnackbar() { 460 Snackbar snackbar = Snackbar.make(getView(), R.string.settings_snackbar_description, 461 Snackbar.LENGTH_LONG); 462 Snackbar.SnackbarLayout layout = (Snackbar.SnackbarLayout) snackbar.getView(); 463 TextView textView = (TextView) layout.findViewById( 464 com.google.android.material.R.id.snackbar_text); 465 layout.setBackgroundResource(R.drawable.snackbar_background); 466 TypedArray typedArray = getContext().obtainStyledAttributes( 467 new int[]{android.R.attr.textColorPrimary, 468 com.android.internal.R.attr.materialColorPrimaryContainer}); 469 textView.setTextColor(typedArray.getColor(0, Color.TRANSPARENT)); 470 snackbar.setActionTextColor(typedArray.getColor(1, Color.TRANSPARENT)); 471 typedArray.recycle(); 472 473 snackbar.setAction(getContext().getString(R.string.settings_snackbar_enable), 474 new View.OnClickListener() { 475 @Override 476 public void onClick(View view) { 477 startSettings(SETTINGS_APP_INFO_REQUEST_CODE); 478 } 479 }); 480 snackbar.show(); 481 } 482 startSettings(int resultCode)483 private void startSettings(int resultCode) { 484 Activity activity = getActivity(); 485 if (activity == null) { 486 return; 487 } 488 Intent appInfoIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 489 Uri uri = Uri.fromParts("package", activity.getPackageName(), /* fragment= */ null); 490 appInfoIntent.setData(uri); 491 startActivityForResult(appInfoIntent, resultCode); 492 } 493 494 /* 495 This is for FeaturedCategories and only present in CategoryAdaptor 496 */ 497 private class FeaturedCategoryHolder extends CategoryHolder { 498 FeaturedCategoryHolder(View itemView)499 FeaturedCategoryHolder(View itemView) { 500 super(itemView); 501 CardView categoryView = itemView.findViewById(R.id.category); 502 categoryView.getLayoutParams().height = 503 SizeCalculator.getFeaturedCategoryTileSize(getActivity()).y; 504 categoryView.setRadius(getResources().getDimension(R.dimen.grid_item_all_radius)); 505 } 506 } 507 508 /* 509 This is re-used between both GroupedCategoryAdaptor and CategoryAdaptor 510 */ 511 private class MyPhotosCategoryHolder extends CategoryHolder { 512 MyPhotosCategoryHolder(View itemView)513 MyPhotosCategoryHolder(View itemView) { 514 super(itemView); 515 // Reuse the height of featured category since My Photos category & featured category 516 // have the same height in current UI design. 517 CardView categoryView = itemView.findViewById(R.id.category); 518 int height = SizeCalculator.getFeaturedCategoryTileSize(getActivity()).y; 519 categoryView.getLayoutParams().height = height; 520 // Use the height as the card corner radius for the "My photos" category 521 // for a stadium border. 522 categoryView.setRadius(height); 523 // We do this since itemView here refers to the broader LinearLayout defined in 524 // the My Photos xml, which includes the section title. Doing this allows us to make 525 // sure that the onClickListener is configured only on the My Photos grid item. 526 if (mIsCreativeWallpaperEnabled) { 527 itemView.setOnClickListener(null); 528 itemView.setClickable(false); 529 itemView.findViewById(R.id.tile).setOnClickListener(this); 530 } 531 } 532 } 533 534 private class GroupCategoryHolder extends RecyclerView.ViewHolder { 535 private static final float INDIVIDUAL_TILE_WEIGHT = 1.0f; 536 LayoutInflater mLayoutInflater = LayoutInflater.from(getActivity()); 537 private ArrayList<Category> mCategories = new ArrayList<>(); 538 private ArrayList<ImageView> mImageViews = new ArrayList<>(); 539 private ArrayList<ImageView> mOverlayIconViews = new ArrayList<>(); 540 private ArrayList<TextView> mTextViews = new ArrayList<>(); 541 GroupCategoryHolder(View itemView, int mCreativeCategoriesSize)542 GroupCategoryHolder(View itemView, int mCreativeCategoriesSize) { 543 super(itemView); 544 LinearLayout linearLayout = itemView.findViewById(R.id.linear_layout_for_cards); 545 for (int i = 0; i < mCreativeCategoriesSize; i++) { 546 LinearLayout gridItemCategory = (LinearLayout) 547 mLayoutInflater.inflate(R.layout.grid_item_category, null); 548 if (gridItemCategory != null) { 549 int position = i; //Used in onClickListener 550 mImageViews.add(gridItemCategory.findViewById(R.id.image)); 551 mOverlayIconViews.add(gridItemCategory.findViewById(R.id.overlay_icon)); 552 mTextViews.add(gridItemCategory.findViewById(R.id.category_title)); 553 setLayoutParams(gridItemCategory); 554 linearLayout.addView(gridItemCategory); 555 gridItemCategory.setOnClickListener(view -> { 556 onClickListenerForCreativeCategory(position); 557 }); 558 // Make sure the column number is announced when there is more than 1 creative 559 // category. 560 if (mCreativeCategoriesSize > 1) { 561 ViewCompat.setAccessibilityDelegate(gridItemCategory, 562 new AccessibilityDelegateCompat() { 563 @Override 564 public void onInitializeAccessibilityNodeInfo(View host, 565 AccessibilityNodeInfoCompat info) { 566 super.onInitializeAccessibilityNodeInfo(host, info); 567 info.setCollectionItemInfo( 568 AccessibilityNodeInfoCompat.CollectionItemInfoCompat 569 .obtain( 570 /* rowIndex= */ 571 CREATIVE_CATEGORY_ROW_INDEX, 572 /* rowSpan= */ 1, 573 /* columnIndex= */ position, 574 /* columnSpan= */ 1, 575 /* heading= */ false)); 576 } 577 }); 578 } 579 } 580 } 581 } 582 onClickListenerForCreativeCategory(int position)583 private void onClickListenerForCreativeCategory(int position) { 584 Activity activity = getActivity(); 585 if (mCategories.get(position).supportsCustomPhotos()) { 586 getCategorySelectorFragmentHost().requestCustomPhotoPicker( 587 new MyPhotosStarter.PermissionChangedListener() { 588 @Override 589 public void onPermissionsGranted() { 590 drawThumbnailAndOverlayIcon( 591 mOverlayIconViews.get(position), 592 mCategories.get(position), 593 mImageViews.get(position)); 594 } 595 596 @Override 597 public void onPermissionsDenied(boolean dontAskAgain) { 598 if (dontAskAgain) { 599 showPermissionSnackbar(); 600 } 601 } 602 }); 603 return; 604 } 605 606 if (mCategories.get(position).isSingleWallpaperCategory()) { 607 WallpaperInfo wallpaper = mCategories.get(position) 608 .getSingleWallpaper(); 609 610 InjectorProvider.getInjector().getWallpaperPersister(activity) 611 .setWallpaperInfoInPreview(wallpaper); 612 wallpaper.showPreview(activity, 613 InjectorProvider.getInjector().getPreviewActivityIntentFactory(), 614 wallpaper instanceof LiveWallpaperInfo 615 ? PREVIEW_LIVE_WALLPAPER_REQUEST_CODE 616 : PREVIEW_WALLPAPER_REQUEST_CODE, true); 617 return; 618 } 619 620 getCategorySelectorFragmentHost().show(mCategories.get(position)); 621 } 622 setLayoutParams(LinearLayout gridItemCategory)623 private void setLayoutParams(LinearLayout gridItemCategory) { 624 LinearLayout.LayoutParams params = 625 (LinearLayout.LayoutParams) gridItemCategory.getLayoutParams(); 626 if (params == null) { 627 params = 628 new LinearLayout.LayoutParams( 629 LinearLayout.LayoutParams.MATCH_PARENT, 630 LinearLayout.LayoutParams.WRAP_CONTENT); 631 } 632 params.setMargins( 633 (int) getResources().getDimension( 634 R.dimen.creative_category_grid_padding_horizontal), 635 (int) getResources().getDimension( 636 R.dimen.creative_category_grid_padding_vertical), 637 (int) getResources().getDimension( 638 R.dimen.creative_category_grid_padding_horizontal), 639 (int) getResources().getDimension( 640 R.dimen.creative_category_grid_padding_vertical)); 641 CardView cardView = gridItemCategory.findViewById(R.id.category); 642 cardView.getLayoutParams().height = SizeCalculator 643 .getFeaturedCategoryTileSize(getActivity()).y / 2; 644 cardView.setRadius(getResources().getDimension(R.dimen.grid_item_all_radius)); 645 params.weight = INDIVIDUAL_TILE_WEIGHT; 646 gridItemCategory.setLayoutParams(params); 647 } 648 drawThumbnailAndOverlayIcon(ImageView mOverlayIconView, Category mCategory, ImageView mImageView)649 private void drawThumbnailAndOverlayIcon(ImageView mOverlayIconView, 650 Category mCategory, ImageView mImageView) { 651 mOverlayIconView.setImageDrawable(mCategory.getOverlayIcon( 652 getActivity().getApplicationContext())); 653 Asset thumbnail = mCategory.getThumbnail(getActivity().getApplicationContext()); 654 if (thumbnail != null) { 655 // Size the overlay icon according to the category. 656 int overlayIconDimenDp = mCategory.getOverlayIconSizeDp(); 657 DisplayMetrics metrics = DisplayMetricsRetriever.getInstance().getDisplayMetrics( 658 getResources(), getActivity().getWindowManager().getDefaultDisplay()); 659 int overlayIconDimenPx = (int) (overlayIconDimenDp * metrics.density); 660 mOverlayIconView.getLayoutParams().width = overlayIconDimenPx; 661 mOverlayIconView.getLayoutParams().height = overlayIconDimenPx; 662 thumbnail.loadDrawable(getActivity(), mImageView, 663 ResourceUtils.getColorAttr( 664 getActivity(), 665 android.R.attr.colorSecondary 666 )); 667 } else { 668 mImageView.setBackgroundColor( 669 getResources().getColor(R.color.myphoto_background_color)); 670 Object nullObj = null; 671 Glide.with(getActivity()) 672 .asDrawable() 673 .load(nullObj) 674 .into(mImageView); 675 } 676 } 677 bindCategory(ArrayList<Category> creativeCategories)678 private void bindCategory(ArrayList<Category> creativeCategories) { 679 for (int i = 0; i < creativeCategories.size(); i++) { 680 mCategories.add(creativeCategories.get(i)); 681 mTextViews.get(i).setText(creativeCategories.get(i).getTitle()); 682 drawThumbnailAndOverlayIcon(mOverlayIconViews.get(i), mCategories.get(i), 683 mImageViews.get(i)); 684 } 685 } 686 } 687 688 /** 689 * RecyclerView Adapter subclass for the category tiles in the RecyclerView. This excludes 690 * CreativeCategory and has FeaturedCategory 691 */ 692 private class CategoryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> 693 implements MyPhotosStarter.PermissionChangedListener { 694 private static final int ITEM_VIEW_TYPE_MY_PHOTOS = 1; 695 private static final int ITEM_VIEW_TYPE_FEATURED_CATEGORY = 2; 696 private static final int ITEM_VIEW_TYPE_CATEGORY = 3; 697 private List<Category> mCategories; 698 CategoryAdapter(List<Category> categories)699 private CategoryAdapter(List<Category> categories) { 700 mCategories = categories; 701 } 702 703 @Override getItemViewType(int position)704 public int getItemViewType(int position) { 705 if (position == 0) { 706 return ITEM_VIEW_TYPE_MY_PHOTOS; 707 } 708 709 if (mIsFeaturedCollectionAvailable && (position == 1 || position == 2)) { 710 return ITEM_VIEW_TYPE_FEATURED_CATEGORY; 711 } 712 713 return ITEM_VIEW_TYPE_CATEGORY; 714 } 715 716 @Override onCreateViewHolder(ViewGroup parent, int viewType)717 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 718 LayoutInflater layoutInflater = LayoutInflater.from(getActivity()); 719 View view; 720 721 switch (viewType) { 722 case ITEM_VIEW_TYPE_MY_PHOTOS: 723 view = layoutInflater.inflate(R.layout.grid_item_category, 724 parent, /* attachToRoot= */ false); 725 return new MyPhotosCategoryHolder(view); 726 case ITEM_VIEW_TYPE_FEATURED_CATEGORY: 727 view = layoutInflater.inflate(R.layout.grid_item_category, 728 parent, /* attachToRoot= */ false); 729 return new FeaturedCategoryHolder(view); 730 case ITEM_VIEW_TYPE_CATEGORY: 731 view = layoutInflater.inflate(R.layout.grid_item_category, 732 parent, /* attachToRoot= */ false); 733 return new CategoryHolder(view); 734 default: 735 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter"); 736 return null; 737 } 738 } 739 740 @Override onBindViewHolder(RecyclerView.ViewHolder holder, int position)741 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { 742 int viewType = getItemViewType(position); 743 switch (viewType) { 744 case ITEM_VIEW_TYPE_MY_PHOTOS: 745 case ITEM_VIEW_TYPE_FEATURED_CATEGORY: 746 case ITEM_VIEW_TYPE_CATEGORY: 747 // Offset position to get category index to account for the non-category view 748 // holders. 749 Category category = mCategories.get(position - NUM_NON_CATEGORY_VIEW_HOLDERS); 750 ((CategoryHolder) holder).bindCategory(category); 751 break; 752 default: 753 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter"); 754 } 755 } 756 757 @Override getItemCount()758 public int getItemCount() { 759 // Add to size of categories to account for the metadata related views. 760 int size = mCategories.size() + NUM_NON_CATEGORY_VIEW_HOLDERS; 761 762 return size; 763 } 764 765 @Override onPermissionsGranted()766 public void onPermissionsGranted() { 767 notifyDataSetChanged(); 768 } 769 770 @Override onPermissionsDenied(boolean dontAskAgain)771 public void onPermissionsDenied(boolean dontAskAgain) { 772 if (!dontAskAgain) { 773 return; 774 } 775 776 String permissionNeededMessage = 777 getString(R.string.permission_needed_explanation_go_to_settings); 778 AlertDialog dialog = new AlertDialog.Builder(getActivity(), R.style.LightDialogTheme) 779 .setMessage(permissionNeededMessage) 780 .setPositiveButton(android.R.string.ok, null /* onClickListener */) 781 .setNegativeButton( 782 R.string.settings_button_label, 783 (dialogInterface, i) -> { 784 startSettings(SETTINGS_APP_INFO_REQUEST_CODE); 785 }) 786 .create(); 787 dialog.show(); 788 } 789 } 790 791 /** 792 * RecyclerView GroupedCategoryAdaptor subclass for the category tiles in the RecyclerView. 793 * This removes FeaturedCategory and adds CreativeCategory with a slightly different layout 794 */ 795 private class GroupedCategoryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> 796 implements MyPhotosStarter.PermissionChangedListener { 797 private static final int ITEM_VIEW_TYPE_MY_PHOTOS = 1; 798 private static final int ITEM_VIEW_TYPE_CREATIVE_CATEGORY = 2; 799 private static final int ITEM_VIEW_TYPE_CATEGORY = 3; 800 private List<Category> mCategories; 801 GroupedCategoryAdapter(List<Category> categories)802 private GroupedCategoryAdapter(List<Category> categories) { 803 mCategories = categories; 804 } 805 806 @Override getItemViewType(int position)807 public int getItemViewType(int position) { 808 if (mCategories.stream().anyMatch(Category::supportsUserCreatedWallpapers)) { 809 if (position == CREATIVE_CATEGORY_ROW_INDEX) { 810 return ITEM_VIEW_TYPE_CREATIVE_CATEGORY; 811 } 812 if (position == 1) { 813 return ITEM_VIEW_TYPE_MY_PHOTOS; 814 } 815 } else { 816 if (position == 0) { 817 return ITEM_VIEW_TYPE_MY_PHOTOS; 818 } 819 } 820 return ITEM_VIEW_TYPE_CATEGORY; 821 } 822 823 @Override onCreateViewHolder(ViewGroup parent, int viewType)824 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 825 LayoutInflater layoutInflater = LayoutInflater.from(getActivity()); 826 827 switch (viewType) { 828 case ITEM_VIEW_TYPE_MY_PHOTOS: 829 View view = layoutInflater.inflate(R.layout.my_photos, 830 parent, /* attachToRoot= */ false); 831 return new MyPhotosCategoryHolder(view); 832 case ITEM_VIEW_TYPE_CREATIVE_CATEGORY: 833 view = layoutInflater.inflate(R.layout.creative_wallpaper, 834 parent, /* attachToRoot= */ false); 835 return new GroupCategoryHolder(view, mCreativeCategories.size()); 836 case ITEM_VIEW_TYPE_CATEGORY: 837 view = layoutInflater.inflate(R.layout.grid_item_category, 838 parent, /* attachToRoot= */ false); 839 return new CategoryHolder(view); 840 default: 841 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter"); 842 return null; 843 } 844 } 845 846 @Override onBindViewHolder(RecyclerView.ViewHolder holder, int position)847 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { 848 int viewType = getItemViewType(position); 849 switch (viewType) { 850 case ITEM_VIEW_TYPE_CREATIVE_CATEGORY: 851 ((GroupCategoryHolder) holder).bindCategory(mCreativeCategories); 852 break; 853 case ITEM_VIEW_TYPE_MY_PHOTOS: 854 holder.setIsRecyclable(false); 855 case ITEM_VIEW_TYPE_CATEGORY: 856 // Offset position to get category index to account for the non-category view 857 // holders. 858 if (mIsCreativeCategoryCollectionAvailable) { 859 int numCreativeCategories = mCreativeCategories.size(); 860 int positionRelativeToCreativeCategory = position + numCreativeCategories 861 - 1; 862 Category category = mCategories.get( 863 positionRelativeToCreativeCategory - NUM_NON_CATEGORY_VIEW_HOLDERS); 864 ((CategoryHolder) holder).bindCategory(category); 865 } else { 866 Category category = mCategories.get(position 867 - NUM_NON_CATEGORY_VIEW_HOLDERS); 868 ((CategoryHolder) holder).bindCategory(category); 869 } 870 break; 871 default: 872 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter"); 873 } 874 } 875 876 @Override getItemCount()877 public int getItemCount() { 878 // Add to size of categories to account for the metadata related views. 879 int size = mCategories.size() + NUM_NON_CATEGORY_VIEW_HOLDERS; 880 // This is done to make sure all CreativeCategories are accounted for 881 // in one single block, therefore subtracted the size of CreativeCategories 882 // from total count 883 if (mCreativeCategories.size() >= 2) { 884 size = size - (mCreativeCategories.size() - 1); 885 } 886 return size; 887 } 888 889 @Override onPermissionsGranted()890 public void onPermissionsGranted() { 891 notifyDataSetChanged(); 892 } 893 894 @Override onPermissionsDenied(boolean dontAskAgain)895 public void onPermissionsDenied(boolean dontAskAgain) { 896 if (!dontAskAgain) { 897 return; 898 } 899 900 String permissionNeededMessage = 901 getString(R.string.permission_needed_explanation_go_to_settings); 902 AlertDialog dialog = new AlertDialog.Builder(getActivity(), R.style.LightDialogTheme) 903 .setMessage(permissionNeededMessage) 904 .setPositiveButton(android.R.string.ok, null /* onClickListener */) 905 .setNegativeButton( 906 R.string.settings_button_label, 907 (dialogInterface, i) -> { 908 startSettings(SETTINGS_APP_INFO_REQUEST_CODE); 909 }) 910 .create(); 911 dialog.show(); 912 } 913 } 914 915 private class GridPaddingDecoration extends RecyclerView.ItemDecoration { 916 917 private final int mPadding; 918 GridPaddingDecoration(int padding)919 GridPaddingDecoration(int padding) { 920 mPadding = padding; 921 } 922 923 @Override getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)924 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 925 RecyclerView.State state) { 926 int position = parent.getChildAdapterPosition(view) - NUM_NON_CATEGORY_VIEW_HOLDERS; 927 if (position >= 0) { 928 outRect.left = mPadding; 929 outRect.right = mPadding; 930 } 931 932 RecyclerView.ViewHolder viewHolder = parent.getChildViewHolder(view); 933 if (viewHolder instanceof MyPhotosCategoryHolder 934 || viewHolder instanceof GroupCategoryHolder 935 || viewHolder instanceof FeaturedCategoryHolder) { 936 outRect.bottom = getResources().getDimensionPixelSize( 937 R.dimen.grid_item_featured_category_padding_bottom); 938 } else { 939 outRect.bottom = getResources().getDimensionPixelSize( 940 R.dimen.grid_item_category_padding_bottom); 941 } 942 } 943 } 944 @Override onActivityResult(int requestCode, int resultCode, Intent data)945 public void onActivityResult(int requestCode, int resultCode, Intent data) { 946 if (requestCode == SETTINGS_APP_INFO_REQUEST_CODE) { 947 notifyDataSetChanged(); 948 } 949 } 950 951 /** 952 * SpanSizeLookup subclass which works with CategoryAdaptor and provides that the item in the 953 * first position spans the number of columns in the RecyclerView and all other items only 954 * take up a single span. 955 */ 956 private class CategorySpanSizeLookup extends GridLayoutManager.SpanSizeLookup { 957 private static final int DEFAULT_CATEGORY_SPAN_SIZE = 2; 958 959 CategoryAdapter mAdapter; 960 CategorySpanSizeLookup(CategoryAdapter adapter)961 private CategorySpanSizeLookup(CategoryAdapter adapter) { 962 mAdapter = adapter; 963 } 964 965 @Override getSpanSize(int position)966 public int getSpanSize(int position) { 967 if (position < NUM_NON_CATEGORY_VIEW_HOLDERS || mAdapter.getItemViewType( 968 position) == CategoryAdapter.ITEM_VIEW_TYPE_MY_PHOTOS) { 969 return getNumColumns() * DEFAULT_CATEGORY_SPAN_SIZE; 970 } 971 972 if (mAdapter.getItemViewType(position) 973 == CategoryAdapter.ITEM_VIEW_TYPE_FEATURED_CATEGORY) { 974 return getNumColumns() * DEFAULT_CATEGORY_SPAN_SIZE / 2; 975 } 976 return DEFAULT_CATEGORY_SPAN_SIZE; 977 } 978 } 979 980 /** 981 * SpanSizeLookup subclass which works with GroupCategoryAdaptor and provides that 982 * item of type photos and items of type CreativeCategory spans the number of columns in the 983 * RecyclerView and all other items only take up a single span. 984 */ 985 private class GroupedCategorySpanSizeLookup extends GridLayoutManager.SpanSizeLookup { 986 private static final int DEFAULT_CATEGORY_SPAN_SIZE = 1; 987 988 GroupedCategoryAdapter mAdapter; 989 GroupedCategorySpanSizeLookup(GroupedCategoryAdapter adapter)990 private GroupedCategorySpanSizeLookup(GroupedCategoryAdapter adapter) { 991 mAdapter = adapter; 992 } 993 994 @Override getSpanSize(int position)995 public int getSpanSize(int position) { 996 if (position < NUM_NON_CATEGORY_VIEW_HOLDERS || mAdapter.getItemViewType( 997 position) == GroupedCategoryAdapter.ITEM_VIEW_TYPE_MY_PHOTOS) { 998 return getNumColumns() * DEFAULT_CATEGORY_SPAN_SIZE; 999 } 1000 1001 if (mAdapter.getItemViewType(position) 1002 == GroupedCategoryAdapter.ITEM_VIEW_TYPE_CREATIVE_CATEGORY) { 1003 return getNumColumns() * DEFAULT_CATEGORY_SPAN_SIZE; 1004 } 1005 return DEFAULT_CATEGORY_SPAN_SIZE; 1006 } 1007 } 1008 1009 @Override getToolbarTextColor()1010 protected int getToolbarTextColor() { 1011 return ContextCompat.getColor(requireContext(), R.color.system_on_surface); 1012 } 1013 } 1014