Bug 1822181 - Refactor TabCounterBuilder to TabCounterView
[gecko.git] / mobile / android / fenix / app / src / main / java / org / mozilla / fenix / home / HomeFragment.kt
blobbd92d6f86cc8b6ca2398baf1911fca3eda2ff3e7
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.fenix.home
7 import android.annotation.SuppressLint
8 import android.content.res.ColorStateList
9 import android.content.res.Configuration
10 import android.graphics.drawable.BitmapDrawable
11 import android.graphics.drawable.ColorDrawable
12 import android.net.Uri
13 import android.os.Bundle
14 import android.os.StrictMode
15 import android.view.Gravity
16 import android.view.LayoutInflater
17 import android.view.View
18 import android.view.ViewGroup
19 import android.widget.Button
20 import android.widget.LinearLayout
21 import android.widget.PopupWindow
22 import androidx.annotation.VisibleForTesting
23 import androidx.appcompat.content.res.AppCompatResources
24 import androidx.constraintlayout.widget.ConstraintLayout
25 import androidx.constraintlayout.widget.ConstraintSet
26 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
27 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
28 import androidx.constraintlayout.widget.ConstraintSet.TOP
29 import androidx.coordinatorlayout.widget.CoordinatorLayout
30 import androidx.core.content.ContextCompat
31 import androidx.core.graphics.drawable.toDrawable
32 import androidx.core.view.isGone
33 import androidx.core.view.isVisible
34 import androidx.core.view.updateLayoutParams
35 import androidx.fragment.app.Fragment
36 import androidx.fragment.app.activityViewModels
37 import androidx.lifecycle.Observer
38 import androidx.lifecycle.lifecycleScope
39 import androidx.navigation.fragment.findNavController
40 import androidx.navigation.fragment.navArgs
41 import androidx.recyclerview.widget.LinearLayoutManager
42 import androidx.recyclerview.widget.LinearSmoothScroller
43 import androidx.recyclerview.widget.RecyclerView.SmoothScroller
44 import com.google.android.material.appbar.AppBarLayout
45 import com.google.android.material.button.MaterialButton
46 import com.google.android.material.snackbar.Snackbar
47 import kotlinx.coroutines.Dispatchers.IO
48 import kotlinx.coroutines.Dispatchers.Main
49 import kotlinx.coroutines.MainScope
50 import kotlinx.coroutines.delay
51 import kotlinx.coroutines.flow.map
52 import kotlinx.coroutines.launch
53 import mozilla.components.browser.menu.view.MenuButton
54 import mozilla.components.browser.state.search.SearchEngine
55 import mozilla.components.browser.state.selector.findTab
56 import mozilla.components.browser.state.selector.normalTabs
57 import mozilla.components.browser.state.selector.privateTabs
58 import mozilla.components.browser.state.state.BrowserState
59 import mozilla.components.browser.state.state.searchEngines
60 import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
61 import mozilla.components.browser.state.store.BrowserStore
62 import mozilla.components.concept.menu.Orientation
63 import mozilla.components.concept.menu.candidate.DrawableMenuIcon
64 import mozilla.components.concept.menu.candidate.TextMenuCandidate
65 import mozilla.components.concept.storage.FrecencyThresholdOption
66 import mozilla.components.concept.sync.AccountObserver
67 import mozilla.components.concept.sync.AuthType
68 import mozilla.components.concept.sync.OAuthAccount
69 import mozilla.components.feature.tab.collections.TabCollection
70 import mozilla.components.feature.top.sites.TopSitesConfig
71 import mozilla.components.feature.top.sites.TopSitesFeature
72 import mozilla.components.feature.top.sites.TopSitesFrecencyConfig
73 import mozilla.components.feature.top.sites.TopSitesProviderConfig
74 import mozilla.components.lib.state.ext.consumeFlow
75 import mozilla.components.lib.state.ext.consumeFrom
76 import mozilla.components.service.glean.private.NoExtras
77 import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
78 import mozilla.components.support.ktx.android.content.getColorFromAttr
79 import mozilla.components.support.ktx.android.content.res.resolveAttribute
80 import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
81 import org.mozilla.fenix.Config
82 import org.mozilla.fenix.GleanMetrics.Events
83 import org.mozilla.fenix.GleanMetrics.HomeScreen
84 import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcutCfr
85 import org.mozilla.fenix.GleanMetrics.UnifiedSearch
86 import org.mozilla.fenix.HomeActivity
87 import org.mozilla.fenix.R
88 import org.mozilla.fenix.addons.showSnackBar
89 import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
90 import org.mozilla.fenix.browser.browsingmode.BrowsingMode
91 import org.mozilla.fenix.components.FenixSnackbar
92 import org.mozilla.fenix.components.PrivateShortcutCreateManager
93 import org.mozilla.fenix.components.TabCollectionStorage
94 import org.mozilla.fenix.components.appstate.AppAction
95 import org.mozilla.fenix.components.toolbar.ToolbarPosition
96 import org.mozilla.fenix.databinding.FragmentHomeBinding
97 import org.mozilla.fenix.ext.components
98 import org.mozilla.fenix.ext.containsQueryParameters
99 import org.mozilla.fenix.ext.hideToolbar
100 import org.mozilla.fenix.ext.increaseTapArea
101 import org.mozilla.fenix.ext.nav
102 import org.mozilla.fenix.ext.requireComponents
103 import org.mozilla.fenix.ext.runIfFragmentIsAttached
104 import org.mozilla.fenix.ext.scaleToBottomOfView
105 import org.mozilla.fenix.ext.settings
106 import org.mozilla.fenix.gleanplumb.DefaultMessageController
107 import org.mozilla.fenix.gleanplumb.MessagingFeature
108 import org.mozilla.fenix.gleanplumb.NimbusMessagingController
109 import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow
110 import org.mozilla.fenix.home.pocket.DefaultPocketStoriesController
111 import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
112 import org.mozilla.fenix.home.recentbookmarks.RecentBookmarksFeature
113 import org.mozilla.fenix.home.recentbookmarks.controller.DefaultRecentBookmarksController
114 import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabFeature
115 import org.mozilla.fenix.home.recentsyncedtabs.controller.DefaultRecentSyncedTabController
116 import org.mozilla.fenix.home.recenttabs.RecentTabsListFeature
117 import org.mozilla.fenix.home.recenttabs.controller.DefaultRecentTabsController
118 import org.mozilla.fenix.home.recentvisits.RecentVisitsFeature
119 import org.mozilla.fenix.home.recentvisits.controller.DefaultRecentVisitsController
120 import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
121 import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
122 import org.mozilla.fenix.home.sessioncontrol.SessionControlView
123 import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
124 import org.mozilla.fenix.home.topsites.DefaultTopSitesView
125 import org.mozilla.fenix.nimbus.FxNimbus
126 import org.mozilla.fenix.onboarding.FenixOnboarding
127 import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
128 import org.mozilla.fenix.perf.runBlockingIncrement
129 import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
130 import org.mozilla.fenix.tabstray.TabsTrayAccessPoint
131 import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
132 import org.mozilla.fenix.utils.ToolbarPopupWindow
133 import org.mozilla.fenix.utils.allowUndo
134 import org.mozilla.fenix.wallpapers.Wallpaper
135 import java.lang.ref.WeakReference
136 import kotlin.math.min
138 @Suppress("TooManyFunctions", "LargeClass")
139 class HomeFragment : Fragment() {
140     private val args by navArgs<HomeFragmentArgs>()
142     @VisibleForTesting
143     internal lateinit var bundleArgs: Bundle
145     @VisibleForTesting
146     @Suppress("VariableNaming")
147     internal var _binding: FragmentHomeBinding? = null
148     private val binding get() = _binding!!
150     private val homeViewModel: HomeScreenViewModel by activityViewModels()
152     private val snackbarAnchorView: View?
153         get() = when (requireContext().settings().toolbarPosition) {
154             ToolbarPosition.BOTTOM -> binding.toolbarLayout
155             ToolbarPosition.TOP -> null
156         }
158     private val searchSelectorMenu by lazy {
159         SearchSelectorMenu(
160             context = requireContext(),
161             interactor = sessionControlInteractor,
162         )
163     }
165     private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
167     private val collectionStorageObserver = object : TabCollectionStorage.Observer {
168         @SuppressLint("NotifyDataSetChanged")
169         override fun onCollectionRenamed(tabCollection: TabCollection, title: String) {
170             lifecycleScope.launch(Main) {
171                 binding.sessionControlRecyclerView.adapter?.notifyDataSetChanged()
172             }
173             showRenamedSnackbar()
174         }
175     }
177     private val store: BrowserStore
178         get() = requireComponents.core.store
180     private val onboarding by lazy {
181         requireComponents.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
182             FenixOnboarding(requireContext())
183         }
184     }
186     private var _sessionControlInteractor: SessionControlInteractor? = null
187     private val sessionControlInteractor: SessionControlInteractor
188         get() = _sessionControlInteractor!!
190     private var sessionControlView: SessionControlView? = null
191     private var appBarLayout: AppBarLayout? = null
192     private lateinit var currentMode: CurrentMode
194     private var lastAppliedWallpaperName: String = Wallpaper.defaultName
196     private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
197     private val messagingFeature = ViewBoundFeatureWrapper<MessagingFeature>()
198     private val recentTabsListFeature = ViewBoundFeatureWrapper<RecentTabsListFeature>()
199     private val recentSyncedTabFeature = ViewBoundFeatureWrapper<RecentSyncedTabFeature>()
200     private val recentBookmarksFeature = ViewBoundFeatureWrapper<RecentBookmarksFeature>()
201     private val historyMetadataFeature = ViewBoundFeatureWrapper<RecentVisitsFeature>()
203     @VisibleForTesting
204     internal var getMenuButton: () -> MenuButton? = { binding.menuButton }
206     override fun onCreate(savedInstanceState: Bundle?) {
207         // DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL!
208         val profilerStartTime = requireComponents.core.engine.profiler?.getProfilerTime()
210         super.onCreate(savedInstanceState)
212         bundleArgs = args.toBundle()
214         if (!onboarding.userHasBeenOnboarded() &&
215             requireContext().settings().shouldShowPrivacyPopWindow &&
216             Config.channel.isMozillaOnline
217         ) {
218             showPrivacyPopWindow(requireContext(), requireActivity())
219         }
221         // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
222         requireComponents.core.engine.profiler?.addMarker(
223             MarkersFragmentLifecycleCallbacks.MARKER_NAME,
224             profilerStartTime,
225             "HomeFragment.onCreate",
226         )
227     }
229     @Suppress("LongMethod")
230     override fun onCreateView(
231         inflater: LayoutInflater,
232         container: ViewGroup?,
233         savedInstanceState: Bundle?,
234     ): View {
235         // DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL!
236         val profilerStartTime = requireComponents.core.engine.profiler?.getProfilerTime()
238         _binding = FragmentHomeBinding.inflate(inflater, container, false)
239         val activity = activity as HomeActivity
240         val components = requireComponents
242         val currentWallpaperName = requireContext().settings().currentWallpaperName
243         applyWallpaper(wallpaperName = currentWallpaperName, orientationChange = false)
245         currentMode = CurrentMode(
246             requireContext(),
247             onboarding,
248             browsingModeManager,
249             ::dispatchModeChanges,
250         )
252         components.appStore.dispatch(AppAction.ModeChange(currentMode.getCurrentMode()))
254         lifecycleScope.launch(IO) {
255             if (requireContext().settings().showPocketRecommendationsFeature) {
256                 val categories = components.core.pocketStoriesService.getStories()
257                     .groupBy { story -> story.category }
258                     .map { (category, stories) -> PocketRecommendedStoriesCategory(category, stories) }
260                 components.appStore.dispatch(AppAction.PocketStoriesCategoriesChange(categories))
262                 if (requireContext().settings().showPocketSponsoredStories) {
263                     components.appStore.dispatch(
264                         AppAction.PocketSponsoredStoriesChange(
265                             components.core.pocketStoriesService.getSponsoredStories(),
266                         ),
267                     )
268                 }
269             } else {
270                 components.appStore.dispatch(AppAction.PocketStoriesClean)
271             }
272         }
274         if (requireContext().settings().isExperimentationEnabled) {
275             messagingFeature.set(
276                 feature = MessagingFeature(
277                     appStore = requireComponents.appStore,
278                 ),
279                 owner = viewLifecycleOwner,
280                 view = binding.root,
281             )
282         }
284         if (requireContext().settings().showTopSitesFeature) {
285             topSitesFeature.set(
286                 feature = TopSitesFeature(
287                     view = DefaultTopSitesView(
288                         appStore = components.appStore,
289                         settings = components.settings,
290                     ),
291                     storage = components.core.topSitesStorage,
292                     config = ::getTopSitesConfig,
293                 ),
294                 owner = viewLifecycleOwner,
295                 view = binding.root,
296             )
297         }
299         if (requireContext().settings().showRecentTabsFeature) {
300             recentTabsListFeature.set(
301                 feature = RecentTabsListFeature(
302                     browserStore = components.core.store,
303                     appStore = components.appStore,
304                 ),
305                 owner = viewLifecycleOwner,
306                 view = binding.root,
307             )
309             if (requireContext().settings().enableTaskContinuityEnhancements) {
310                 recentSyncedTabFeature.set(
311                     feature = RecentSyncedTabFeature(
312                         context = requireContext(),
313                         appStore = requireComponents.appStore,
314                         syncStore = requireComponents.backgroundServices.syncStore,
315                         storage = requireComponents.backgroundServices.syncedTabsStorage,
316                         accountManager = requireComponents.backgroundServices.accountManager,
317                         historyStorage = requireComponents.core.historyStorage,
318                         coroutineScope = viewLifecycleOwner.lifecycleScope,
319                     ),
320                     owner = viewLifecycleOwner,
321                     view = binding.root,
322                 )
323             }
324         }
326         if (requireContext().settings().showRecentBookmarksFeature) {
327             recentBookmarksFeature.set(
328                 feature = RecentBookmarksFeature(
329                     appStore = components.appStore,
330                     bookmarksUseCase = run {
331                         requireContext().components.useCases.bookmarksUseCases
332                     },
333                     scope = viewLifecycleOwner.lifecycleScope,
334                 ),
335                 owner = viewLifecycleOwner,
336                 view = binding.root,
337             )
338         }
340         if (requireContext().settings().historyMetadataUIFeature) {
341             historyMetadataFeature.set(
342                 feature = RecentVisitsFeature(
343                     appStore = components.appStore,
344                     historyMetadataStorage = components.core.historyStorage,
345                     historyHighlightsStorage = components.core.lazyHistoryStorage,
346                     scope = viewLifecycleOwner.lifecycleScope,
347                 ),
348                 owner = viewLifecycleOwner,
349                 view = binding.root,
350             )
351         }
353         requireContext().settings().showUnifiedSearchFeature.let {
354             binding.searchSelectorButton.isVisible = it
355             binding.searchEngineIcon.isGone = it
356         }
358         binding.searchSelectorButton.apply {
359             setOnClickListener {
360                 val orientation = if (context.settings().shouldUseBottomToolbar) {
361                     Orientation.UP
362                 } else {
363                     Orientation.DOWN
364                 }
366                 UnifiedSearch.searchMenuTapped.record(NoExtras())
367                 searchSelectorMenu.menuController.show(
368                     anchor = it.findViewById(R.id.search_selector),
369                     orientation = orientation,
370                 )
371             }
372         }
374         _sessionControlInteractor = SessionControlInteractor(
375             controller = DefaultSessionControlController(
376                 activity = activity,
377                 settings = components.settings,
378                 engine = components.core.engine,
379                 messageController = DefaultMessageController(
380                     appStore = components.appStore,
381                     messagingController = NimbusMessagingController(components.analytics.messagingStorage),
382                     homeActivity = activity,
383                 ),
384                 store = store,
385                 tabCollectionStorage = components.core.tabCollectionStorage,
386                 addTabUseCase = components.useCases.tabsUseCases.addTab,
387                 restoreUseCase = components.useCases.tabsUseCases.restore,
388                 reloadUrlUseCase = components.useCases.sessionUseCases.reload,
389                 selectTabUseCase = components.useCases.tabsUseCases.selectTab,
390                 appStore = components.appStore,
391                 navController = findNavController(),
392                 viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
393                 hideOnboarding = ::hideOnboardingAndOpenSearch,
394                 registerCollectionStorageObserver = ::registerCollectionStorageObserver,
395                 removeCollectionWithUndo = ::removeCollectionWithUndo,
396                 showTabTray = ::openTabsTray,
397             ),
398             recentTabController = DefaultRecentTabsController(
399                 selectTabUseCase = components.useCases.tabsUseCases.selectTab,
400                 navController = findNavController(),
401                 appStore = components.appStore,
402             ),
403             recentSyncedTabController = DefaultRecentSyncedTabController(
404                 tabsUseCase = requireComponents.useCases.tabsUseCases,
405                 navController = findNavController(),
406                 accessPoint = TabsTrayAccessPoint.HomeRecentSyncedTab,
407                 appStore = components.appStore,
408             ),
409             recentBookmarksController = DefaultRecentBookmarksController(
410                 activity = activity,
411                 navController = findNavController(),
412                 appStore = components.appStore,
413             ),
414             recentVisitsController = DefaultRecentVisitsController(
415                 navController = findNavController(),
416                 appStore = components.appStore,
417                 selectOrAddTabUseCase = components.useCases.tabsUseCases.selectOrAddTab,
418                 storage = components.core.historyStorage,
419                 scope = viewLifecycleOwner.lifecycleScope,
420                 store = components.core.store,
421             ),
422             pocketStoriesController = DefaultPocketStoriesController(
423                 homeActivity = activity,
424                 appStore = components.appStore,
425             ),
426         )
428         updateLayout(binding.root)
429         sessionControlView = SessionControlView(
430             containerView = binding.sessionControlRecyclerView,
431             viewLifecycleOwner = viewLifecycleOwner,
432             interactor = sessionControlInteractor,
433         )
435         updateSessionControlView()
437         appBarLayout = binding.homeAppBar
439         disableAppBarDragging()
441         activity.themeManager.applyStatusBarTheme(activity)
443         FxNimbus.features.homescreen.recordExposure()
445         // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
446         requireComponents.core.engine.profiler?.addMarker(
447             MarkersFragmentLifecycleCallbacks.MARKER_NAME,
448             profilerStartTime,
449             "HomeFragment.onCreateView",
450         )
451         return binding.root
452     }
454     override fun onConfigurationChanged(newConfig: Configuration) {
455         super.onConfigurationChanged(newConfig)
457         getMenuButton()?.dismissMenu()
459         val currentWallpaperName = requireContext().settings().currentWallpaperName
460         applyWallpaper(wallpaperName = currentWallpaperName, orientationChange = true)
461     }
463     /**
464      * Returns a [TopSitesConfig] which specifies how many top sites to display and whether or
465      * not frequently visited sites should be displayed.
466      */
467     @VisibleForTesting
468     internal fun getTopSitesConfig(): TopSitesConfig {
469         val settings = requireContext().settings()
470         return TopSitesConfig(
471             totalSites = settings.topSitesMaxLimit,
472             frecencyConfig = TopSitesFrecencyConfig(
473                 FrecencyThresholdOption.SKIP_ONE_TIME_PAGES,
474             ) { !Uri.parse(it.url).containsQueryParameters(settings.frecencyFilterQuery) },
475             providerConfig = TopSitesProviderConfig(
476                 showProviderTopSites = settings.showContileFeature,
477                 maxThreshold = TOP_SITES_PROVIDER_MAX_THRESHOLD,
478                 providerFilter = { topSite ->
479                     when (store.state.search.selectedOrDefaultSearchEngine?.name) {
480                         AMAZON_SEARCH_ENGINE_NAME -> topSite.title != AMAZON_SPONSORED_TITLE
481                         EBAY_SPONSORED_TITLE -> topSite.title != EBAY_SPONSORED_TITLE
482                         else -> true
483                     }
484                 },
485             ),
486         )
487     }
489     /**
490      * The [SessionControlView] is forced to update with our current state when we call
491      * [HomeFragment.onCreateView] in order to be able to draw everything at once with the current
492      * data in our store. The [View.consumeFrom] coroutine dispatch
493      * doesn't get run right away which means that we won't draw on the first layout pass.
494      */
495     private fun updateSessionControlView() {
496         if (browsingModeManager.mode == BrowsingMode.Private) {
497             binding.root.consumeFrom(requireContext().components.appStore, viewLifecycleOwner) {
498                 sessionControlView?.update(it)
499             }
500         } else {
501             sessionControlView?.update(requireContext().components.appStore.state)
503             binding.root.consumeFrom(requireContext().components.appStore, viewLifecycleOwner) {
504                 sessionControlView?.update(it, shouldReportMetrics = true)
505             }
506         }
507     }
509     private fun disableAppBarDragging() {
510         if (binding.homeAppBar.layoutParams != null) {
511             val appBarLayoutParams = binding.homeAppBar.layoutParams as CoordinatorLayout.LayoutParams
512             val appBarBehavior = AppBarLayout.Behavior()
513             appBarBehavior.setDragCallback(
514                 object : AppBarLayout.Behavior.DragCallback() {
515                     override fun canDrag(appBarLayout: AppBarLayout): Boolean {
516                         return false
517                     }
518                 },
519             )
520             appBarLayoutParams.behavior = appBarBehavior
521         }
522         binding.homeAppBar.setExpanded(true)
523     }
525     private fun updateLayout(view: View) {
526         when (requireContext().settings().toolbarPosition) {
527             ToolbarPosition.TOP -> {
528                 binding.toolbarLayout.layoutParams = CoordinatorLayout.LayoutParams(
529                     ConstraintLayout.LayoutParams.MATCH_PARENT,
530                     ConstraintLayout.LayoutParams.WRAP_CONTENT,
531                 ).apply {
532                     gravity = Gravity.TOP
533                 }
535                 ConstraintSet().apply {
536                     clone(binding.toolbarLayout)
537                     clear(binding.bottomBar.id, BOTTOM)
538                     clear(binding.bottomBarShadow.id, BOTTOM)
539                     connect(binding.bottomBar.id, TOP, PARENT_ID, TOP)
540                     connect(binding.bottomBarShadow.id, TOP, binding.bottomBar.id, BOTTOM)
541                     connect(binding.bottomBarShadow.id, BOTTOM, PARENT_ID, BOTTOM)
542                     applyTo(binding.toolbarLayout)
543                 }
545                 binding.bottomBar.background = AppCompatResources.getDrawable(
546                     view.context,
547                     view.context.theme.resolveAttribute(R.attr.bottomBarBackgroundTop),
548                 )
550                 binding.homeAppBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
551                     topMargin =
552                         resources.getDimensionPixelSize(R.dimen.home_fragment_top_toolbar_header_margin)
553                 }
554             }
555             ToolbarPosition.BOTTOM -> {
556             }
557         }
558     }
560     @Suppress("LongMethod", "ComplexMethod")
561     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
562         // DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL!
563         val profilerStartTime = requireComponents.core.engine.profiler?.getProfilerTime()
565         super.onViewCreated(view, savedInstanceState)
566         HomeScreen.homeScreenDisplayed.record(NoExtras())
567         HomeScreen.homeScreenViewCount.add()
569         observeSearchEngineChanges()
570         observeSearchEngineNameChanges()
571         observeWallpaperUpdates()
573         HomeMenuView(
574             view = view,
575             context = view.context,
576             lifecycleOwner = viewLifecycleOwner,
577             homeActivity = activity as HomeActivity,
578             navController = findNavController(),
579             menuButton = WeakReference(binding.menuButton),
580             hideOnboardingIfNeeded = ::hideOnboardingIfNeeded,
581         ).build()
583         TabCounterView(
584             context = requireContext(),
585             browsingModeManager = browsingModeManager,
586             navController = findNavController(),
587             tabCounter = binding.tabButton,
588         )
590         binding.toolbar.compoundDrawablePadding =
591             view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding)
592         binding.toolbarWrapper.setOnClickListener {
593             navigateToSearch()
594         }
596         binding.toolbarWrapper.setOnLongClickListener {
597             ToolbarPopupWindow.show(
598                 WeakReference(it),
599                 handlePasteAndGo = sessionControlInteractor::onPasteAndGo,
600                 handlePaste = sessionControlInteractor::onPaste,
601                 copyVisible = false,
602             )
603             true
604         }
606         PrivateBrowsingButtonView(binding.privateBrowsingButton, browsingModeManager) { newMode ->
607             sessionControlInteractor.onPrivateModeButtonClicked(
608                 newMode,
609                 onboarding.userHasBeenOnboarded(),
610             )
611         }
613         consumeFrom(requireComponents.core.store) {
614             updateTabCounter(it)
615         }
617         homeViewModel.sessionToDelete?.also {
618             if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) {
619                 removeAllTabsAndShowSnackbar(it)
620             } else {
621                 removeTabAndShowSnackbar(it)
622             }
623         }
625         homeViewModel.sessionToDelete = null
627         updateTabCounter(requireComponents.core.store.state)
629         if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR)) {
630             navigateToSearch()
631         } else if (bundleArgs.getBoolean(SCROLL_TO_COLLECTION)) {
632             MainScope().launch {
633                 delay(ANIM_SCROLL_DELAY)
634                 val smoothScroller: SmoothScroller =
635                     object : LinearSmoothScroller(sessionControlView!!.view.context) {
636                         override fun getVerticalSnapPreference(): Int {
637                             return SNAP_TO_START
638                         }
639                     }
640                 val recyclerView = sessionControlView!!.view
641                 val adapter = recyclerView.adapter!!
642                 val collectionPosition = IntRange(0, adapter.itemCount - 1).firstOrNull {
643                     adapter.getItemViewType(it) == CollectionHeaderViewHolder.LAYOUT_ID
644                 }
645                 collectionPosition?.run {
646                     val linearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
647                     smoothScroller.targetPosition = this
648                     linearLayoutManager.startSmoothScroll(smoothScroller)
649                 }
650             }
651         }
653         consumeFlow(requireComponents.core.store) { flow ->
654             flow.map { state -> state.search }
655                 .ifChanged()
656                 .collect { search ->
657                     updateSearchSelectorMenu(search.searchEngines)
658                 }
659         }
661         // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
662         requireComponents.core.engine.profiler?.addMarker(
663             MarkersFragmentLifecycleCallbacks.MARKER_NAME,
664             profilerStartTime,
665             "HomeFragment.onViewCreated",
666         )
667     }
669     private fun updateSearchSelectorMenu(searchEngines: List<SearchEngine>) {
670         val searchEngineList = searchEngines
671             .map {
672                 TextMenuCandidate(
673                     text = it.name,
674                     start = DrawableMenuIcon(
675                         drawable = it.icon.toDrawable(resources),
676                         tint = if (it.type == SearchEngine.Type.APPLICATION) {
677                             requireContext().getColorFromAttr(R.attr.textPrimary)
678                         } else {
679                             null
680                         },
681                     ),
682                 ) {
683                     sessionControlInteractor.onMenuItemTapped(SearchSelectorMenu.Item.SearchEngine(it))
684                 }
685             }
687         searchSelectorMenu.menuController.submitList(searchSelectorMenu.menuItems(searchEngineList))
688     }
690     private fun observeSearchEngineChanges() {
691         consumeFlow(store) { flow ->
692             flow.map { state -> state.search.selectedOrDefaultSearchEngine }
693                 .ifChanged()
694                 .collect { searchEngine ->
695                     val name = searchEngine?.name
696                     val icon = searchEngine?.let {
697                         // Changing dimensions doesn't not affect the icon size, not sure what the
698                         // code is doing:  https://github.com/mozilla-mobile/fenix/issues/27763
699                         val iconSize =
700                             requireContext().resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
701                         BitmapDrawable(requireContext().resources, searchEngine.icon).apply {
702                             setBounds(0, 0, iconSize, iconSize)
703                         }
704                     }
706                     if (requireContext().settings().showUnifiedSearchFeature) {
707                         binding.searchSelectorButton.setIcon(icon, name)
708                     } else {
709                         binding.searchEngineIcon.setImageDrawable(icon)
710                     }
711                 }
712         }
713     }
715     /**
716      * Method used to listen to search engine name changes and trigger a top sites update accordingly
717      */
718     private fun observeSearchEngineNameChanges() {
719         consumeFlow(store) { flow ->
720             flow.map { state ->
721                 when (state.search.selectedOrDefaultSearchEngine?.name) {
722                     AMAZON_SEARCH_ENGINE_NAME -> AMAZON_SPONSORED_TITLE
723                     EBAY_SPONSORED_TITLE -> EBAY_SPONSORED_TITLE
724                     else -> null
725                 }
726             }
727                 .ifChanged()
728                 .collect {
729                     topSitesFeature.withFeature {
730                         it.storage.notifyObservers { onStorageUpdated() }
731                     }
732                 }
733         }
734     }
736     private fun removeAllTabsAndShowSnackbar(sessionCode: String) {
737         if (sessionCode == ALL_PRIVATE_TABS) {
738             requireComponents.useCases.tabsUseCases.removePrivateTabs()
739         } else {
740             requireComponents.useCases.tabsUseCases.removeNormalTabs()
741         }
743         val snackbarMessage = if (sessionCode == ALL_PRIVATE_TABS) {
744             getString(R.string.snackbar_private_tabs_closed)
745         } else {
746             getString(R.string.snackbar_tabs_closed)
747         }
749         viewLifecycleOwner.lifecycleScope.allowUndo(
750             requireView(),
751             snackbarMessage,
752             requireContext().getString(R.string.snackbar_deleted_undo),
753             {
754                 requireComponents.useCases.tabsUseCases.undo.invoke()
755             },
756             operation = { },
757             anchorView = snackbarAnchorView,
758         )
759     }
761     private fun removeTabAndShowSnackbar(sessionId: String) {
762         val tab = store.state.findTab(sessionId) ?: return
764         requireComponents.useCases.tabsUseCases.removeTab(sessionId)
766         val snackbarMessage = if (tab.content.private) {
767             requireContext().getString(R.string.snackbar_private_tab_closed)
768         } else {
769             requireContext().getString(R.string.snackbar_tab_closed)
770         }
772         viewLifecycleOwner.lifecycleScope.allowUndo(
773             requireView(),
774             snackbarMessage,
775             requireContext().getString(R.string.snackbar_deleted_undo),
776             {
777                 requireComponents.useCases.tabsUseCases.undo.invoke()
778                 findNavController().navigate(
779                     HomeFragmentDirections.actionGlobalBrowser(null),
780                 )
781             },
782             operation = { },
783             anchorView = snackbarAnchorView,
784         )
785     }
787     override fun onDestroyView() {
788         super.onDestroyView()
790         _sessionControlInteractor = null
791         sessionControlView = null
792         appBarLayout = null
793         _binding = null
794         bundleArgs.clear()
795         lastAppliedWallpaperName = Wallpaper.defaultName
796     }
798     override fun onStart() {
799         super.onStart()
801         subscribeToTabCollections()
803         val context = requireContext()
805         requireComponents.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
806             // By the time this code runs, we may not be attached to a context or have a view lifecycle owner.
807             if ((this@HomeFragment).view?.context == null) {
808                 return@runIfReadyOrQueue
809             }
811             requireComponents.backgroundServices.accountManager.register(
812                 currentMode,
813                 owner = this@HomeFragment.viewLifecycleOwner,
814             )
815             requireComponents.backgroundServices.accountManager.register(
816                 object : AccountObserver {
817                     override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
818                         if (authType != AuthType.Existing) {
819                             view?.let {
820                                 FenixSnackbar.make(
821                                     view = it,
822                                     duration = Snackbar.LENGTH_SHORT,
823                                     isDisplayedWithBrowserToolbar = false,
824                                 )
825                                     .setText(it.context.getString(R.string.onboarding_firefox_account_sync_is_on))
826                                     .setAnchorView(binding.toolbarLayout)
827                                     .show()
828                             }
829                         }
830                     }
831                 },
832                 owner = this@HomeFragment.viewLifecycleOwner,
833             )
834         }
836         if (browsingModeManager.mode.isPrivate &&
837             // We will be showing the search dialog and don't want to show the CFR while the dialog shows
838             !bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) &&
839             context.settings().shouldShowPrivateModeCfr
840         ) {
841             recommendPrivateBrowsingShortcut()
842         }
844         // We only want this observer live just before we navigate away to the collection creation screen
845         requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver)
847         lifecycleScope.launch(IO) {
848             requireComponents.reviewPromptController.promptReview(requireActivity())
849         }
850     }
852     private fun dispatchModeChanges(mode: Mode) {
853         if (mode != Mode.fromBrowsingMode(browsingModeManager.mode)) {
854             requireContext().components.appStore.dispatch(AppAction.ModeChange(mode))
855         }
856     }
858     @VisibleForTesting
859     internal fun removeCollectionWithUndo(tabCollection: TabCollection) {
860         val snackbarMessage = getString(R.string.snackbar_collection_deleted)
862         lifecycleScope.allowUndo(
863             requireView(),
864             snackbarMessage,
865             getString(R.string.snackbar_deleted_undo),
866             {
867                 requireComponents.core.tabCollectionStorage.createCollection(tabCollection)
868             },
869             operation = { },
870             elevation = TOAST_ELEVATION,
871             anchorView = null,
872         )
874         lifecycleScope.launch(IO) {
875             requireComponents.core.tabCollectionStorage.removeCollection(tabCollection)
876         }
877     }
879     override fun onResume() {
880         super.onResume()
881         if (browsingModeManager.mode == BrowsingMode.Private) {
882             activity?.window?.setBackgroundDrawableResource(R.drawable.private_home_background_gradient)
883         }
885         hideToolbar()
887         // Whenever a tab is selected its last access timestamp is automatically updated by A-C.
888         // However, in the case of resuming the app to the home fragment, we already have an
889         // existing selected tab, but its last access timestamp is outdated. No action is
890         // triggered to cause an automatic update on warm start (no tab selection occurs). So we
891         // update it manually here.
892         requireComponents.useCases.sessionUseCases.updateLastAccess()
893     }
895     override fun onPause() {
896         super.onPause()
897         if (browsingModeManager.mode == BrowsingMode.Private) {
898             activity?.window?.setBackgroundDrawable(
899                 ColorDrawable(
900                     ContextCompat.getColor(
901                         requireContext(),
902                         R.color.fx_mobile_private_layer_color_1,
903                     ),
904                 ),
905             )
906         }
908         // Counterpart to the update in onResume to keep the last access timestamp of the selected
909         // tab up-to-date.
910         requireComponents.useCases.sessionUseCases.updateLastAccess()
911     }
913     @SuppressLint("InflateParams")
914     private fun recommendPrivateBrowsingShortcut() {
915         context?.let { context ->
916             val layout = LayoutInflater.from(context)
917                 .inflate(R.layout.pbm_shortcut_popup, null)
918             val privateBrowsingRecommend =
919                 PopupWindow(
920                     layout,
921                     min(
922                         (resources.displayMetrics.widthPixels / CFR_WIDTH_DIVIDER).toInt(),
923                         (resources.displayMetrics.heightPixels / CFR_WIDTH_DIVIDER).toInt(),
924                     ),
925                     LinearLayout.LayoutParams.WRAP_CONTENT,
926                     true,
927                 )
928             layout.findViewById<Button>(R.id.cfr_pos_button).apply {
929                 this.increaseTapArea(CFR_TAP_INCREASE_DPS)
931                 setOnClickListener {
932                     PrivateBrowsingShortcutCfr.addShortcut.record(NoExtras())
933                     PrivateShortcutCreateManager.createPrivateShortcut(context)
934                     privateBrowsingRecommend.dismiss()
935                 }
936             }
937             layout.findViewById<Button>(R.id.cfr_neg_button).apply {
938                 setOnClickListener {
939                     PrivateBrowsingShortcutCfr.cancel.record()
940                     privateBrowsingRecommend.dismiss()
941                 }
942             }
943             // We want to show the popup only after privateBrowsingButton is available.
944             // Otherwise, we will encounter an activity token error.
945             binding.privateBrowsingButton.post {
946                 runIfFragmentIsAttached {
947                     context.settings().showedPrivateModeContextualFeatureRecommender = true
948                     context.settings().lastCfrShownTimeInMillis = System.currentTimeMillis()
949                     privateBrowsingRecommend.showAsDropDown(
950                         binding.privateBrowsingButton,
951                         0,
952                         CFR_Y_OFFSET,
953                         Gravity.TOP or Gravity.END,
954                     )
955                 }
956             }
957         }
958     }
960     private fun hideOnboardingIfNeeded() {
961         if (!onboarding.userHasBeenOnboarded()) {
962             onboarding.finish()
963             requireContext().components.appStore.dispatch(
964                 AppAction.ModeChange(
965                     mode = currentMode.getCurrentMode(),
966                 ),
967             )
968         }
969     }
971     private fun hideOnboardingAndOpenSearch() {
972         hideOnboardingIfNeeded()
973         appBarLayout?.setExpanded(true, true)
974         navigateToSearch()
975     }
977     @VisibleForTesting
978     internal fun navigateToSearch() {
979         val directions =
980             HomeFragmentDirections.actionGlobalSearchDialog(
981                 sessionId = null,
982             )
984         nav(R.id.homeFragment, directions, getToolbarNavOptions(requireContext()))
986         Events.searchBarTapped.record(Events.SearchBarTappedExtra("HOME"))
987     }
989     private fun subscribeToTabCollections(): Observer<List<TabCollection>> {
990         return Observer<List<TabCollection>> {
991             requireComponents.core.tabCollectionStorage.cachedTabCollections = it
992             requireComponents.appStore.dispatch(AppAction.CollectionsChange(it))
993         }.also { observer ->
994             requireComponents.core.tabCollectionStorage.getCollections().observe(this, observer)
995         }
996     }
998     private fun registerCollectionStorageObserver() {
999         requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
1000     }
1002     private fun showRenamedSnackbar() {
1003         view?.let { view ->
1004             val string = view.context.getString(R.string.snackbar_collection_renamed)
1005             FenixSnackbar.make(
1006                 view = view,
1007                 duration = Snackbar.LENGTH_LONG,
1008                 isDisplayedWithBrowserToolbar = false,
1009             )
1010                 .setText(string)
1011                 .setAnchorView(snackbarAnchorView)
1012                 .show()
1013         }
1014     }
1016     private fun openTabsTray() {
1017         findNavController().nav(
1018             R.id.homeFragment,
1019             HomeFragmentDirections.actionGlobalTabsTrayFragment(),
1020         )
1021     }
1023     // TODO use [FenixTabCounterToolbarButton] instead of [TabCounter]:
1024     // https://github.com/mozilla-mobile/fenix/issues/16792
1025     private fun updateTabCounter(browserState: BrowserState) {
1026         val tabCount = if (browsingModeManager.mode.isPrivate) {
1027             browserState.privateTabs.size
1028         } else {
1029             browserState.normalTabs.size
1030         }
1032         binding.tabButton.setCountWithAnimation(tabCount)
1033         // The add_tabs_to_collections_button is added at runtime. We need to search for it in the same way.
1034         sessionControlView?.view?.findViewById<MaterialButton>(R.id.add_tabs_to_collections_button)
1035             ?.isVisible = tabCount > 0
1036     }
1038     @VisibleForTesting
1039     internal fun shouldEnableWallpaper() =
1040         (activity as? HomeActivity)?.themeManager?.currentTheme?.isPrivate?.not() ?: false
1042     private fun applyWallpaper(wallpaperName: String, orientationChange: Boolean) {
1043         when {
1044             !shouldEnableWallpaper() ||
1045                 (wallpaperName == lastAppliedWallpaperName && !orientationChange) -> return
1046             Wallpaper.nameIsDefault(wallpaperName) -> {
1047                 binding.wallpaperImageView.isVisible = false
1048                 lastAppliedWallpaperName = wallpaperName
1049             }
1050             else -> {
1051                 runBlockingIncrement {
1052                     // loadBitmap does file lookups based on name, so we don't need a fully
1053                     // qualified type to load the image
1054                     val wallpaper = Wallpaper.Default.copy(name = wallpaperName)
1055                     val wallpaperImage =
1056                         requireComponents.useCases.wallpaperUseCases.loadBitmap(wallpaper)
1057                     wallpaperImage?.let {
1058                         it.scaleToBottomOfView(binding.wallpaperImageView)
1059                         binding.wallpaperImageView.isVisible = true
1060                         lastAppliedWallpaperName = wallpaperName
1061                     } ?: run {
1062                         with(binding.wallpaperImageView) {
1063                             isVisible = false
1064                             showSnackBar(
1065                                 view = this,
1066                                 text = resources.getString(R.string.wallpaper_select_error_snackbar_message),
1067                             )
1068                         }
1069                         // If setting a wallpaper failed reset also the contrasting text color.
1070                         requireContext().settings().currentWallpaperTextColor = 0L
1071                         lastAppliedWallpaperName = Wallpaper.defaultName
1072                     }
1073                 }
1074             }
1075         }
1076         // Logo color should be updated in all cases.
1077         applyWallpaperTextColor()
1078     }
1080     /**
1081      * Apply a color better contrasting with the current wallpaper to the Fenix logo and private mode switcher.
1082      */
1083     @VisibleForTesting
1084     internal fun applyWallpaperTextColor() {
1085         val tintColor = when (val color = requireContext().settings().currentWallpaperTextColor.toInt()) {
1086             0 -> null // a null ColorStateList will clear the current tint
1087             else -> ColorStateList.valueOf(color)
1088         }
1090         binding.wordmarkText.imageTintList = tintColor
1091         binding.privateBrowsingButton.imageTintList = tintColor
1092     }
1094     private fun observeWallpaperUpdates() {
1095         consumeFrom(requireComponents.appStore) {
1096             val currentWallpaper = it.wallpaperState.currentWallpaper
1097             if (currentWallpaper.name != lastAppliedWallpaperName) {
1098                 applyWallpaper(wallpaperName = currentWallpaper.name, orientationChange = false)
1099             }
1100         }
1101     }
1103     companion object {
1104         const val ALL_NORMAL_TABS = "all_normal"
1105         const val ALL_PRIVATE_TABS = "all_private"
1107         private const val FOCUS_ON_ADDRESS_BAR = "focusOnAddressBar"
1109         private const val SCROLL_TO_COLLECTION = "scrollToCollection"
1110         private const val ANIM_SCROLL_DELAY = 100L
1112         private const val CFR_WIDTH_DIVIDER = 1.7
1113         private const val CFR_Y_OFFSET = -20
1115         private const val CFR_TAP_INCREASE_DPS = 6
1117         // Sponsored top sites titles and search engine names used for filtering
1118         const val AMAZON_SPONSORED_TITLE = "Amazon"
1119         const val AMAZON_SEARCH_ENGINE_NAME = "Amazon.com"
1120         const val EBAY_SPONSORED_TITLE = "eBay"
1122         // Elevation for undo toasts
1123         internal const val TOAST_ELEVATION = 80f
1124     }