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