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.graphics.Point; 25 import android.graphics.PorterDuff; 26 import android.graphics.Rect; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.provider.Settings; 30 import android.util.DisplayMetrics; 31 import android.util.Log; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.widget.ImageView; 36 import android.widget.ProgressBar; 37 import android.widget.TextView; 38 39 import androidx.annotation.NonNull; 40 import androidx.annotation.Nullable; 41 import androidx.cardview.widget.CardView; 42 import androidx.fragment.app.Fragment; 43 import androidx.recyclerview.widget.GridLayoutManager; 44 import androidx.recyclerview.widget.RecyclerView; 45 46 import com.android.wallpaper.R; 47 import com.android.wallpaper.asset.Asset; 48 import com.android.wallpaper.model.Category; 49 import com.android.wallpaper.model.CategoryProvider; 50 import com.android.wallpaper.model.LiveWallpaperInfo; 51 import com.android.wallpaper.model.WallpaperInfo; 52 import com.android.wallpaper.module.InjectorProvider; 53 import com.android.wallpaper.module.UserEventLogger; 54 import com.android.wallpaper.util.DeepLinkUtils; 55 import com.android.wallpaper.util.DisplayMetricsRetriever; 56 import com.android.wallpaper.util.ResourceUtils; 57 import com.android.wallpaper.util.SizeCalculator; 58 import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate; 59 import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate.BottomSheetHost; 60 61 import com.bumptech.glide.Glide; 62 63 import java.util.ArrayList; 64 import java.util.List; 65 66 /** 67 * Displays the UI which contains the categories of the wallpaper. 68 */ 69 public class CategorySelectorFragment extends AppbarFragment { 70 71 // The number of ViewHolders that don't pertain to category tiles. 72 // Currently 2: one for the metadata section and one for the "Select wallpaper" header. 73 private static final int NUM_NON_CATEGORY_VIEW_HOLDERS = 0; 74 private static final int SETTINGS_APP_INFO_REQUEST_CODE = 1; 75 private static final String TAG = "CategorySelectorFragment"; 76 77 /** 78 * Interface to be implemented by an Fragment hosting a {@link CategorySelectorFragment} 79 */ 80 public interface CategorySelectorFragmentHost { 81 82 /** 83 * Requests to show the Android custom photo picker for the sake of picking a photo 84 * to set as the device's wallpaper. 85 */ requestCustomPhotoPicker(MyPhotosStarter.PermissionChangedListener listener)86 void requestCustomPhotoPicker(MyPhotosStarter.PermissionChangedListener listener); 87 88 /** 89 * Shows the wallpaper page of the specific category. 90 * 91 * @param category the wallpaper's {@link Category} 92 */ show(Category category)93 void show(Category category); 94 95 96 /** 97 * Indicates if the host has toolbar to show the title. If it does, we should set the title 98 * there. 99 */ isHostToolbarShown()100 boolean isHostToolbarShown(); 101 102 /** 103 * Sets the title in the host's toolbar. 104 */ setToolbarTitle(CharSequence title)105 void setToolbarTitle(CharSequence title); 106 107 /** 108 * Fetches the wallpaper categories. 109 */ fetchCategories()110 void fetchCategories(); 111 112 /** 113 * Cleans up the listeners which will be notified when there's a package event. 114 */ cleanUp()115 void cleanUp(); 116 } 117 118 private final CategoryProvider mCategoryProvider; 119 120 private RecyclerView mImageGrid; 121 private CategoryAdapter mAdapter; 122 private ArrayList<Category> mCategories = new ArrayList<>(); 123 private Point mTileSizePx; 124 private boolean mAwaitingCategories; 125 private boolean mIsFeaturedCollectionAvailable; 126 CategorySelectorFragment()127 public CategorySelectorFragment() { 128 mAdapter = new CategoryAdapter(mCategories); 129 mCategoryProvider = InjectorProvider.getInjector().getCategoryProvider(getContext()); 130 } 131 132 @Nullable 133 @Override onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)134 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 135 @Nullable Bundle savedInstanceState) { 136 View view = inflater.inflate(R.layout.fragment_category_selector, container, 137 /* attachToRoot= */ false); 138 mImageGrid = view.findViewById(R.id.category_grid); 139 mImageGrid.addItemDecoration(new GridPaddingDecoration(getResources().getDimensionPixelSize( 140 R.dimen.grid_item_category_padding_horizontal))); 141 142 mTileSizePx = SizeCalculator.getCategoryTileSize(getActivity()); 143 144 mImageGrid.setAdapter(mAdapter); 145 146 GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), 147 getNumColumns() * CategorySpanSizeLookup.DEFAULT_CATEGORY_SPAN_SIZE); 148 gridLayoutManager.setSpanSizeLookup(new CategorySpanSizeLookup(mAdapter)); 149 mImageGrid.setLayoutManager(gridLayoutManager); 150 mImageGrid.setAccessibilityDelegateCompat( 151 new WallpaperPickerRecyclerViewAccessibilityDelegate( 152 mImageGrid, (BottomSheetHost) getParentFragment(), getNumColumns())); 153 154 if (getCategorySelectorFragmentHost().isHostToolbarShown()) { 155 view.findViewById(R.id.header_bar).setVisibility(View.GONE); 156 getCategorySelectorFragmentHost().setToolbarTitle(getText(R.string.wallpaper_title)); 157 } else { 158 setUpToolbar(view); 159 setTitle(getText(R.string.wallpaper_title)); 160 } 161 162 if (!DeepLinkUtils.isDeepLink(getActivity().getIntent())) { 163 getCategorySelectorFragmentHost().fetchCategories(); 164 } 165 166 // For nav bar edge-to-edge effect. 167 view.setOnApplyWindowInsetsListener((v, windowInsets) -> { 168 // For status bar height. 169 v.setPadding( 170 v.getPaddingLeft(), 171 windowInsets.getSystemWindowInsetTop(), 172 v.getPaddingRight(), 173 v.getPaddingBottom()); 174 175 View gridView = v.findViewById(R.id.category_grid); 176 gridView.setPadding( 177 gridView.getPaddingLeft(), 178 gridView.getPaddingTop(), 179 gridView.getPaddingRight(), 180 windowInsets.getSystemWindowInsetBottom()); 181 return windowInsets.consumeSystemWindowInsets(); 182 }); 183 return view; 184 } 185 186 @Override onDestroyView()187 public void onDestroyView() { 188 getCategorySelectorFragmentHost().cleanUp(); 189 super.onDestroyView(); 190 } 191 192 /** 193 * Inserts the given category into the categories list in priority order. 194 */ addCategory(Category category, boolean loading)195 void addCategory(Category category, boolean loading) { 196 // If not previously waiting for categories, enter the waiting state by showing the loading 197 // indicator. 198 if (loading && !mAwaitingCategories) { 199 mAdapter.notifyItemChanged(getNumColumns()); 200 mAdapter.notifyItemInserted(getNumColumns()); 201 mAwaitingCategories = true; 202 } 203 // Not add existing category to category list 204 if (mCategories.indexOf(category) >= 0) { 205 updateCategory(category); 206 return; 207 } 208 209 int priority = category.getPriority(); 210 211 int index = 0; 212 while (index < mCategories.size() && priority >= mCategories.get(index).getPriority()) { 213 index++; 214 } 215 216 mCategories.add(index, category); 217 if (mAdapter != null) { 218 // Offset the index because of the static metadata element at beginning of RecyclerView. 219 mAdapter.notifyItemInserted(index + NUM_NON_CATEGORY_VIEW_HOLDERS); 220 } 221 } 222 removeCategory(Category category)223 void removeCategory(Category category) { 224 int index = mCategories.indexOf(category); 225 if (index >= 0) { 226 mCategories.remove(index); 227 mAdapter.notifyItemRemoved(index + NUM_NON_CATEGORY_VIEW_HOLDERS); 228 } 229 } 230 updateCategory(Category category)231 void updateCategory(Category category) { 232 int index = mCategories.indexOf(category); 233 if (index >= 0) { 234 mCategories.remove(index); 235 mCategories.add(index, category); 236 mAdapter.notifyItemChanged(index + NUM_NON_CATEGORY_VIEW_HOLDERS); 237 } 238 } 239 clearCategories()240 void clearCategories() { 241 mCategories.clear(); 242 mAdapter.notifyDataSetChanged(); 243 } 244 245 /** 246 * Notifies the CategoryFragment that no further categories are expected so it may hide 247 * the loading indicator. 248 */ doneFetchingCategories()249 void doneFetchingCategories() { 250 if (mAwaitingCategories) { 251 mAdapter.notifyItemRemoved(mAdapter.getItemCount() - 1); 252 mAwaitingCategories = false; 253 } 254 255 mIsFeaturedCollectionAvailable = mCategoryProvider.isFeaturedCollectionAvailable(); 256 } 257 notifyDataSetChanged()258 void notifyDataSetChanged() { 259 mAdapter.notifyDataSetChanged(); 260 } 261 getNumColumns()262 private int getNumColumns() { 263 Activity activity = getActivity(); 264 return activity == null ? 1 : SizeCalculator.getNumCategoryColumns(activity); 265 } 266 267 getCategorySelectorFragmentHost()268 private CategorySelectorFragmentHost getCategorySelectorFragmentHost() { 269 Fragment parentFragment = getParentFragment(); 270 if (parentFragment != null) { 271 return (CategorySelectorFragmentHost) parentFragment; 272 } else { 273 return (CategorySelectorFragmentHost) getActivity(); 274 } 275 } 276 277 /** 278 * ViewHolder subclass for a category tile in the RecyclerView. 279 */ 280 private class CategoryHolder extends RecyclerView.ViewHolder implements View.OnClickListener { 281 private Category mCategory; 282 private ImageView mImageView; 283 private ImageView mOverlayIconView; 284 private TextView mTitleView; 285 CategoryHolder(View itemView)286 CategoryHolder(View itemView) { 287 super(itemView); 288 itemView.setOnClickListener(this); 289 290 mImageView = itemView.findViewById(R.id.image); 291 mOverlayIconView = itemView.findViewById(R.id.overlay_icon); 292 mTitleView = itemView.findViewById(R.id.category_title); 293 294 CardView categoryView = itemView.findViewById(R.id.category); 295 categoryView.getLayoutParams().height = mTileSizePx.y; 296 categoryView.setRadius(getResources().getDimension(R.dimen.grid_item_all_radius_small)); 297 } 298 299 @Override onClick(View view)300 public void onClick(View view) { 301 Activity activity = getActivity(); 302 final UserEventLogger eventLogger = 303 InjectorProvider.getInjector().getUserEventLogger(activity); 304 eventLogger.logCategorySelected(mCategory.getCollectionId()); 305 306 if (mCategory.supportsCustomPhotos()) { 307 getCategorySelectorFragmentHost().requestCustomPhotoPicker( 308 new MyPhotosStarter.PermissionChangedListener() { 309 @Override 310 public void onPermissionsGranted() { 311 drawThumbnailAndOverlayIcon(); 312 } 313 314 @Override 315 public void onPermissionsDenied(boolean dontAskAgain) { 316 // No-op 317 } 318 }); 319 return; 320 } 321 322 if (mCategory.isSingleWallpaperCategory()) { 323 WallpaperInfo wallpaper = mCategory.getSingleWallpaper(); 324 // Log click on individual wallpaper 325 eventLogger.logIndividualWallpaperSelected(mCategory.getCollectionId()); 326 327 InjectorProvider.getInjector().getWallpaperPersister(activity) 328 .setWallpaperInfoInPreview(wallpaper); 329 wallpaper.showPreview(activity, 330 new PreviewActivity.PreviewActivityIntentFactory(), 331 wallpaper instanceof LiveWallpaperInfo ? PREVIEW_LIVE_WALLPAPER_REQUEST_CODE 332 : PREVIEW_WALLPAPER_REQUEST_CODE); 333 return; 334 } 335 336 getCategorySelectorFragmentHost().show(mCategory); 337 } 338 339 /** 340 * Binds the given category to this CategoryHolder. 341 */ bindCategory(Category category)342 private void bindCategory(Category category) { 343 mCategory = category; 344 mTitleView.setText(category.getTitle()); 345 drawThumbnailAndOverlayIcon(); 346 } 347 348 /** 349 * Draws the CategoryHolder's thumbnail and overlay icon. 350 */ drawThumbnailAndOverlayIcon()351 private void drawThumbnailAndOverlayIcon() { 352 mOverlayIconView.setImageDrawable(mCategory.getOverlayIcon( 353 getActivity().getApplicationContext())); 354 355 // Size the overlay icon according to the category. 356 int overlayIconDimenDp = mCategory.getOverlayIconSizeDp(); 357 DisplayMetrics metrics = DisplayMetricsRetriever.getInstance().getDisplayMetrics( 358 getResources(), getActivity().getWindowManager().getDefaultDisplay()); 359 int overlayIconDimenPx = (int) (overlayIconDimenDp * metrics.density); 360 mOverlayIconView.getLayoutParams().width = overlayIconDimenPx; 361 mOverlayIconView.getLayoutParams().height = overlayIconDimenPx; 362 363 Asset thumbnail = mCategory.getThumbnail(getActivity().getApplicationContext()); 364 if (thumbnail != null) { 365 thumbnail.loadDrawable(getActivity(), mImageView, 366 ResourceUtils.getColorAttr( 367 getActivity(), 368 android.R.attr.colorSecondary 369 )); 370 } else { 371 // TODO(orenb): Replace this workaround for b/62584914 with a proper way of 372 // unloading the ImageView such that no incorrect image is improperly loaded upon 373 // rapid scroll. 374 Object nullObj = null; 375 Glide.with(getActivity()) 376 .asDrawable() 377 .load(nullObj) 378 .into(mImageView); 379 380 } 381 } 382 } 383 384 private class FeaturedCategoryHolder extends CategoryHolder { 385 FeaturedCategoryHolder(View itemView)386 FeaturedCategoryHolder(View itemView) { 387 super(itemView); 388 CardView categoryView = itemView.findViewById(R.id.category); 389 categoryView.getLayoutParams().height = 390 SizeCalculator.getFeaturedCategoryTileSize(getActivity()).y; 391 categoryView.setRadius(getResources().getDimension(R.dimen.grid_item_all_radius)); 392 } 393 } 394 395 private class MyPhotosCategoryHolder extends CategoryHolder { 396 MyPhotosCategoryHolder(View itemView)397 MyPhotosCategoryHolder(View itemView) { 398 super(itemView); 399 // Reuse the height of featured category since My Photos category & featured category 400 // have the same height in current UI design. 401 CardView categoryView = itemView.findViewById(R.id.category); 402 int height = SizeCalculator.getFeaturedCategoryTileSize(getActivity()).y; 403 categoryView.getLayoutParams().height = height; 404 // Use the height as the card corner radius for the "My photos" category 405 // for a stadium border. 406 categoryView.setRadius(height); 407 } 408 } 409 410 /** 411 * ViewHolder subclass for the loading indicator ("spinner") shown when categories are being 412 * fetched. 413 */ 414 private class LoadingIndicatorHolder extends RecyclerView.ViewHolder { LoadingIndicatorHolder(View view)415 private LoadingIndicatorHolder(View view) { 416 super(view); 417 ProgressBar progressBar = view.findViewById(R.id.loading_indicator); 418 progressBar.getIndeterminateDrawable().setColorFilter( 419 ResourceUtils.getColorAttr( 420 getActivity(), 421 android.R.attr.colorAccent 422 ), PorterDuff.Mode.SRC_IN); 423 } 424 } 425 426 /** 427 * RecyclerView Adapter subclass for the category tiles in the RecyclerView. 428 */ 429 private class CategoryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> 430 implements MyPhotosStarter.PermissionChangedListener { 431 private static final int ITEM_VIEW_TYPE_MY_PHOTOS = 1; 432 private static final int ITEM_VIEW_TYPE_FEATURED_CATEGORY = 2; 433 private static final int ITEM_VIEW_TYPE_CATEGORY = 3; 434 private static final int ITEM_VIEW_TYPE_LOADING_INDICATOR = 4; 435 private List<Category> mCategories; 436 CategoryAdapter(List<Category> categories)437 private CategoryAdapter(List<Category> categories) { 438 mCategories = categories; 439 } 440 441 @Override getItemViewType(int position)442 public int getItemViewType(int position) { 443 if (mAwaitingCategories && position == getItemCount() - 1) { 444 return ITEM_VIEW_TYPE_LOADING_INDICATOR; 445 } 446 447 if (position == 0) { 448 return ITEM_VIEW_TYPE_MY_PHOTOS; 449 } 450 451 if (mIsFeaturedCollectionAvailable && (position == 1 || position == 2)) { 452 return ITEM_VIEW_TYPE_FEATURED_CATEGORY; 453 } 454 455 return ITEM_VIEW_TYPE_CATEGORY; 456 } 457 458 @Override onCreateViewHolder(ViewGroup parent, int viewType)459 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 460 LayoutInflater layoutInflater = LayoutInflater.from(getActivity()); 461 View view; 462 463 switch (viewType) { 464 case ITEM_VIEW_TYPE_LOADING_INDICATOR: 465 view = layoutInflater.inflate(R.layout.grid_item_loading_indicator, 466 parent, /* attachToRoot= */ false); 467 return new LoadingIndicatorHolder(view); 468 case ITEM_VIEW_TYPE_MY_PHOTOS: 469 view = layoutInflater.inflate(R.layout.grid_item_category, 470 parent, /* attachToRoot= */ false); 471 return new MyPhotosCategoryHolder(view); 472 case ITEM_VIEW_TYPE_FEATURED_CATEGORY: 473 view = layoutInflater.inflate(R.layout.grid_item_category, 474 parent, /* attachToRoot= */ false); 475 return new FeaturedCategoryHolder(view); 476 case ITEM_VIEW_TYPE_CATEGORY: 477 view = layoutInflater.inflate(R.layout.grid_item_category, 478 parent, /* attachToRoot= */ false); 479 return new CategoryHolder(view); 480 default: 481 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter"); 482 return null; 483 } 484 } 485 486 @Override onBindViewHolder(RecyclerView.ViewHolder holder, int position)487 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { 488 int viewType = getItemViewType(position); 489 490 switch (viewType) { 491 case ITEM_VIEW_TYPE_MY_PHOTOS: 492 case ITEM_VIEW_TYPE_FEATURED_CATEGORY: 493 case ITEM_VIEW_TYPE_CATEGORY: 494 // Offset position to get category index to account for the non-category view 495 // holders. 496 Category category = mCategories.get(position - NUM_NON_CATEGORY_VIEW_HOLDERS); 497 ((CategoryHolder) holder).bindCategory(category); 498 break; 499 case ITEM_VIEW_TYPE_LOADING_INDICATOR: 500 // No op. 501 break; 502 default: 503 Log.e(TAG, "Unsupported viewType " + viewType + " in CategoryAdapter"); 504 } 505 } 506 507 @Override getItemCount()508 public int getItemCount() { 509 // Add to size of categories to account for the metadata related views. 510 // Add 1 more for the loading indicator if not yet done loading. 511 int size = mCategories.size() + NUM_NON_CATEGORY_VIEW_HOLDERS; 512 if (mAwaitingCategories) { 513 size += 1; 514 } 515 516 return size; 517 } 518 519 @Override onPermissionsGranted()520 public void onPermissionsGranted() { 521 notifyDataSetChanged(); 522 } 523 524 @Override onPermissionsDenied(boolean dontAskAgain)525 public void onPermissionsDenied(boolean dontAskAgain) { 526 if (!dontAskAgain) { 527 return; 528 } 529 530 String permissionNeededMessage = 531 getString(R.string.permission_needed_explanation_go_to_settings); 532 AlertDialog dialog = new AlertDialog.Builder(getActivity(), R.style.LightDialogTheme) 533 .setMessage(permissionNeededMessage) 534 .setPositiveButton(android.R.string.ok, null /* onClickListener */) 535 .setNegativeButton( 536 R.string.settings_button_label, 537 (dialogInterface, i) -> { 538 Intent appInfoIntent = 539 new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 540 Uri uri = Uri.fromParts("package", 541 getActivity().getPackageName(), /* fragment= */ null); 542 appInfoIntent.setData(uri); 543 startActivityForResult( 544 appInfoIntent, SETTINGS_APP_INFO_REQUEST_CODE); 545 }) 546 .create(); 547 dialog.show(); 548 } 549 } 550 551 private class GridPaddingDecoration extends RecyclerView.ItemDecoration { 552 553 private final int mPadding; 554 GridPaddingDecoration(int padding)555 GridPaddingDecoration(int padding) { 556 mPadding = padding; 557 } 558 559 @Override getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)560 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 561 RecyclerView.State state) { 562 int position = parent.getChildAdapterPosition(view) - NUM_NON_CATEGORY_VIEW_HOLDERS; 563 if (position >= 0) { 564 outRect.left = mPadding; 565 outRect.right = mPadding; 566 } 567 568 RecyclerView.ViewHolder viewHolder = parent.getChildViewHolder(view); 569 if (viewHolder instanceof MyPhotosCategoryHolder 570 || viewHolder instanceof FeaturedCategoryHolder) { 571 outRect.bottom = getResources().getDimensionPixelSize( 572 R.dimen.grid_item_featured_category_padding_bottom); 573 } else { 574 outRect.bottom = getResources().getDimensionPixelSize( 575 R.dimen.grid_item_category_padding_bottom); 576 } 577 } 578 } 579 580 /** 581 * SpanSizeLookup subclass which provides that the item in the first position spans the number 582 * of columns in the RecyclerView and all other items only take up a single span. 583 */ 584 private class CategorySpanSizeLookup extends GridLayoutManager.SpanSizeLookup { 585 private static final int DEFAULT_CATEGORY_SPAN_SIZE = 2; 586 587 CategoryAdapter mAdapter; 588 CategorySpanSizeLookup(CategoryAdapter adapter)589 private CategorySpanSizeLookup(CategoryAdapter adapter) { 590 mAdapter = adapter; 591 } 592 593 @Override getSpanSize(int position)594 public int getSpanSize(int position) { 595 if (position < NUM_NON_CATEGORY_VIEW_HOLDERS || mAdapter.getItemViewType(position) 596 == CategoryAdapter.ITEM_VIEW_TYPE_LOADING_INDICATOR || mAdapter.getItemViewType( 597 position) == CategoryAdapter.ITEM_VIEW_TYPE_MY_PHOTOS) { 598 return getNumColumns() * DEFAULT_CATEGORY_SPAN_SIZE; 599 } 600 601 if (mAdapter.getItemViewType(position) 602 == CategoryAdapter.ITEM_VIEW_TYPE_FEATURED_CATEGORY) { 603 return getNumColumns() * DEFAULT_CATEGORY_SPAN_SIZE / 2; 604 } 605 606 return DEFAULT_CATEGORY_SPAN_SIZE; 607 } 608 } 609 } 610