• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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