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