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