1 /* <lambda>null2 * Copyright (C) 2022 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.individual 17 18 import CreativeCategoryHolder 19 import android.app.Activity 20 import android.app.ProgressDialog 21 import android.app.WallpaperManager 22 import android.content.DialogInterface 23 import android.content.res.Configuration 24 import android.content.res.Resources 25 import android.content.res.Resources.ID_NULL 26 import android.graphics.Point 27 import android.os.Build 28 import android.os.Build.VERSION_CODES 29 import android.os.Bundle 30 import android.service.wallpaper.WallpaperService 31 import android.text.TextUtils 32 import android.util.ArraySet 33 import android.util.Log 34 import android.view.LayoutInflater 35 import android.view.MenuItem 36 import android.view.View 37 import android.view.ViewGroup 38 import android.view.WindowInsets 39 import android.widget.ImageView 40 import android.widget.RelativeLayout 41 import android.widget.TextView 42 import android.widget.Toast 43 import androidx.annotation.DrawableRes 44 import androidx.cardview.widget.CardView 45 import androidx.core.content.ContextCompat 46 import androidx.core.widget.ContentLoadingProgressBar 47 import androidx.fragment.app.DialogFragment 48 import androidx.lifecycle.lifecycleScope 49 import androidx.recyclerview.widget.GridLayoutManager 50 import androidx.recyclerview.widget.RecyclerView 51 import com.android.wallpaper.R 52 import com.android.wallpaper.model.Category 53 import com.android.wallpaper.model.CategoryProvider 54 import com.android.wallpaper.model.CategoryReceiver 55 import com.android.wallpaper.model.LiveWallpaperInfo 56 import com.android.wallpaper.model.WallpaperCategory 57 import com.android.wallpaper.model.WallpaperInfo 58 import com.android.wallpaper.model.WallpaperRotationInitializer 59 import com.android.wallpaper.model.WallpaperRotationInitializer.NetworkPreference 60 import com.android.wallpaper.module.InjectorProvider 61 import com.android.wallpaper.module.PackageStatusNotifier 62 import com.android.wallpaper.picker.AppbarFragment 63 import com.android.wallpaper.picker.FragmentTransactionChecker 64 import com.android.wallpaper.picker.MyPhotosStarter.MyPhotosStarterProvider 65 import com.android.wallpaper.picker.RotationStarter 66 import com.android.wallpaper.picker.StartRotationDialogFragment 67 import com.android.wallpaper.picker.StartRotationErrorDialogFragment 68 import com.android.wallpaper.util.ActivityUtils 69 import com.android.wallpaper.util.DiskBasedLogger 70 import com.android.wallpaper.util.LaunchUtils 71 import com.android.wallpaper.util.SizeCalculator 72 import com.android.wallpaper.widget.GridPaddingDecoration 73 import com.android.wallpaper.widget.GridPaddingDecorationCreativeCategory 74 import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate 75 import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate.BottomSheetHost 76 import com.bumptech.glide.Glide 77 import com.bumptech.glide.MemoryCategory 78 import java.util.Date 79 import kotlinx.coroutines.coroutineScope 80 import kotlinx.coroutines.launch 81 82 /** Displays the Main UI for picking an individual wallpaper image. */ 83 class IndividualPickerFragment2 : 84 AppbarFragment(), 85 RotationStarter, 86 StartRotationErrorDialogFragment.Listener, 87 StartRotationDialogFragment.Listener { 88 89 companion object { 90 private const val TAG = "IndividualPickerFrag2" 91 92 /** 93 * Position of a special tile that doesn't belong to an individual wallpaper of the 94 * category, such as "my photos" or "daily rotation". 95 */ 96 private const val SPECIAL_FIXED_TILE_ADAPTER_POSITION = 0 97 98 private const val ARG_CATEGORY_COLLECTION_ID = "category_collection_id" 99 100 private const val UNUSED_REQUEST_CODE = 1 101 private const val TAG_START_ROTATION_DIALOG = "start_rotation_dialog" 102 private const val TAG_START_ROTATION_ERROR_DIALOG = "start_rotation_error_dialog" 103 private const val PROGRESS_DIALOG_INDETERMINATE = true 104 private const val KEY_NIGHT_MODE = "IndividualPickerFragment.NIGHT_MODE" 105 private const val MAX_CAPACITY_IN_FEWER_COLUMN_LAYOUT = 8 106 private val PROGRESS_DIALOG_NO_TITLE = null 107 private var isCreativeCategory = false 108 109 fun newInstance(collectionId: String?): IndividualPickerFragment2 { 110 val args = Bundle() 111 args.putString(ARG_CATEGORY_COLLECTION_ID, collectionId) 112 val fragment = IndividualPickerFragment2() 113 fragment.arguments = args 114 return fragment 115 } 116 } 117 118 private lateinit var imageGrid: RecyclerView 119 private var adapter: IndividualAdapter? = null 120 private var category: WallpaperCategory? = null 121 private var wallpaperRotationInitializer: WallpaperRotationInitializer? = null 122 private lateinit var items: MutableList<PickerItem> 123 private var packageStatusNotifier: PackageStatusNotifier? = null 124 private var isWallpapersReceived = false 125 126 private var appStatusListener: PackageStatusNotifier.Listener? = null 127 private var progressDialog: ProgressDialog? = null 128 129 private var loading: ContentLoadingProgressBar? = null 130 private var shouldReloadWallpapers = false 131 private lateinit var categoryProvider: CategoryProvider 132 private var appliedWallpaperIds: Set<String> = setOf() 133 private var mIsCreativeWallpaperEnabled = false 134 135 /** 136 * Staged error dialog fragments that were unable to be shown when the activity didn't allow 137 * committing fragment transactions. 138 */ 139 private var stagedStartRotationErrorDialogFragment: StartRotationErrorDialogFragment? = null 140 141 private var wallpaperManager: WallpaperManager? = null 142 143 override fun onCreate(savedInstanceState: Bundle?) { 144 super.onCreate(savedInstanceState) 145 val injector = InjectorProvider.getInjector() 146 val appContext = requireContext().applicationContext 147 mIsCreativeWallpaperEnabled = injector.getFlags().isAIWallpaperEnabled(appContext) 148 wallpaperManager = WallpaperManager.getInstance(appContext) 149 packageStatusNotifier = injector.getPackageStatusNotifier(appContext) 150 items = ArrayList() 151 152 // Clear Glide's cache if night-mode changed to ensure thumbnails are reloaded 153 if ( 154 savedInstanceState != null && 155 (savedInstanceState.getInt(KEY_NIGHT_MODE) != 156 resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) 157 ) { 158 Glide.get(requireContext()).clearMemory() 159 } 160 categoryProvider = injector.getCategoryProvider(appContext) 161 categoryProvider.fetchCategories( 162 object : CategoryReceiver { 163 override fun onCategoryReceived(category: Category) { 164 // Do nothing. 165 } 166 167 override fun doneFetchingCategories() { 168 val fetchedCategory = 169 categoryProvider.getCategory( 170 arguments?.getString(ARG_CATEGORY_COLLECTION_ID) 171 ) 172 if (fetchedCategory != null && fetchedCategory !is WallpaperCategory) { 173 return 174 } 175 176 if (fetchedCategory == null) { 177 DiskBasedLogger.e(TAG, "Failed to find the category.", context) 178 179 // The absence of this category in the CategoryProvider indicates a broken 180 // state, see b/38030129. Hence, finish the activity and return. 181 getIndividualPickerFragmentHost().moveToPreviousFragment() 182 Toast.makeText( 183 context, 184 R.string.collection_not_exist_msg, 185 Toast.LENGTH_SHORT 186 ) 187 .show() 188 return 189 } 190 category = fetchedCategory as WallpaperCategory 191 category?.let { onCategoryLoaded(it) } 192 } 193 }, 194 false 195 ) 196 } 197 198 fun onCategoryLoaded(category: Category) { 199 val fragmentHost = getIndividualPickerFragmentHost() 200 if (fragmentHost.isHostToolbarShown) { 201 fragmentHost.setToolbarTitle(category.title) 202 } else { 203 setTitle(category.title) 204 } 205 wallpaperRotationInitializer = category.wallpaperRotationInitializer 206 if (mToolbar != null && isRotationEnabled()) { 207 setUpToolbarMenu(R.menu.individual_picker_menu) 208 } 209 var shouldForceReload = false 210 if (category.supportsThirdParty()) { 211 shouldForceReload = true 212 } 213 fetchWallpapers(shouldForceReload) 214 registerPackageListener(category) 215 } 216 217 private fun fetchWallpapers(forceReload: Boolean) { 218 isCreativeCategory = false 219 items.clear() 220 isWallpapersReceived = false 221 updateLoading() 222 val context = requireContext() 223 val userCreatedWallpapers = mutableListOf<WallpaperInfo>() 224 225 category?.fetchWallpapers( 226 context.applicationContext, 227 { fetchedWallpapers -> 228 if (getContext() == null) { 229 Log.w(TAG, "Null context!!") 230 return@fetchWallpapers 231 } 232 isWallpapersReceived = true 233 updateLoading() 234 val supportsUserCreated = category?.supportsUserCreatedWallpapers() == true 235 val byGroup = fetchedWallpapers.groupBy { it.getGroupName(context) }.toMutableMap() 236 val appliedWallpaperIds = 237 getAppliedWallpaperIds().also { this.appliedWallpaperIds = it } 238 val firstEntry = byGroup.keys.firstOrNull() 239 val currentWallpaper: android.app.WallpaperInfo? = 240 WallpaperManager.getInstance(context).wallpaperInfo 241 242 // Handle first group (templates/items that allow to create a new wallpaper) 243 if (mIsCreativeWallpaperEnabled && firstEntry != null && supportsUserCreated) { 244 val wallpapers = byGroup.getValue(firstEntry) 245 isCreativeCategory = true 246 247 if (wallpapers.size > 1 && !TextUtils.isEmpty(firstEntry)) { 248 addItemHeader(firstEntry, items.isEmpty()) 249 addTemplates(wallpapers, userCreatedWallpapers) 250 byGroup.remove(firstEntry) 251 } 252 } 253 254 // Handle other groups 255 if (byGroup.isNotEmpty()) { 256 byGroup.forEach { (groupName, wallpapers) -> 257 if (!TextUtils.isEmpty(groupName)) { 258 addItemHeader(groupName, items.isEmpty()) 259 } 260 addWallpaperItems(wallpapers, currentWallpaper, appliedWallpaperIds) 261 } 262 } 263 maybeSetUpImageGrid() 264 adapter?.notifyDataSetChanged() 265 266 // Finish activity if no wallpapers are found (on phone) 267 if (fetchedWallpapers.isEmpty()) { 268 activity?.finish() 269 } 270 }, 271 forceReload 272 ) 273 } 274 275 // Add item header based on whether it's the first one or not 276 private fun addItemHeader(groupName: String, isFirst: Boolean) { 277 items.add( 278 if (isFirst) { 279 PickerItem.FirstHeaderItem(groupName) 280 } else { 281 PickerItem.HeaderItem(groupName) 282 } 283 ) 284 } 285 286 /** 287 * This function iterates through a set of templates, which represent items that users can 288 * select to create new wallpapers. For each template, it creates a PickerItem of type 289 * CreativeCollection. 290 */ 291 private fun addTemplates( 292 wallpapers: List<WallpaperInfo>, 293 userCreatedWallpapers: MutableList<WallpaperInfo> 294 ) { 295 wallpapers.map { 296 if (category?.supportsUserCreatedWallpapers() == true) { 297 userCreatedWallpapers.add(it) 298 } 299 } 300 301 if (userCreatedWallpapers.isNotEmpty()) { 302 items.add(PickerItem.CreativeCollection(userCreatedWallpapers)) 303 } 304 } 305 306 /** 307 * This function iterates through a set of wallpaper items, and creates a PickerItem of type 308 * WallpaperItem 309 */ 310 private fun addWallpaperItems( 311 wallpapers: List<WallpaperInfo>, 312 currentWallpaper: android.app.WallpaperInfo?, 313 appliedWallpaperIds: Set<String> 314 ) { 315 items.addAll( 316 wallpapers.map { 317 val isApplied = 318 if (it is LiveWallpaperInfo) it.isApplied(currentWallpaper) 319 else appliedWallpaperIds.contains(it.wallpaperId) 320 PickerItem.WallpaperItem(it, isApplied) 321 } 322 ) 323 } 324 325 private fun registerPackageListener(category: Category) { 326 if (category.supportsThirdParty()) { 327 appStatusListener = 328 PackageStatusNotifier.Listener { pkgName: String?, status: Int -> 329 if ( 330 status != PackageStatusNotifier.PackageStatus.REMOVED || 331 category.containsThirdParty(pkgName) 332 ) { 333 fetchWallpapers(true) 334 } 335 } 336 packageStatusNotifier?.addListener( 337 appStatusListener, 338 WallpaperService.SERVICE_INTERFACE 339 ) 340 } 341 } 342 343 private fun updateLoading() { 344 if (isWallpapersReceived) { 345 loading?.hide() 346 } else { 347 loading?.show() 348 } 349 } 350 351 override fun onSaveInstanceState(outState: Bundle) { 352 super.onSaveInstanceState(outState) 353 outState.putInt( 354 KEY_NIGHT_MODE, 355 resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK 356 ) 357 } 358 359 override fun onCreateView( 360 inflater: LayoutInflater, 361 container: ViewGroup?, 362 savedInstanceState: Bundle? 363 ): View { 364 val view: View = inflater.inflate(R.layout.fragment_individual_picker, container, false) 365 if (getIndividualPickerFragmentHost().isHostToolbarShown) { 366 view.findViewById<View>(R.id.header_bar).visibility = View.GONE 367 setUpArrowEnabled(/* upArrow= */ true) 368 if (isRotationEnabled()) { 369 getIndividualPickerFragmentHost().setToolbarMenu(R.menu.individual_picker_menu) 370 } 371 } else { 372 setUpToolbar(view) 373 if (isRotationEnabled()) { 374 setUpToolbarMenu(R.menu.individual_picker_menu) 375 } 376 setTitle(category?.title) 377 } 378 imageGrid = view.findViewById<View>(R.id.wallpaper_grid) as RecyclerView 379 loading = view.findViewById(R.id.loading_indicator) 380 updateLoading() 381 maybeSetUpImageGrid() 382 // For nav bar edge-to-edge effect. 383 imageGrid.setOnApplyWindowInsetsListener { v: View, windowInsets: WindowInsets -> 384 v.setPadding( 385 v.paddingLeft, 386 v.paddingTop, 387 v.paddingRight, 388 windowInsets.systemWindowInsetBottom 389 ) 390 windowInsets.consumeSystemWindowInsets() 391 } 392 return view 393 } 394 395 private fun getIndividualPickerFragmentHost(): 396 IndividualPickerFragment.IndividualPickerFragmentHost { 397 val parentFragment = parentFragment 398 return if (parentFragment != null) { 399 parentFragment as IndividualPickerFragment.IndividualPickerFragmentHost 400 } else { 401 activity as IndividualPickerFragment.IndividualPickerFragmentHost 402 } 403 } 404 405 private fun maybeSetUpImageGrid() { 406 // Skip if mImageGrid been initialized yet 407 if (!this::imageGrid.isInitialized) { 408 return 409 } 410 // Skip if category hasn't loaded yet 411 if (category == null) { 412 return 413 } 414 if (context == null) { 415 return 416 } 417 418 // Wallpaper count could change, so we may need to change the layout(2 or 3 columns layout) 419 val gridLayoutManager = imageGrid.layoutManager as GridLayoutManager? 420 val needUpdateLayout = gridLayoutManager?.spanCount != getNumColumns() 421 422 // Skip if the adapter was already created and don't need to change the layout 423 if (adapter != null && !needUpdateLayout) { 424 return 425 } 426 427 // Clear the old decoration 428 val decorationCount = imageGrid.itemDecorationCount 429 for (i in 0 until decorationCount) { 430 imageGrid.removeItemDecorationAt(i) 431 } 432 val edgePadding = getEdgePadding() 433 434 if (isCreativeCategory) { 435 imageGrid.addItemDecoration( 436 GridPaddingDecorationCreativeCategory( 437 getGridItemPaddingHorizontal(), 438 getGridItemPaddingBottom(), 439 edgePadding 440 ) 441 ) 442 } else { 443 imageGrid.addItemDecoration( 444 GridPaddingDecoration(getGridItemPaddingHorizontal(), getGridItemPaddingBottom()) 445 ) 446 imageGrid.setPadding( 447 edgePadding, 448 imageGrid.paddingTop, 449 edgePadding, 450 imageGrid.paddingBottom 451 ) 452 } 453 454 val tileSizePx = 455 if (isFewerColumnLayout()) { 456 SizeCalculator.getFeaturedIndividualTileSize(requireActivity()) 457 } else { 458 SizeCalculator.getIndividualTileSize(requireActivity()) 459 } 460 setUpImageGrid(tileSizePx, checkNotNull(category)) 461 imageGrid.setAccessibilityDelegateCompat( 462 WallpaperPickerRecyclerViewAccessibilityDelegate( 463 imageGrid, 464 parentFragment as BottomSheetHost?, 465 getNumColumns() 466 ) 467 ) 468 } 469 470 private fun isFewerColumnLayout(): Boolean = 471 (!mIsCreativeWallpaperEnabled || category?.supportsUserCreatedWallpapers() == false) && 472 items.count { it is PickerItem.WallpaperItem } <= MAX_CAPACITY_IN_FEWER_COLUMN_LAYOUT 473 474 private fun getGridItemPaddingHorizontal(): Int { 475 return if (isFewerColumnLayout()) { 476 resources.getDimensionPixelSize( 477 R.dimen.grid_item_featured_individual_padding_horizontal 478 ) 479 } else { 480 resources.getDimensionPixelSize(R.dimen.grid_item_individual_padding_horizontal) 481 } 482 } 483 484 private fun getGridItemPaddingBottom(): Int { 485 return if (isFewerColumnLayout()) { 486 resources.getDimensionPixelSize(R.dimen.grid_item_featured_individual_padding_bottom) 487 } else { 488 resources.getDimensionPixelSize(R.dimen.grid_item_individual_padding_bottom) 489 } 490 } 491 492 private fun getEdgePadding(): Int { 493 return if (isFewerColumnLayout()) { 494 resources.getDimensionPixelSize(R.dimen.featured_wallpaper_grid_edge_space) 495 } else { 496 resources.getDimensionPixelSize(R.dimen.wallpaper_grid_edge_space) 497 } 498 } 499 500 /** 501 * Create the adapter and assign it to mImageGrid. Both mImageGrid and mCategory are guaranteed 502 * to not be null when this method is called. 503 */ 504 private fun setUpImageGrid(tileSizePx: Point, category: Category) { 505 adapter = 506 IndividualAdapter( 507 items, 508 category, 509 requireActivity(), 510 tileSizePx, 511 isRotationEnabled(), 512 isFewerColumnLayout(), 513 getEdgePadding(), 514 imageGrid.paddingTop, 515 imageGrid.paddingBottom 516 ) 517 imageGrid.adapter = adapter 518 val gridLayoutManager = GridLayoutManager(activity, getNumColumns()) 519 gridLayoutManager.spanSizeLookup = 520 object : GridLayoutManager.SpanSizeLookup() { 521 override fun getSpanSize(position: Int): Int { 522 return if (position >= 0 && position < items.size) { 523 when (items[position]) { 524 is PickerItem.CreativeCollection, 525 is PickerItem.FirstHeaderItem, 526 is PickerItem.HeaderItem -> gridLayoutManager.spanCount 527 else -> 1 528 } 529 } else { 530 1 531 } 532 } 533 } 534 imageGrid.layoutManager = gridLayoutManager 535 } 536 537 private suspend fun fetchWallpapersIfNeeded() { 538 coroutineScope { 539 if (isWallpapersReceived && (shouldReloadWallpapers || isAppliedWallpaperChanged())) { 540 fetchWallpapers(true) 541 } 542 } 543 } 544 545 override fun onResume() { 546 super.onResume() 547 val preferences = InjectorProvider.getInjector().getPreferences(requireActivity()) 548 preferences.lastAppActiveTimestamp = Date().time 549 550 // Reset Glide memory settings to a "normal" level of usage since it may have been lowered 551 // in PreviewFragment. 552 Glide.get(requireContext()).setMemoryCategory(MemoryCategory.NORMAL) 553 554 // Show the staged 'start rotation' error dialog fragment if there is one that was unable to 555 // be shown earlier when this fragment's hosting activity didn't allow committing fragment 556 // transactions. 557 if (isAdded) { 558 stagedStartRotationErrorDialogFragment?.show( 559 parentFragmentManager, 560 TAG_START_ROTATION_ERROR_DIALOG 561 ) 562 lifecycleScope.launch { fetchWallpapersIfNeeded() } 563 } 564 stagedStartRotationErrorDialogFragment = null 565 } 566 567 override fun onPause() { 568 shouldReloadWallpapers = category?.supportsWallpaperSetUpdates() ?: false 569 super.onPause() 570 } 571 572 override fun onDestroyView() { 573 super.onDestroyView() 574 getIndividualPickerFragmentHost().removeToolbarMenu() 575 } 576 577 override fun onDestroy() { 578 super.onDestroy() 579 progressDialog?.dismiss() 580 if (appStatusListener != null) { 581 packageStatusNotifier?.removeListener(appStatusListener) 582 } 583 } 584 585 override fun onStartRotationDialogDismiss(dialog: DialogInterface) { 586 // TODO(b/159310028): Refactor fragment layer to make it able to restore from config change. 587 // This is to handle config change with StartRotationDialog popup, the StartRotationDialog 588 // still holds a reference to the destroyed Fragment and is calling 589 // onStartRotationDialogDismissed on that destroyed Fragment. 590 } 591 592 override fun retryStartRotation(@NetworkPreference networkPreference: Int) { 593 startRotation(networkPreference) 594 } 595 596 override fun startRotation(@NetworkPreference networkPreference: Int) { 597 if (!isRotationEnabled()) { 598 Log.e(TAG, "Rotation is not enabled for this category " + category?.title) 599 return 600 } 601 602 val themeResId = 603 if (Build.VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) { 604 R.style.ProgressDialogThemePreL 605 } else { 606 R.style.LightDialogTheme 607 } 608 val progressDialog = ProgressDialog(activity, themeResId) 609 progressDialog.setTitle(PROGRESS_DIALOG_NO_TITLE) 610 progressDialog.setMessage(resources.getString(R.string.start_rotation_progress_message)) 611 progressDialog.isIndeterminate = PROGRESS_DIALOG_INDETERMINATE 612 progressDialog.show() 613 this.progressDialog = progressDialog 614 615 val appContext = requireActivity().applicationContext 616 wallpaperRotationInitializer?.setFirstWallpaperInRotation( 617 appContext, 618 networkPreference, 619 object : WallpaperRotationInitializer.Listener { 620 override fun onFirstWallpaperInRotationSet() { 621 progressDialog?.dismiss() 622 623 // The fragment may be detached from its containing activity if the user exits 624 // the app before the first wallpaper image in rotation finishes downloading. 625 val activity: Activity? = activity 626 if (wallpaperRotationInitializer!!.startRotation(appContext)) { 627 if (activity != null) { 628 try { 629 Toast.makeText( 630 activity, 631 R.string.wallpaper_set_successfully_message, 632 Toast.LENGTH_SHORT 633 ) 634 .show() 635 } catch (e: Resources.NotFoundException) { 636 Log.e(TAG, "Could not show toast $e") 637 } 638 activity.setResult(Activity.RESULT_OK) 639 activity.finish() 640 if (!ActivityUtils.isSUWMode(appContext)) { 641 // Go back to launcher home. 642 LaunchUtils.launchHome(appContext) 643 } 644 } 645 } else { // Failed to start rotation. 646 showStartRotationErrorDialog(networkPreference) 647 } 648 } 649 650 override fun onError() { 651 progressDialog?.dismiss() 652 showStartRotationErrorDialog(networkPreference) 653 } 654 } 655 ) 656 } 657 658 private fun showStartRotationErrorDialog(@NetworkPreference networkPreference: Int) { 659 val activity = activity as FragmentTransactionChecker? 660 if (activity != null) { 661 val startRotationErrorDialogFragment = 662 StartRotationErrorDialogFragment.newInstance(networkPreference) 663 startRotationErrorDialogFragment.setTargetFragment( 664 this@IndividualPickerFragment2, 665 UNUSED_REQUEST_CODE 666 ) 667 if (activity.isSafeToCommitFragmentTransaction) { 668 startRotationErrorDialogFragment.show( 669 parentFragmentManager, 670 TAG_START_ROTATION_ERROR_DIALOG 671 ) 672 } else { 673 stagedStartRotationErrorDialogFragment = startRotationErrorDialogFragment 674 } 675 } 676 } 677 678 private fun getNumColumns(): Int { 679 val activity = this.activity ?: return 1 680 return if (isFewerColumnLayout()) { 681 SizeCalculator.getNumFeaturedIndividualColumns(activity) 682 } else { 683 SizeCalculator.getNumIndividualColumns(activity) 684 } 685 } 686 687 /** Returns whether rotation is enabled for this category. */ 688 private fun isRotationEnabled() = wallpaperRotationInitializer != null 689 690 override fun onMenuItemClick(item: MenuItem): Boolean { 691 if (item.itemId == R.id.daily_rotation) { 692 showRotationDialog() 693 return true 694 } 695 return super.onMenuItemClick(item) 696 } 697 698 /** Popups a daily rotation dialog for the uses to confirm. */ 699 private fun showRotationDialog() { 700 val startRotationDialogFragment: DialogFragment = StartRotationDialogFragment() 701 startRotationDialogFragment.setTargetFragment( 702 this@IndividualPickerFragment2, 703 UNUSED_REQUEST_CODE 704 ) 705 startRotationDialogFragment.show(parentFragmentManager, TAG_START_ROTATION_DIALOG) 706 } 707 708 private fun getAppliedWallpaperIds(): Set<String> { 709 val prefs = InjectorProvider.getInjector().getPreferences(requireContext()) 710 val wallpaperInfo = wallpaperManager?.wallpaperInfo 711 val appliedWallpaperIds: MutableSet<String> = ArraySet() 712 val homeWallpaperId = 713 if (wallpaperInfo != null) { 714 wallpaperInfo.serviceName 715 } else { 716 prefs.homeWallpaperRemoteId 717 } 718 if (!TextUtils.isEmpty(homeWallpaperId)) { 719 appliedWallpaperIds.add(homeWallpaperId) 720 } 721 val isLockWallpaperApplied = 722 wallpaperManager!!.getWallpaperId(WallpaperManager.FLAG_LOCK) >= 0 723 val lockWallpaperId = prefs.lockWallpaperRemoteId 724 if (isLockWallpaperApplied && !TextUtils.isEmpty(lockWallpaperId)) { 725 appliedWallpaperIds.add(lockWallpaperId) 726 } 727 return appliedWallpaperIds 728 } 729 730 // TODO(b/277180178): Extract the check to another class for unit testing 731 private fun isAppliedWallpaperChanged(): Boolean { 732 // Reload wallpapers if the current wallpapers have changed 733 getAppliedWallpaperIds().let { 734 if (appliedWallpaperIds.isEmpty() || appliedWallpaperIds != it) { 735 return true 736 } 737 } 738 return false 739 } 740 741 sealed class PickerItem(val title: CharSequence = "") { 742 class WallpaperItem(val wallpaperInfo: WallpaperInfo, val isApplied: Boolean) : 743 PickerItem() 744 745 class HeaderItem(title: CharSequence) : PickerItem(title) 746 747 class FirstHeaderItem(title: CharSequence) : PickerItem(title) 748 749 class CreativeCollection(val templates: List<WallpaperInfo>) : PickerItem() 750 } 751 752 /** RecyclerView Adapter subclass for the wallpaper tiles in the RecyclerView. */ 753 class IndividualAdapter( 754 private val items: List<PickerItem>, 755 private val category: Category, 756 private val activity: Activity, 757 private val tileSizePx: Point, 758 private val isRotationEnabled: Boolean, 759 private val isFewerColumnLayout: Boolean, 760 private val edgePadding: Int, 761 private val bottomPadding: Int, 762 private val topPadding: Int 763 ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { 764 companion object { 765 const val ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER = 2 766 const val ITEM_VIEW_TYPE_MY_PHOTOS = 3 767 const val ITEM_VIEW_TYPE_HEADER = 4 768 const val ITEM_VIEW_TYPE_HEADER_TOP = 5 769 const val ITEM_VIEW_TYPE_CREATIVE = 6 770 } 771 772 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 773 return when (viewType) { 774 ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER -> createIndividualHolder(parent) 775 ITEM_VIEW_TYPE_MY_PHOTOS -> createMyPhotosHolder(parent) 776 ITEM_VIEW_TYPE_CREATIVE -> creativeCategoryHolder(parent) 777 ITEM_VIEW_TYPE_HEADER -> createTitleHolder(parent, /* removePaddingTop= */ false) 778 ITEM_VIEW_TYPE_HEADER_TOP -> createTitleHolder(parent, /* removePaddingTop= */ true) 779 else -> { 780 throw RuntimeException("Unsupported viewType $viewType in IndividualAdapter") 781 } 782 } 783 } 784 785 override fun getItemViewType(position: Int): Int { 786 // A category cannot have both a "start rotation" tile and a "my photos" tile. 787 return if ( 788 category.supportsCustomPhotos() && 789 !isRotationEnabled && 790 position == SPECIAL_FIXED_TILE_ADAPTER_POSITION 791 ) { 792 ITEM_VIEW_TYPE_MY_PHOTOS 793 } else { 794 when (items[position]) { 795 is PickerItem.WallpaperItem -> ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER 796 is PickerItem.HeaderItem -> ITEM_VIEW_TYPE_HEADER 797 is PickerItem.FirstHeaderItem -> ITEM_VIEW_TYPE_HEADER_TOP 798 is PickerItem.CreativeCollection -> ITEM_VIEW_TYPE_CREATIVE 799 } 800 } 801 } 802 803 override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 804 when (val viewType = getItemViewType(position)) { 805 ITEM_VIEW_TYPE_CREATIVE -> bindCreativeCategoryHolder(holder, position) 806 ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER -> bindIndividualHolder(holder, position) 807 ITEM_VIEW_TYPE_MY_PHOTOS -> (holder as MyPhotosViewHolder?)!!.bind() 808 ITEM_VIEW_TYPE_HEADER, 809 ITEM_VIEW_TYPE_HEADER_TOP -> { 810 val textView = holder.itemView as TextView 811 val item = items[position] 812 textView.text = item.title 813 textView.contentDescription = item.title 814 } 815 else -> Log.e(TAG, "Unexpected viewType $viewType in IndividualAdapter") 816 } 817 } 818 819 override fun getItemCount(): Int { 820 return if (category.supportsCustomPhotos()) { 821 items.size + 1 822 } else { 823 items.size 824 } 825 } 826 827 private fun createIndividualHolder(parent: ViewGroup): RecyclerView.ViewHolder { 828 val layoutInflater = LayoutInflater.from(activity) 829 val view: View = layoutInflater.inflate(R.layout.grid_item_image, parent, false) 830 return PreviewIndividualHolder(activity, tileSizePx.y, view) 831 } 832 833 private fun creativeCategoryHolder(parent: ViewGroup): RecyclerView.ViewHolder { 834 val layoutInflater = LayoutInflater.from(activity) 835 val view: View = 836 layoutInflater.inflate(R.layout.creative_category_holder, parent, false) 837 if (isCreativeCategory) { 838 view.setPadding(edgePadding, topPadding, edgePadding, bottomPadding) 839 } 840 return CreativeCategoryHolder( 841 activity, 842 view, 843 ) 844 } 845 846 private fun createMyPhotosHolder(parent: ViewGroup): RecyclerView.ViewHolder { 847 val layoutInflater = LayoutInflater.from(activity) 848 val view: View = layoutInflater.inflate(R.layout.grid_item_my_photos, parent, false) 849 return MyPhotosViewHolder( 850 activity, 851 (activity as MyPhotosStarterProvider).myPhotosStarter, 852 tileSizePx.y, 853 view 854 ) 855 } 856 857 private fun bindCreativeCategoryHolder(holder: RecyclerView.ViewHolder, position: Int) { 858 val wallpaperIndex = if (category.supportsCustomPhotos()) position - 1 else position 859 val item = items[wallpaperIndex] as PickerItem.CreativeCollection 860 (holder as CreativeCategoryHolder).bind( 861 item.templates, 862 SizeCalculator.getFeaturedIndividualTileSize(activity).y 863 ) 864 } 865 866 private fun createTitleHolder( 867 parent: ViewGroup, 868 removePaddingTop: Boolean 869 ): RecyclerView.ViewHolder { 870 val layoutInflater = LayoutInflater.from(activity) 871 val view = 872 layoutInflater.inflate(R.layout.grid_item_header, parent, /* attachToRoot= */ false) 873 var startPadding = view.paddingStart 874 if (isCreativeCategory) { 875 startPadding += edgePadding 876 } 877 if (removePaddingTop) { 878 view.setPaddingRelative( 879 startPadding, 880 /* top= */ 0, 881 view.paddingEnd, 882 view.paddingBottom 883 ) 884 } else { 885 view.setPaddingRelative( 886 startPadding, 887 view.paddingTop, 888 view.paddingEnd, 889 view.paddingBottom 890 ) 891 } 892 return object : RecyclerView.ViewHolder(view) {} 893 } 894 895 private fun bindIndividualHolder(holder: RecyclerView.ViewHolder, position: Int) { 896 val wallpaperIndex = if (category.supportsCustomPhotos()) position - 1 else position 897 val item = items[wallpaperIndex] as PickerItem.WallpaperItem 898 val wallpaper = item.wallpaperInfo 899 wallpaper.computeColorInfo(holder.itemView.context) 900 (holder as IndividualHolder).bindWallpaper(wallpaper) 901 val container = holder.itemView.findViewById<CardView>(R.id.wallpaper_container) 902 val radiusId: Int = 903 if (isFewerColumnLayout) { 904 R.dimen.grid_item_all_radius 905 } else { 906 R.dimen.grid_item_all_radius_small 907 } 908 container.radius = activity.resources.getDimension(radiusId) 909 showBadge(holder, R.drawable.wallpaper_check_circle_24dp, item.isApplied) 910 if (!item.isApplied) { 911 showBadge(holder, wallpaper.badgeDrawableRes, wallpaper.badgeDrawableRes != ID_NULL) 912 } 913 } 914 915 private fun showBadge( 916 holder: RecyclerView.ViewHolder, 917 @DrawableRes icon: Int, 918 show: Boolean 919 ) { 920 val badge = holder.itemView.findViewById<ImageView>(R.id.indicator_icon) 921 if (show) { 922 val margin = 923 if (isFewerColumnLayout) { 924 activity.resources.getDimension(R.dimen.grid_item_badge_margin) 925 } else { 926 activity.resources.getDimension(R.dimen.grid_item_badge_margin_small) 927 } 928 .toInt() 929 val layoutParams = badge.layoutParams as RelativeLayout.LayoutParams 930 layoutParams.setMargins(margin, margin, margin, margin) 931 badge.layoutParams = layoutParams 932 badge.setBackgroundResource(icon) 933 badge.visibility = View.VISIBLE 934 } else { 935 badge.visibility = View.GONE 936 } 937 } 938 } 939 940 override fun getToolbarColorId(): Int { 941 return android.R.color.transparent 942 } 943 944 override fun getToolbarTextColor(): Int { 945 return ContextCompat.getColor(requireContext(), R.color.system_on_surface) 946 } 947 } 948