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