[fenix] For https://github.com/mozilla-mobile/fenix/issues/25115 - Remove showWallpap...
[gecko.git] / mobile / android / fenix / app / src / main / java / org / mozilla / fenix / home / HomeFragment.kt
blobd05cb28cca7479bfccf4cdb59c01e35b347f4d29
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.Context
9 import android.content.res.Configuration
10 import android.graphics.drawable.BitmapDrawable
11 import android.graphics.drawable.ColorDrawable
12 import android.os.Bundle
13 import android.os.StrictMode
14 import android.view.Gravity
15 import android.view.LayoutInflater
16 import android.view.View
17 import android.view.ViewGroup
18 import android.view.ViewTreeObserver
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.view.isVisible
32 import androidx.core.view.updateLayoutParams
33 import androidx.fragment.app.Fragment
34 import androidx.fragment.app.activityViewModels
35 import androidx.lifecycle.Observer
36 import androidx.lifecycle.lifecycleScope
37 import androidx.navigation.fragment.findNavController
38 import androidx.navigation.fragment.navArgs
39 import com.google.android.material.appbar.AppBarLayout
40 import com.google.android.material.button.MaterialButton
41 import com.google.android.material.snackbar.Snackbar
42 import kotlinx.coroutines.Dispatchers.IO
43 import kotlinx.coroutines.Dispatchers.Main
44 import kotlinx.coroutines.flow.collect
45 import kotlinx.coroutines.flow.map
46 import kotlinx.coroutines.launch
47 import mozilla.appservices.places.BookmarkRoot
48 import mozilla.components.browser.menu.view.MenuButton
49 import mozilla.components.browser.state.selector.findTab
50 import mozilla.components.browser.state.selector.normalTabs
51 import mozilla.components.browser.state.selector.privateTabs
52 import mozilla.components.browser.state.state.BrowserState
53 import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
54 import mozilla.components.browser.state.store.BrowserStore
55 import mozilla.components.concept.storage.FrecencyThresholdOption
56 import mozilla.components.concept.sync.AccountObserver
57 import mozilla.components.concept.sync.AuthType
58 import mozilla.components.concept.sync.OAuthAccount
59 import mozilla.components.feature.tab.collections.TabCollection
60 import mozilla.components.feature.top.sites.TopSitesConfig
61 import mozilla.components.feature.top.sites.TopSitesFeature
62 import mozilla.components.feature.top.sites.TopSitesProviderConfig
63 import mozilla.components.lib.state.ext.consumeFlow
64 import mozilla.components.lib.state.ext.consumeFrom
65 import mozilla.components.service.glean.private.NoExtras
66 import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
67 import mozilla.components.support.ktx.android.content.res.resolveAttribute
68 import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
69 import mozilla.components.ui.tabcounter.TabCounterMenu
70 import org.mozilla.fenix.BrowserDirection
71 import org.mozilla.fenix.Config
72 import org.mozilla.fenix.FeatureFlags
73 import org.mozilla.fenix.GleanMetrics.Events
74 import org.mozilla.fenix.GleanMetrics.HomeScreen
75 import org.mozilla.fenix.GleanMetrics.StartOnHome
76 import org.mozilla.fenix.GleanMetrics.Wallpapers
77 import org.mozilla.fenix.HomeActivity
78 import org.mozilla.fenix.R
79 import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
80 import org.mozilla.fenix.browser.BrowserFragmentDirections
81 import org.mozilla.fenix.browser.browsingmode.BrowsingMode
82 import org.mozilla.fenix.components.FenixSnackbar
83 import org.mozilla.fenix.components.PrivateShortcutCreateManager
84 import org.mozilla.fenix.components.TabCollectionStorage
85 import org.mozilla.fenix.components.accounts.AccountState
86 import org.mozilla.fenix.components.appstate.AppAction
87 import org.mozilla.fenix.components.toolbar.FenixTabCounterMenu
88 import org.mozilla.fenix.components.toolbar.ToolbarPosition
89 import org.mozilla.fenix.databinding.FragmentHomeBinding
90 import org.mozilla.fenix.ext.components
91 import org.mozilla.fenix.ext.hideToolbar
92 import org.mozilla.fenix.ext.nav
93 import org.mozilla.fenix.ext.requireComponents
94 import org.mozilla.fenix.ext.runIfFragmentIsAttached
95 import org.mozilla.fenix.ext.settings
96 import org.mozilla.fenix.gleanplumb.DefaultMessageController
97 import org.mozilla.fenix.gleanplumb.MessagingFeature
98 import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow
99 import org.mozilla.fenix.home.pocket.DefaultPocketStoriesController
100 import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
101 import org.mozilla.fenix.home.recentbookmarks.RecentBookmarksFeature
102 import org.mozilla.fenix.home.recentbookmarks.controller.DefaultRecentBookmarksController
103 import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabFeature
104 import org.mozilla.fenix.home.recentsyncedtabs.controller.DefaultRecentSyncedTabController
105 import org.mozilla.fenix.home.recenttabs.RecentTabsListFeature
106 import org.mozilla.fenix.home.recenttabs.controller.DefaultRecentTabsController
107 import org.mozilla.fenix.home.recentvisits.RecentVisitsFeature
108 import org.mozilla.fenix.home.recentvisits.controller.DefaultRecentVisitsController
109 import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
110 import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
111 import org.mozilla.fenix.home.sessioncontrol.SessionControlView
112 import org.mozilla.fenix.home.topsites.DefaultTopSitesView
113 import org.mozilla.fenix.nimbus.FxNimbus
114 import org.mozilla.fenix.onboarding.FenixOnboarding
115 import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
116 import org.mozilla.fenix.settings.SupportUtils
117 import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
118 import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
119 import org.mozilla.fenix.tabstray.TabsTrayAccessPoint
120 import org.mozilla.fenix.theme.ThemeManager
121 import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
122 import org.mozilla.fenix.utils.ToolbarPopupWindow
123 import org.mozilla.fenix.utils.allowUndo
124 import org.mozilla.fenix.wallpapers.WallpaperManager
125 import org.mozilla.fenix.whatsnew.WhatsNew
126 import java.lang.ref.WeakReference
127 import kotlin.math.min
128 import org.mozilla.fenix.GleanMetrics.HomeMenu as HomeMenuMetrics
130 @Suppress("TooManyFunctions", "LargeClass")
131 class HomeFragment : Fragment() {
132     private val args by navArgs<HomeFragmentArgs>()
133     private lateinit var bundleArgs: Bundle
135     private var _binding: FragmentHomeBinding? = null
136     private val binding get() = _binding!!
138     private val homeViewModel: HomeScreenViewModel by activityViewModels()
140     private val snackbarAnchorView: View?
141         get() = when (requireContext().settings().toolbarPosition) {
142             ToolbarPosition.BOTTOM -> binding.toolbarLayout
143             ToolbarPosition.TOP -> null
144         }
146     private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
148     private val collectionStorageObserver = object : TabCollectionStorage.Observer {
149         @SuppressLint("NotifyDataSetChanged")
150         override fun onCollectionRenamed(tabCollection: TabCollection, title: String) {
151             lifecycleScope.launch(Main) {
152                 binding.sessionControlRecyclerView.adapter?.notifyDataSetChanged()
153             }
154             showRenamedSnackbar()
155         }
156     }
158     private val store: BrowserStore
159         get() = requireComponents.core.store
161     private val onboarding by lazy {
162         requireComponents.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
163             FenixOnboarding(requireContext())
164         }
165     }
167     private val syncedTabFeature by lazy {
168         RecentSyncedTabFeature(
169             store = requireComponents.appStore,
170             context = requireContext(),
171             storage = requireComponents.backgroundServices.syncedTabsStorage,
172             accountManager = requireComponents.backgroundServices.accountManager,
173             lifecycleOwner = viewLifecycleOwner,
174         )
175     }
177     private var _sessionControlInteractor: SessionControlInteractor? = null
178     private val sessionControlInteractor: SessionControlInteractor
179         get() = _sessionControlInteractor!!
181     private var sessionControlView: SessionControlView? = null
182     private var appBarLayout: AppBarLayout? = null
183     private lateinit var currentMode: CurrentMode
185     private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
186     private val messagingFeature = ViewBoundFeatureWrapper<MessagingFeature>()
187     private val recentTabsListFeature = ViewBoundFeatureWrapper<RecentTabsListFeature>()
188     private val recentSyncedTabFeature = ViewBoundFeatureWrapper<RecentSyncedTabFeature>()
189     private val recentBookmarksFeature = ViewBoundFeatureWrapper<RecentBookmarksFeature>()
190     private val historyMetadataFeature = ViewBoundFeatureWrapper<RecentVisitsFeature>()
192     @VisibleForTesting
193     internal var getMenuButton: () -> MenuButton? = { binding.menuButton }
195     override fun onCreate(savedInstanceState: Bundle?) {
196         // DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL!
197         val profilerStartTime = requireComponents.core.engine.profiler?.getProfilerTime()
199         super.onCreate(savedInstanceState)
201         bundleArgs = args.toBundle()
203         if (!onboarding.userHasBeenOnboarded() &&
204             requireContext().settings().shouldShowPrivacyPopWindow &&
205             Config.channel.isMozillaOnline
206         ) {
207             showPrivacyPopWindow(requireContext(), requireActivity())
208         }
210         // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
211         requireComponents.core.engine.profiler?.addMarker(
212             MarkersFragmentLifecycleCallbacks.MARKER_NAME, profilerStartTime, "HomeFragment.onCreate",
213         )
214     }
216     @Suppress("LongMethod")
217     override fun onCreateView(
218         inflater: LayoutInflater,
219         container: ViewGroup?,
220         savedInstanceState: Bundle?
221     ): View {
222         // DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL!
223         val profilerStartTime = requireComponents.core.engine.profiler?.getProfilerTime()
225         _binding = FragmentHomeBinding.inflate(inflater, container, false)
226         val activity = activity as HomeActivity
227         val components = requireComponents
229         currentMode = CurrentMode(
230             requireContext(),
231             onboarding,
232             browsingModeManager,
233             ::dispatchModeChanges
234         )
236         components.appStore.dispatch(AppAction.ModeChange(currentMode.getCurrentMode()))
238         lifecycleScope.launch(IO) {
239             if (requireContext().settings().showPocketRecommendationsFeature) {
240                 val categories = components.core.pocketStoriesService.getStories()
241                     .groupBy { story -> story.category }
242                     .map { (category, stories) -> PocketRecommendedStoriesCategory(category, stories) }
244                 components.appStore.dispatch(AppAction.PocketStoriesCategoriesChange(categories))
245             } else {
246                 components.appStore.dispatch(AppAction.PocketStoriesChange(emptyList()))
247             }
248         }
250         if (requireContext().settings().isExperimentationEnabled) {
251             messagingFeature.set(
252                 feature = MessagingFeature(
253                     store = requireComponents.appStore,
254                 ),
255                 owner = viewLifecycleOwner,
256                 view = binding.root
257             )
258         }
260         if (requireContext().settings().showTopSitesFeature) {
261             topSitesFeature.set(
262                 feature = TopSitesFeature(
263                     view = DefaultTopSitesView(
264                         store = components.appStore,
265                         settings = components.settings
266                     ),
267                     storage = components.core.topSitesStorage,
268                     config = ::getTopSitesConfig
269                 ),
270                 owner = viewLifecycleOwner,
271                 view = binding.root
272             )
273         }
275         if (requireContext().settings().showRecentTabsFeature) {
276             recentTabsListFeature.set(
277                 feature = RecentTabsListFeature(
278                     browserStore = components.core.store,
279                     appStore = components.appStore
280                 ),
281                 owner = viewLifecycleOwner,
282                 view = binding.root
283             )
285             if (FeatureFlags.taskContinuityFeature) {
286                 recentSyncedTabFeature.set(
287                     feature = syncedTabFeature,
288                     owner = viewLifecycleOwner,
289                     view = binding.root
290                 )
291             }
292         }
294         if (requireContext().settings().showRecentBookmarksFeature) {
295             recentBookmarksFeature.set(
296                 feature = RecentBookmarksFeature(
297                     appStore = components.appStore,
298                     bookmarksUseCase = run {
299                         requireContext().components.useCases.bookmarksUseCases
300                     },
301                     scope = viewLifecycleOwner.lifecycleScope
302                 ),
303                 owner = viewLifecycleOwner,
304                 view = binding.root
305             )
306         }
308         if (requireContext().settings().historyMetadataUIFeature) {
309             historyMetadataFeature.set(
310                 feature = RecentVisitsFeature(
311                     appStore = components.appStore,
312                     historyMetadataStorage = components.core.historyStorage,
313                     historyHighlightsStorage = components.core.lazyHistoryStorage,
314                     scope = viewLifecycleOwner.lifecycleScope
315                 ),
316                 owner = viewLifecycleOwner,
317                 view = binding.root
318             )
319         }
321         _sessionControlInteractor = SessionControlInteractor(
322             controller = DefaultSessionControlController(
323                 activity = activity,
324                 settings = components.settings,
325                 engine = components.core.engine,
326                 messageController = DefaultMessageController(
327                     appStore = components.appStore,
328                     messagingStorage = components.analytics.messagingStorage,
329                     homeActivity = activity,
330                 ),
331                 store = store,
332                 tabCollectionStorage = components.core.tabCollectionStorage,
333                 addTabUseCase = components.useCases.tabsUseCases.addTab,
334                 restoreUseCase = components.useCases.tabsUseCases.restore,
335                 reloadUrlUseCase = components.useCases.sessionUseCases.reload,
336                 selectTabUseCase = components.useCases.tabsUseCases.selectTab,
337                 appStore = components.appStore,
338                 navController = findNavController(),
339                 viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
340                 hideOnboarding = ::hideOnboardingAndOpenSearch,
341                 registerCollectionStorageObserver = ::registerCollectionStorageObserver,
342                 removeCollectionWithUndo = ::removeCollectionWithUndo,
343                 showTabTray = ::openTabsTray
344             ),
345             recentTabController = DefaultRecentTabsController(
346                 selectTabUseCase = components.useCases.tabsUseCases.selectTab,
347                 navController = findNavController(),
348                 store = components.core.store,
349                 appStore = components.appStore,
350             ),
351             recentSyncedTabController = DefaultRecentSyncedTabController(
352                 addNewTabUseCase = requireComponents.useCases.tabsUseCases.addTab,
353                 navController = findNavController(),
354                 accessPoint = TabsTrayAccessPoint.HomeRecentSyncedTab,
355             ),
356             recentBookmarksController = DefaultRecentBookmarksController(
357                 activity = activity,
358                 navController = findNavController(),
359                 appStore = components.appStore,
360             ),
361             recentVisitsController = DefaultRecentVisitsController(
362                 navController = findNavController(),
363                 appStore = components.appStore,
364                 selectOrAddTabUseCase = components.useCases.tabsUseCases.selectOrAddTab,
365                 storage = components.core.historyStorage,
366                 scope = viewLifecycleOwner.lifecycleScope,
367                 store = components.core.store,
368             ),
369             pocketStoriesController = DefaultPocketStoriesController(
370                 homeActivity = activity,
371                 appStore = components.appStore,
372                 navController = findNavController(),
373             )
374         )
376         updateLayout(binding.root)
377         sessionControlView = SessionControlView(
378             binding.sessionControlRecyclerView,
379             viewLifecycleOwner,
380             sessionControlInteractor
381         )
383         updateSessionControlView()
385         appBarLayout = binding.homeAppBar
387         activity.themeManager.applyStatusBarTheme(activity)
389         FxNimbus.features.homescreen.recordExposure()
391         displayWallpaperIfEnabled()
393         // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
394         requireComponents.core.engine.profiler?.addMarker(
395             MarkersFragmentLifecycleCallbacks.MARKER_NAME, profilerStartTime, "HomeFragment.onCreateView",
396         )
398         return binding.root
399     }
401     override fun onConfigurationChanged(newConfig: Configuration) {
402         super.onConfigurationChanged(newConfig)
404         getMenuButton()?.dismissMenu()
405         displayWallpaperIfEnabled()
406     }
408     /**
409      * Returns a [TopSitesConfig] which specifies how many top sites to display and whether or
410      * not frequently visited sites should be displayed.
411      */
412     @VisibleForTesting
413     internal fun getTopSitesConfig(): TopSitesConfig {
414         val settings = requireContext().settings()
415         return TopSitesConfig(
416             totalSites = settings.topSitesMaxLimit,
417             frecencyConfig = FrecencyThresholdOption.SKIP_ONE_TIME_PAGES,
418             providerConfig = TopSitesProviderConfig(
419                 showProviderTopSites = settings.showContileFeature,
420                 maxThreshold = TOP_SITES_PROVIDER_MAX_THRESHOLD
421             )
422         )
423     }
425     /**
426      * The [SessionControlView] is forced to update with our current state when we call
427      * [HomeFragment.onCreateView] in order to be able to draw everything at once with the current
428      * data in our store. The [View.consumeFrom] coroutine dispatch
429      * doesn't get run right away which means that we won't draw on the first layout pass.
430      */
431     private fun updateSessionControlView() {
432         if (browsingModeManager.mode == BrowsingMode.Private) {
433             binding.root.consumeFrom(requireContext().components.appStore, viewLifecycleOwner) {
434                 sessionControlView?.update(it)
435             }
436         } else {
437             sessionControlView?.update(requireContext().components.appStore.state)
439             binding.root.consumeFrom(requireContext().components.appStore, viewLifecycleOwner) {
440                 sessionControlView?.update(it, shouldReportMetrics = true)
441             }
442         }
443     }
445     private fun updateLayout(view: View) {
446         when (requireContext().settings().toolbarPosition) {
447             ToolbarPosition.TOP -> {
448                 binding.toolbarLayout.layoutParams = CoordinatorLayout.LayoutParams(
449                     ConstraintLayout.LayoutParams.MATCH_PARENT,
450                     ConstraintLayout.LayoutParams.WRAP_CONTENT
451                 ).apply {
452                     gravity = Gravity.TOP
453                 }
455                 ConstraintSet().apply {
456                     clone(binding.toolbarLayout)
457                     clear(binding.bottomBar.id, BOTTOM)
458                     clear(binding.bottomBarShadow.id, BOTTOM)
459                     connect(binding.bottomBar.id, TOP, PARENT_ID, TOP)
460                     connect(binding.bottomBarShadow.id, TOP, binding.bottomBar.id, BOTTOM)
461                     connect(binding.bottomBarShadow.id, BOTTOM, PARENT_ID, BOTTOM)
462                     applyTo(binding.toolbarLayout)
463                 }
465                 binding.bottomBar.background = AppCompatResources.getDrawable(
466                     view.context,
467                     view.context.theme.resolveAttribute(R.attr.bottomBarBackgroundTop)
468                 )
470                 binding.homeAppBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
471                     topMargin =
472                         resources.getDimensionPixelSize(R.dimen.home_fragment_top_toolbar_header_margin)
473                 }
474             }
475             ToolbarPosition.BOTTOM -> {
476             }
477         }
478     }
480     @Suppress("LongMethod", "ComplexMethod")
481     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
482         // DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL!
483         val profilerStartTime = requireComponents.core.engine.profiler?.getProfilerTime()
485         super.onViewCreated(view, savedInstanceState)
486         HomeScreen.homeScreenDisplayed.record(NoExtras())
487         HomeScreen.homeScreenViewCount.add()
489         observeSearchEngineChanges()
490         createHomeMenu(requireContext(), WeakReference(binding.menuButton))
491         createTabCounterMenu()
493         binding.menuButton.setColorFilter(
494             ContextCompat.getColor(
495                 requireContext(),
496                 ThemeManager.resolveAttribute(R.attr.textPrimary, requireContext())
497             )
498         )
500         binding.toolbar.compoundDrawablePadding =
501             view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding)
502         binding.toolbarWrapper.setOnClickListener {
503             navigateToSearch()
504         }
506         binding.toolbarWrapper.setOnLongClickListener {
507             ToolbarPopupWindow.show(
508                 WeakReference(it),
509                 handlePasteAndGo = sessionControlInteractor::onPasteAndGo,
510                 handlePaste = sessionControlInteractor::onPaste,
511                 copyVisible = false
512             )
513             true
514         }
516         binding.tabButton.setOnClickListener {
517             StartOnHome.openTabsTray.record(NoExtras())
518             openTabsTray()
519         }
521         PrivateBrowsingButtonView(binding.privateBrowsingButton, browsingModeManager) { newMode ->
522             sessionControlInteractor.onPrivateModeButtonClicked(
523                 newMode,
524                 onboarding.userHasBeenOnboarded()
525             )
526         }
528         consumeFrom(requireComponents.core.store) {
529             updateTabCounter(it)
530         }
532         homeViewModel.sessionToDelete?.also {
533             if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) {
534                 removeAllTabsAndShowSnackbar(it)
535             } else {
536                 removeTabAndShowSnackbar(it)
537             }
538         }
540         homeViewModel.sessionToDelete = null
542         updateTabCounter(requireComponents.core.store.state)
544         if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR)) {
545             navigateToSearch()
546         }
548         // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
549         requireComponents.core.engine.profiler?.addMarker(
550             MarkersFragmentLifecycleCallbacks.MARKER_NAME, profilerStartTime, "HomeFragment.onViewCreated",
551         )
552     }
554     private fun observeSearchEngineChanges() {
555         consumeFlow(store) { flow ->
556             flow.map { state -> state.search.selectedOrDefaultSearchEngine }
557                 .ifChanged()
558                 .collect { searchEngine ->
559                     if (searchEngine != null) {
560                         val iconSize =
561                             requireContext().resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
562                         val searchIcon =
563                             BitmapDrawable(requireContext().resources, searchEngine.icon)
564                         searchIcon.setBounds(0, 0, iconSize, iconSize)
565                         binding.searchEngineIcon.setImageDrawable(searchIcon)
566                     } else {
567                         binding.searchEngineIcon.setImageDrawable(null)
568                     }
569                 }
570         }
571     }
573     private fun createTabCounterMenu() {
574         val browsingModeManager = (activity as HomeActivity).browsingModeManager
575         val mode = browsingModeManager.mode
577         val onItemTapped: (TabCounterMenu.Item) -> Unit = {
578             if (it is TabCounterMenu.Item.NewTab) {
579                 browsingModeManager.mode = BrowsingMode.Normal
580             } else if (it is TabCounterMenu.Item.NewPrivateTab) {
581                 browsingModeManager.mode = BrowsingMode.Private
582             }
583         }
585         val tabCounterMenu = FenixTabCounterMenu(
586             requireContext(),
587             onItemTapped,
588             iconColor = if (mode == BrowsingMode.Private) {
589                 ContextCompat.getColor(requireContext(), R.color.fx_mobile_private_text_color_primary)
590             } else {
591                 null
592             }
593         )
595         val inverseBrowsingMode = when (mode) {
596             BrowsingMode.Normal -> BrowsingMode.Private
597             BrowsingMode.Private -> BrowsingMode.Normal
598         }
600         tabCounterMenu.updateMenu(showOnly = inverseBrowsingMode)
601         binding.tabButton.setOnLongClickListener {
602             tabCounterMenu.menuController.show(anchor = it)
603             true
604         }
605     }
607     private fun removeAllTabsAndShowSnackbar(sessionCode: String) {
608         if (sessionCode == ALL_PRIVATE_TABS) {
609             requireComponents.useCases.tabsUseCases.removePrivateTabs()
610         } else {
611             requireComponents.useCases.tabsUseCases.removeNormalTabs()
612         }
614         val snackbarMessage = if (sessionCode == ALL_PRIVATE_TABS) {
615             getString(R.string.snackbar_private_tabs_closed)
616         } else {
617             getString(R.string.snackbar_tabs_closed)
618         }
620         viewLifecycleOwner.lifecycleScope.allowUndo(
621             requireView(),
622             snackbarMessage,
623             requireContext().getString(R.string.snackbar_deleted_undo),
624             {
625                 requireComponents.useCases.tabsUseCases.undo.invoke()
626             },
627             operation = { },
628             anchorView = snackbarAnchorView
629         )
630     }
632     private fun removeTabAndShowSnackbar(sessionId: String) {
633         val tab = store.state.findTab(sessionId) ?: return
635         requireComponents.useCases.tabsUseCases.removeTab(sessionId)
637         val snackbarMessage = if (tab.content.private) {
638             requireContext().getString(R.string.snackbar_private_tab_closed)
639         } else {
640             requireContext().getString(R.string.snackbar_tab_closed)
641         }
643         viewLifecycleOwner.lifecycleScope.allowUndo(
644             requireView(),
645             snackbarMessage,
646             requireContext().getString(R.string.snackbar_deleted_undo),
647             {
648                 requireComponents.useCases.tabsUseCases.undo.invoke()
649                 findNavController().navigate(
650                     HomeFragmentDirections.actionGlobalBrowser(null)
651                 )
652             },
653             operation = { },
654             anchorView = snackbarAnchorView
655         )
656     }
658     override fun onDestroyView() {
659         super.onDestroyView()
661         _sessionControlInteractor = null
662         sessionControlView = null
663         appBarLayout = null
664         _binding = null
665         bundleArgs.clear()
666     }
668     override fun onStart() {
669         super.onStart()
671         subscribeToTabCollections()
673         val context = requireContext()
675         requireComponents.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
676             // By the time this code runs, we may not be attached to a context or have a view lifecycle owner.
677             if ((this@HomeFragment).view?.context == null) {
678                 return@runIfReadyOrQueue
679             }
681             requireComponents.backgroundServices.accountManager.register(
682                 currentMode,
683                 owner = this@HomeFragment.viewLifecycleOwner
684             )
685             requireComponents.backgroundServices.accountManager.register(
686                 object : AccountObserver {
687                     override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
688                         if (authType != AuthType.Existing) {
689                             view?.let {
690                                 FenixSnackbar.make(
691                                     view = it,
692                                     duration = Snackbar.LENGTH_SHORT,
693                                     isDisplayedWithBrowserToolbar = false
694                                 )
695                                     .setText(it.context.getString(R.string.onboarding_firefox_account_sync_is_on))
696                                     .setAnchorView(binding.toolbarLayout)
697                                     .show()
698                             }
699                         }
700                     }
701                 },
702                 owner = this@HomeFragment.viewLifecycleOwner
703             )
704         }
706         if (browsingModeManager.mode.isPrivate &&
707             // We will be showing the search dialog and don't want to show the CFR while the dialog shows
708             !bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) &&
709             context.settings().shouldShowPrivateModeCfr
710         ) {
711             recommendPrivateBrowsingShortcut()
712         }
714         // We only want this observer live just before we navigate away to the collection creation screen
715         requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver)
717         lifecycleScope.launch(IO) {
718             requireComponents.reviewPromptController.promptReview(requireActivity())
719         }
721         if (shouldEnableWallpaper() && context.settings().wallpapersSwitchedByLogoTap) {
722             binding.wordmark.contentDescription =
723                 context.getString(R.string.wallpaper_logo_content_description)
724             binding.wordmark.setOnClickListener {
725                 val manager = requireComponents.wallpaperManager
726                 val newWallpaper = manager.switchToNextWallpaper()
727                 Wallpapers.wallpaperSwitched.record(
728                     Wallpapers.WallpaperSwitchedExtra(
729                         name = newWallpaper.name,
730                         themeCollection = newWallpaper::class.simpleName
731                     )
732                 )
733                 manager.updateWallpaper(
734                     wallpaperContainer = binding.wallpaperImageView,
735                     newWallpaper = newWallpaper
736                 )
737             }
738         }
739     }
741     private fun dispatchModeChanges(mode: Mode) {
742         if (mode != Mode.fromBrowsingMode(browsingModeManager.mode)) {
743             requireContext().components.appStore.dispatch(AppAction.ModeChange(mode))
744         }
745     }
747     @VisibleForTesting
748     internal fun removeCollectionWithUndo(tabCollection: TabCollection) {
749         val snackbarMessage = getString(R.string.snackbar_collection_deleted)
751         lifecycleScope.allowUndo(
752             requireView(),
753             snackbarMessage,
754             getString(R.string.snackbar_deleted_undo),
755             {
756                 requireComponents.core.tabCollectionStorage.createCollection(tabCollection)
757             },
758             operation = { },
759             elevation = TOAST_ELEVATION,
760             anchorView = null
761         )
763         lifecycleScope.launch(IO) {
764             requireComponents.core.tabCollectionStorage.removeCollection(tabCollection)
765         }
766     }
768     override fun onResume() {
769         super.onResume()
770         if (browsingModeManager.mode == BrowsingMode.Private) {
771             activity?.window?.setBackgroundDrawableResource(R.drawable.private_home_background_gradient)
772         }
774         hideToolbar()
776         // Whenever a tab is selected its last access timestamp is automatically updated by A-C.
777         // However, in the case of resuming the app to the home fragment, we already have an
778         // existing selected tab, but its last access timestamp is outdated. No action is
779         // triggered to cause an automatic update on warm start (no tab selection occurs). So we
780         // update it manually here.
781         requireComponents.useCases.sessionUseCases.updateLastAccess()
782         if (shouldAnimateLogoForWallpaper()) {
783             _binding?.sessionControlRecyclerView?.viewTreeObserver?.addOnGlobalLayoutListener(
784                 homeLayoutListenerForLogoAnimation
785             )
786         }
787     }
789     // To try to find a good time to show the logo animation, we are waiting until all
790     // the sub-recyclerviews (recentBookmarks, collections, recentTabs,recentVisits
791     // and pocketStories) on the home screen have been layout.
792     private val homeLayoutListenerForLogoAnimation = object : ViewTreeObserver.OnGlobalLayoutListener {
793         override fun onGlobalLayout() {
794             _binding?.let { safeBindings ->
795                 requireComponents.wallpaperManager.animateLogoIfNeeded(safeBindings.wordmark)
796                 safeBindings.sessionControlRecyclerView.viewTreeObserver.removeOnGlobalLayoutListener(
797                     this
798                 )
799             }
800         }
801     }
803     override fun onPause() {
804         super.onPause()
805         if (browsingModeManager.mode == BrowsingMode.Private) {
806             activity?.window?.setBackgroundDrawable(
807                 ColorDrawable(
808                     ContextCompat.getColor(
809                         requireContext(),
810                         R.color.fx_mobile_private_layer_color_1
811                     )
812                 )
813             )
814         }
816         // Counterpart to the update in onResume to keep the last access timestamp of the selected
817         // tab up-to-date.
818         requireComponents.useCases.sessionUseCases.updateLastAccess()
819     }
821     @SuppressLint("InflateParams")
822     private fun recommendPrivateBrowsingShortcut() {
823         context?.let { context ->
824             val layout = LayoutInflater.from(context)
825                 .inflate(R.layout.pbm_shortcut_popup, null)
826             val privateBrowsingRecommend =
827                 PopupWindow(
828                     layout,
829                     min(
830                         (resources.displayMetrics.widthPixels / CFR_WIDTH_DIVIDER).toInt(),
831                         (resources.displayMetrics.heightPixels / CFR_WIDTH_DIVIDER).toInt()
832                     ),
833                     LinearLayout.LayoutParams.WRAP_CONTENT,
834                     true
835                 )
836             layout.findViewById<Button>(R.id.cfr_pos_button).apply {
837                 setOnClickListener {
838                     PrivateShortcutCreateManager.createPrivateShortcut(context)
839                     privateBrowsingRecommend.dismiss()
840                 }
841             }
842             layout.findViewById<Button>(R.id.cfr_neg_button).apply {
843                 setOnClickListener {
844                     privateBrowsingRecommend.dismiss()
845                 }
846             }
847             // We want to show the popup only after privateBrowsingButton is available.
848             // Otherwise, we will encounter an activity token error.
849             binding.privateBrowsingButton.post {
850                 runIfFragmentIsAttached {
851                     context.settings().showedPrivateModeContextualFeatureRecommender = true
852                     context.settings().lastCfrShownTimeInMillis = System.currentTimeMillis()
853                     privateBrowsingRecommend.showAsDropDown(
854                         binding.privateBrowsingButton, 0, CFR_Y_OFFSET, Gravity.TOP or Gravity.END
855                     )
856                 }
857             }
858         }
859     }
861     private fun hideOnboardingIfNeeded() {
862         if (!onboarding.userHasBeenOnboarded()) {
863             onboarding.finish()
864             requireContext().components.appStore.dispatch(
865                 AppAction.ModeChange(
866                     mode = currentMode.getCurrentMode()
867                 )
868             )
869         }
870     }
872     private fun hideOnboardingAndOpenSearch() {
873         hideOnboardingIfNeeded()
874         appBarLayout?.setExpanded(true, true)
875         navigateToSearch()
876     }
878     @VisibleForTesting
879     internal fun navigateToSearch() {
880         val directions =
881             HomeFragmentDirections.actionGlobalSearchDialog(
882                 sessionId = null
883             )
885         nav(R.id.homeFragment, directions, getToolbarNavOptions(requireContext()))
887         Events.searchBarTapped.record(Events.SearchBarTappedExtra("HOME"))
888     }
890     @SuppressWarnings("ComplexMethod", "LongMethod")
891     private fun createHomeMenu(context: Context, menuButtonView: WeakReference<MenuButton>) =
892         HomeMenu(
893             this.viewLifecycleOwner,
894             context,
895             onItemTapped = {
896                 if (it !is HomeMenu.Item.DesktopMode) {
897                     hideOnboardingIfNeeded()
898                 }
900                 when (it) {
901                     HomeMenu.Item.Settings -> {
902                         nav(
903                             R.id.homeFragment,
904                             HomeFragmentDirections.actionGlobalSettingsFragment()
905                         )
906                         HomeMenuMetrics.settingsItemClicked.record(NoExtras())
907                     }
908                     HomeMenu.Item.CustomizeHome -> {
909                         HomeScreen.customizeHomeClicked.record(NoExtras())
910                         nav(
911                             R.id.homeFragment,
912                             HomeFragmentDirections.actionGlobalHomeSettingsFragment()
913                         )
914                     }
915                     is HomeMenu.Item.SyncAccount -> {
916                         val directions = when (it.accountState) {
917                             AccountState.AUTHENTICATED ->
918                                 BrowserFragmentDirections.actionGlobalAccountSettingsFragment()
919                             AccountState.NEEDS_REAUTHENTICATION ->
920                                 BrowserFragmentDirections.actionGlobalAccountProblemFragment()
921                             AccountState.NO_ACCOUNT ->
922                                 BrowserFragmentDirections.actionGlobalTurnOnSync()
923                         }
924                         nav(
925                             R.id.homeFragment,
926                             directions
927                         )
928                     }
929                     HomeMenu.Item.Bookmarks -> {
930                         nav(
931                             R.id.homeFragment,
932                             HomeFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
933                         )
934                     }
935                     HomeMenu.Item.History -> {
936                         nav(
937                             R.id.homeFragment,
938                             HomeFragmentDirections.actionGlobalHistoryFragment()
939                         )
940                     }
941                     HomeMenu.Item.Downloads -> {
942                         nav(
943                             R.id.homeFragment,
944                             HomeFragmentDirections.actionGlobalDownloadsFragment()
945                         )
946                     }
947                     HomeMenu.Item.Help -> {
948                         (activity as HomeActivity).openToBrowserAndLoad(
949                             searchTermOrURL = SupportUtils.getSumoURLForTopic(context, HELP),
950                             newTab = true,
951                             from = BrowserDirection.FromHome
952                         )
953                     }
954                     HomeMenu.Item.WhatsNew -> {
955                         WhatsNew.userViewedWhatsNew(context)
956                         Events.whatsNewTapped.record(NoExtras())
957                         (activity as HomeActivity).openToBrowserAndLoad(
958                             searchTermOrURL = SupportUtils.getWhatsNewUrl(context),
959                             newTab = true,
960                             from = BrowserDirection.FromHome
961                         )
962                     }
963                     // We need to show the snackbar while the browsing data is deleting(if "Delete
964                     // browsing data on quit" is activated). After the deletion is over, the snackbar
965                     // is dismissed.
966                     HomeMenu.Item.Quit -> activity?.let { activity ->
967                         deleteAndQuit(
968                             activity,
969                             viewLifecycleOwner.lifecycleScope,
970                             view?.let { view ->
971                                 FenixSnackbar.make(
972                                     view = view,
973                                     isDisplayedWithBrowserToolbar = false
974                                 )
975                             }
976                         )
977                     }
978                     HomeMenu.Item.ReconnectSync -> {
979                         nav(
980                             R.id.homeFragment,
981                             HomeFragmentDirections.actionGlobalAccountProblemFragment()
982                         )
983                     }
984                     HomeMenu.Item.Extensions -> {
985                         nav(
986                             R.id.homeFragment,
987                             HomeFragmentDirections.actionGlobalAddonsManagementFragment()
988                         )
989                     }
990                     is HomeMenu.Item.DesktopMode -> {
991                         context.settings().openNextTabInDesktopMode = it.checked
992                     }
993                 }
994             },
995             onHighlightPresent = { menuButtonView.get()?.setHighlight(it) },
996             onMenuBuilderChanged = { menuButtonView.get()?.menuBuilder = it }
997         )
999     private fun subscribeToTabCollections(): Observer<List<TabCollection>> {
1000         return Observer<List<TabCollection>> {
1001             requireComponents.core.tabCollectionStorage.cachedTabCollections = it
1002             requireComponents.appStore.dispatch(AppAction.CollectionsChange(it))
1003         }.also { observer ->
1004             requireComponents.core.tabCollectionStorage.getCollections().observe(this, observer)
1005         }
1006     }
1008     private fun registerCollectionStorageObserver() {
1009         requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
1010     }
1012     private fun showRenamedSnackbar() {
1013         view?.let { view ->
1014             val string = view.context.getString(R.string.snackbar_collection_renamed)
1015             FenixSnackbar.make(
1016                 view = view,
1017                 duration = Snackbar.LENGTH_LONG,
1018                 isDisplayedWithBrowserToolbar = false
1019             )
1020                 .setText(string)
1021                 .setAnchorView(snackbarAnchorView)
1022                 .show()
1023         }
1024     }
1026     private fun openTabsTray() {
1027         findNavController().nav(
1028             R.id.homeFragment,
1029             HomeFragmentDirections.actionGlobalTabsTrayFragment()
1030         )
1031     }
1033     // TODO use [FenixTabCounterToolbarButton] instead of [TabCounter]:
1034     // https://github.com/mozilla-mobile/fenix/issues/16792
1035     private fun updateTabCounter(browserState: BrowserState) {
1036         val tabCount = if (browsingModeManager.mode.isPrivate) {
1037             browserState.privateTabs.size
1038         } else {
1039             browserState.normalTabs.size
1040         }
1042         binding.tabButton.setCountWithAnimation(tabCount)
1043         // The add_tabs_to_collections_button is added at runtime. We need to search for it in the same way.
1044         sessionControlView?.view?.findViewById<MaterialButton>(R.id.add_tabs_to_collections_button)
1045             ?.isVisible = tabCount > 0
1046     }
1048     private fun displayWallpaperIfEnabled() {
1049         if (shouldEnableWallpaper()) {
1050             val wallpaperManger = requireComponents.wallpaperManager
1051             // We only want to update the wallpaper when it's different from the default one
1052             // as the default is applied already on xml by default.
1053             if (wallpaperManger.currentWallpaper != WallpaperManager.defaultWallpaper) {
1054                 wallpaperManger.updateWallpaper(binding.wallpaperImageView, wallpaperManger.currentWallpaper)
1055             }
1056         }
1057     }
1059     // We want to show the animation in a time when the user less distracted
1060     // The Heuristics are:
1061     // 1) The animation hasn't shown before.
1062     // 2) The user has onboarded.
1063     // 3) It's the third time the user enters the app.
1064     // 4) The user is part of the right audience.
1065     @Suppress("MagicNumber")
1066     private fun shouldAnimateLogoForWallpaper(): Boolean {
1067         val localContext = context ?: return false
1068         val settings = localContext.settings()
1070         return shouldEnableWallpaper() && settings.shouldAnimateFirefoxLogo &&
1071             onboarding.userHasBeenOnboarded() &&
1072             settings.numberOfAppLaunches >= 3
1073     }
1075     private fun shouldEnableWallpaper() =
1076         (activity as? HomeActivity)?.themeManager?.currentTheme?.isPrivate?.not() ?: false
1078     companion object {
1079         const val ALL_NORMAL_TABS = "all_normal"
1080         const val ALL_PRIVATE_TABS = "all_private"
1082         private const val FOCUS_ON_ADDRESS_BAR = "focusOnAddressBar"
1084         private const val CFR_WIDTH_DIVIDER = 1.7
1085         private const val CFR_Y_OFFSET = -20
1087         // Elevation for undo toasts
1088         internal const val TOAST_ELEVATION = 80f
1089     }