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