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
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()
154 showRenamedSnackbar()
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())
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,
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>()
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
207 showPrivacyPopWindow(requireContext(), requireActivity())
210 // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
211 requireComponents.core.engine.profiler?.addMarker(
212 MarkersFragmentLifecycleCallbacks.MARKER_NAME, profilerStartTime, "HomeFragment.onCreate",
216 @Suppress("LongMethod")
217 override fun onCreateView(
218 inflater: LayoutInflater,
219 container: ViewGroup?,
220 savedInstanceState: Bundle?
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(
233 ::dispatchModeChanges
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))
246 components.appStore.dispatch(AppAction.PocketStoriesChange(emptyList()))
250 if (requireContext().settings().isExperimentationEnabled) {
251 messagingFeature.set(
252 feature = MessagingFeature(
253 store = requireComponents.appStore,
255 owner = viewLifecycleOwner,
260 if (requireContext().settings().showTopSitesFeature) {
262 feature = TopSitesFeature(
263 view = DefaultTopSitesView(
264 store = components.appStore,
265 settings = components.settings
267 storage = components.core.topSitesStorage,
268 config = ::getTopSitesConfig
270 owner = viewLifecycleOwner,
275 if (requireContext().settings().showRecentTabsFeature) {
276 recentTabsListFeature.set(
277 feature = RecentTabsListFeature(
278 browserStore = components.core.store,
279 appStore = components.appStore
281 owner = viewLifecycleOwner,
285 if (FeatureFlags.taskContinuityFeature) {
286 recentSyncedTabFeature.set(
287 feature = syncedTabFeature,
288 owner = viewLifecycleOwner,
294 if (requireContext().settings().showRecentBookmarksFeature) {
295 recentBookmarksFeature.set(
296 feature = RecentBookmarksFeature(
297 appStore = components.appStore,
298 bookmarksUseCase = run {
299 requireContext().components.useCases.bookmarksUseCases
301 scope = viewLifecycleOwner.lifecycleScope
303 owner = viewLifecycleOwner,
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
316 owner = viewLifecycleOwner,
321 _sessionControlInteractor = SessionControlInteractor(
322 controller = DefaultSessionControlController(
324 settings = components.settings,
325 engine = components.core.engine,
326 messageController = DefaultMessageController(
327 appStore = components.appStore,
328 messagingStorage = components.analytics.messagingStorage,
329 homeActivity = activity,
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
345 recentTabController = DefaultRecentTabsController(
346 selectTabUseCase = components.useCases.tabsUseCases.selectTab,
347 navController = findNavController(),
348 store = components.core.store,
349 appStore = components.appStore,
351 recentSyncedTabController = DefaultRecentSyncedTabController(
352 addNewTabUseCase = requireComponents.useCases.tabsUseCases.addTab,
353 navController = findNavController(),
354 accessPoint = TabsTrayAccessPoint.HomeRecentSyncedTab,
356 recentBookmarksController = DefaultRecentBookmarksController(
358 navController = findNavController(),
359 appStore = components.appStore,
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,
369 pocketStoriesController = DefaultPocketStoriesController(
370 homeActivity = activity,
371 appStore = components.appStore,
372 navController = findNavController(),
376 updateLayout(binding.root)
377 sessionControlView = SessionControlView(
378 binding.sessionControlRecyclerView,
380 sessionControlInteractor
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",
401 override fun onConfigurationChanged(newConfig: Configuration) {
402 super.onConfigurationChanged(newConfig)
404 getMenuButton()?.dismissMenu()
405 displayWallpaperIfEnabled()
409 * Returns a [TopSitesConfig] which specifies how many top sites to display and whether or
410 * not frequently visited sites should be displayed.
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
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.
431 private fun updateSessionControlView() {
432 if (browsingModeManager.mode == BrowsingMode.Private) {
433 binding.root.consumeFrom(requireContext().components.appStore, viewLifecycleOwner) {
434 sessionControlView?.update(it)
437 sessionControlView?.update(requireContext().components.appStore.state)
439 binding.root.consumeFrom(requireContext().components.appStore, viewLifecycleOwner) {
440 sessionControlView?.update(it, shouldReportMetrics = true)
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
452 gravity = Gravity.TOP
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)
465 binding.bottomBar.background = AppCompatResources.getDrawable(
467 view.context.theme.resolveAttribute(R.attr.bottomBarBackgroundTop)
470 binding.homeAppBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
472 resources.getDimensionPixelSize(R.dimen.home_fragment_top_toolbar_header_margin)
475 ToolbarPosition.BOTTOM -> {
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(
496 ThemeManager.resolveAttribute(R.attr.textPrimary, requireContext())
500 binding.toolbar.compoundDrawablePadding =
501 view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding)
502 binding.toolbarWrapper.setOnClickListener {
506 binding.toolbarWrapper.setOnLongClickListener {
507 ToolbarPopupWindow.show(
509 handlePasteAndGo = sessionControlInteractor::onPasteAndGo,
510 handlePaste = sessionControlInteractor::onPaste,
516 binding.tabButton.setOnClickListener {
517 StartOnHome.openTabsTray.record(NoExtras())
521 PrivateBrowsingButtonView(binding.privateBrowsingButton, browsingModeManager) { newMode ->
522 sessionControlInteractor.onPrivateModeButtonClicked(
524 onboarding.userHasBeenOnboarded()
528 consumeFrom(requireComponents.core.store) {
532 homeViewModel.sessionToDelete?.also {
533 if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) {
534 removeAllTabsAndShowSnackbar(it)
536 removeTabAndShowSnackbar(it)
540 homeViewModel.sessionToDelete = null
542 updateTabCounter(requireComponents.core.store.state)
544 if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR)) {
548 // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
549 requireComponents.core.engine.profiler?.addMarker(
550 MarkersFragmentLifecycleCallbacks.MARKER_NAME, profilerStartTime, "HomeFragment.onViewCreated",
554 private fun observeSearchEngineChanges() {
555 consumeFlow(store) { flow ->
556 flow.map { state -> state.search.selectedOrDefaultSearchEngine }
558 .collect { searchEngine ->
559 if (searchEngine != null) {
561 requireContext().resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
563 BitmapDrawable(requireContext().resources, searchEngine.icon)
564 searchIcon.setBounds(0, 0, iconSize, iconSize)
565 binding.searchEngineIcon.setImageDrawable(searchIcon)
567 binding.searchEngineIcon.setImageDrawable(null)
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
585 val tabCounterMenu = FenixTabCounterMenu(
588 iconColor = if (mode == BrowsingMode.Private) {
589 ContextCompat.getColor(requireContext(), R.color.fx_mobile_private_text_color_primary)
595 val inverseBrowsingMode = when (mode) {
596 BrowsingMode.Normal -> BrowsingMode.Private
597 BrowsingMode.Private -> BrowsingMode.Normal
600 tabCounterMenu.updateMenu(showOnly = inverseBrowsingMode)
601 binding.tabButton.setOnLongClickListener {
602 tabCounterMenu.menuController.show(anchor = it)
607 private fun removeAllTabsAndShowSnackbar(sessionCode: String) {
608 if (sessionCode == ALL_PRIVATE_TABS) {
609 requireComponents.useCases.tabsUseCases.removePrivateTabs()
611 requireComponents.useCases.tabsUseCases.removeNormalTabs()
614 val snackbarMessage = if (sessionCode == ALL_PRIVATE_TABS) {
615 getString(R.string.snackbar_private_tabs_closed)
617 getString(R.string.snackbar_tabs_closed)
620 viewLifecycleOwner.lifecycleScope.allowUndo(
623 requireContext().getString(R.string.snackbar_deleted_undo),
625 requireComponents.useCases.tabsUseCases.undo.invoke()
628 anchorView = snackbarAnchorView
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)
640 requireContext().getString(R.string.snackbar_tab_closed)
643 viewLifecycleOwner.lifecycleScope.allowUndo(
646 requireContext().getString(R.string.snackbar_deleted_undo),
648 requireComponents.useCases.tabsUseCases.undo.invoke()
649 findNavController().navigate(
650 HomeFragmentDirections.actionGlobalBrowser(null)
654 anchorView = snackbarAnchorView
658 override fun onDestroyView() {
659 super.onDestroyView()
661 _sessionControlInteractor = null
662 sessionControlView = null
668 override fun 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
681 requireComponents.backgroundServices.accountManager.register(
683 owner = this@HomeFragment.viewLifecycleOwner
685 requireComponents.backgroundServices.accountManager.register(
686 object : AccountObserver {
687 override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
688 if (authType != AuthType.Existing) {
692 duration = Snackbar.LENGTH_SHORT,
693 isDisplayedWithBrowserToolbar = false
695 .setText(it.context.getString(R.string.onboarding_firefox_account_sync_is_on))
696 .setAnchorView(binding.toolbarLayout)
702 owner = this@HomeFragment.viewLifecycleOwner
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
711 recommendPrivateBrowsingShortcut()
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())
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
733 manager.updateWallpaper(
734 wallpaperContainer = binding.wallpaperImageView,
735 newWallpaper = newWallpaper
741 private fun dispatchModeChanges(mode: Mode) {
742 if (mode != Mode.fromBrowsingMode(browsingModeManager.mode)) {
743 requireContext().components.appStore.dispatch(AppAction.ModeChange(mode))
748 internal fun removeCollectionWithUndo(tabCollection: TabCollection) {
749 val snackbarMessage = getString(R.string.snackbar_collection_deleted)
751 lifecycleScope.allowUndo(
754 getString(R.string.snackbar_deleted_undo),
756 requireComponents.core.tabCollectionStorage.createCollection(tabCollection)
759 elevation = TOAST_ELEVATION,
763 lifecycleScope.launch(IO) {
764 requireComponents.core.tabCollectionStorage.removeCollection(tabCollection)
768 override fun onResume() {
770 if (browsingModeManager.mode == BrowsingMode.Private) {
771 activity?.window?.setBackgroundDrawableResource(R.drawable.private_home_background_gradient)
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
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(
803 override fun onPause() {
805 if (browsingModeManager.mode == BrowsingMode.Private) {
806 activity?.window?.setBackgroundDrawable(
808 ContextCompat.getColor(
810 R.color.fx_mobile_private_layer_color_1
816 // Counterpart to the update in onResume to keep the last access timestamp of the selected
818 requireComponents.useCases.sessionUseCases.updateLastAccess()
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 =
830 (resources.displayMetrics.widthPixels / CFR_WIDTH_DIVIDER).toInt(),
831 (resources.displayMetrics.heightPixels / CFR_WIDTH_DIVIDER).toInt()
833 LinearLayout.LayoutParams.WRAP_CONTENT,
836 layout.findViewById<Button>(R.id.cfr_pos_button).apply {
838 PrivateShortcutCreateManager.createPrivateShortcut(context)
839 privateBrowsingRecommend.dismiss()
842 layout.findViewById<Button>(R.id.cfr_neg_button).apply {
844 privateBrowsingRecommend.dismiss()
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
861 private fun hideOnboardingIfNeeded() {
862 if (!onboarding.userHasBeenOnboarded()) {
864 requireContext().components.appStore.dispatch(
865 AppAction.ModeChange(
866 mode = currentMode.getCurrentMode()
872 private fun hideOnboardingAndOpenSearch() {
873 hideOnboardingIfNeeded()
874 appBarLayout?.setExpanded(true, true)
879 internal fun navigateToSearch() {
881 HomeFragmentDirections.actionGlobalSearchDialog(
885 nav(R.id.homeFragment, directions, getToolbarNavOptions(requireContext()))
887 Events.searchBarTapped.record(Events.SearchBarTappedExtra("HOME"))
890 @SuppressWarnings("ComplexMethod", "LongMethod")
891 private fun createHomeMenu(context: Context, menuButtonView: WeakReference<MenuButton>) =
893 this.viewLifecycleOwner,
896 if (it !is HomeMenu.Item.DesktopMode) {
897 hideOnboardingIfNeeded()
901 HomeMenu.Item.Settings -> {
904 HomeFragmentDirections.actionGlobalSettingsFragment()
906 HomeMenuMetrics.settingsItemClicked.record(NoExtras())
908 HomeMenu.Item.CustomizeHome -> {
909 HomeScreen.customizeHomeClicked.record(NoExtras())
912 HomeFragmentDirections.actionGlobalHomeSettingsFragment()
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()
929 HomeMenu.Item.Bookmarks -> {
932 HomeFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
935 HomeMenu.Item.History -> {
938 HomeFragmentDirections.actionGlobalHistoryFragment()
941 HomeMenu.Item.Downloads -> {
944 HomeFragmentDirections.actionGlobalDownloadsFragment()
947 HomeMenu.Item.Help -> {
948 (activity as HomeActivity).openToBrowserAndLoad(
949 searchTermOrURL = SupportUtils.getSumoURLForTopic(context, HELP),
951 from = BrowserDirection.FromHome
954 HomeMenu.Item.WhatsNew -> {
955 WhatsNew.userViewedWhatsNew(context)
956 Events.whatsNewTapped.record(NoExtras())
957 (activity as HomeActivity).openToBrowserAndLoad(
958 searchTermOrURL = SupportUtils.getWhatsNewUrl(context),
960 from = BrowserDirection.FromHome
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
966 HomeMenu.Item.Quit -> activity?.let { activity ->
969 viewLifecycleOwner.lifecycleScope,
973 isDisplayedWithBrowserToolbar = false
978 HomeMenu.Item.ReconnectSync -> {
981 HomeFragmentDirections.actionGlobalAccountProblemFragment()
984 HomeMenu.Item.Extensions -> {
987 HomeFragmentDirections.actionGlobalAddonsManagementFragment()
990 is HomeMenu.Item.DesktopMode -> {
991 context.settings().openNextTabInDesktopMode = it.checked
995 onHighlightPresent = { menuButtonView.get()?.setHighlight(it) },
996 onMenuBuilderChanged = { menuButtonView.get()?.menuBuilder = it }
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)
1008 private fun registerCollectionStorageObserver() {
1009 requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
1012 private fun showRenamedSnackbar() {
1014 val string = view.context.getString(R.string.snackbar_collection_renamed)
1017 duration = Snackbar.LENGTH_LONG,
1018 isDisplayedWithBrowserToolbar = false
1021 .setAnchorView(snackbarAnchorView)
1026 private fun openTabsTray() {
1027 findNavController().nav(
1029 HomeFragmentDirections.actionGlobalTabsTrayFragment()
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
1039 browserState.normalTabs.size
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
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)
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
1075 private fun shouldEnableWallpaper() =
1076 (activity as? HomeActivity)?.themeManager?.currentTheme?.isPrivate?.not() ?: false
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