[fenix] For https://github.com/mozilla-mobile/fenix/issues/24991 - Refactor the HomeMenu creation from HomeFragment to HomeMenuBuilder

pull/600/head
Gabriel Luong 2 years ago committed by mergify[bot]
parent 4eb6d2ea18
commit b04e075f51

@ -5,7 +5,6 @@
package org.mozilla.fenix.home
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
@ -39,10 +38,8 @@ import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.menu.view.MenuButton
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.normalTabs
@ -65,7 +62,6 @@ import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.content.res.resolveAttribute
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.ui.tabcounter.TabCounterMenu
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.GleanMetrics.Events
@ -75,12 +71,10 @@ import org.mozilla.fenix.GleanMetrics.Wallpapers
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.PrivateShortcutCreateManager
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.accounts.AccountState
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.toolbar.FenixTabCounterMenu
import org.mozilla.fenix.components.toolbar.ToolbarPosition
@ -111,19 +105,13 @@ import org.mozilla.fenix.home.topsites.DefaultTopSitesView
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
import org.mozilla.fenix.tabstray.TabsTrayAccessPoint
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
import org.mozilla.fenix.utils.ToolbarPopupWindow
import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.wallpapers.WallpaperManager
import org.mozilla.fenix.whatsnew.WhatsNew
import java.lang.ref.WeakReference
import kotlin.math.min
import org.mozilla.fenix.GleanMetrics.HomeMenu as HomeMenuMetrics
@Suppress("TooManyFunctions", "LargeClass")
class HomeFragment : Fragment() {
@ -493,15 +481,18 @@ class HomeFragment : Fragment() {
observeSearchEngineChanges()
observeSearchEngineNameChanges()
createHomeMenu(requireContext(), WeakReference(binding.menuButton))
createTabCounterMenu()
binding.menuButton.setColorFilter(
ContextCompat.getColor(
requireContext(),
ThemeManager.resolveAttribute(R.attr.textPrimary, requireContext())
)
)
HomeMenuBuilder(
view = view,
context = view.context,
lifecycleOwner = viewLifecycleOwner,
homeActivity = activity as HomeActivity,
navController = findNavController(),
menuButton = WeakReference(binding.menuButton),
hideOnboardingIfNeeded = ::hideOnboardingIfNeeded,
).build()
createTabCounterMenu()
binding.toolbar.compoundDrawablePadding =
view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding)
@ -914,115 +905,6 @@ class HomeFragment : Fragment() {
Events.searchBarTapped.record(Events.SearchBarTappedExtra("HOME"))
}
@SuppressWarnings("ComplexMethod", "LongMethod")
private fun createHomeMenu(context: Context, menuButtonView: WeakReference<MenuButton>) =
HomeMenu(
this.viewLifecycleOwner,
context,
onItemTapped = {
if (it !is HomeMenu.Item.DesktopMode) {
hideOnboardingIfNeeded()
}
when (it) {
HomeMenu.Item.Settings -> {
nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalSettingsFragment()
)
HomeMenuMetrics.settingsItemClicked.record(NoExtras())
}
HomeMenu.Item.CustomizeHome -> {
HomeScreen.customizeHomeClicked.record(NoExtras())
nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalHomeSettingsFragment()
)
}
is HomeMenu.Item.SyncAccount -> {
val directions = when (it.accountState) {
AccountState.AUTHENTICATED ->
BrowserFragmentDirections.actionGlobalAccountSettingsFragment()
AccountState.NEEDS_REAUTHENTICATION ->
BrowserFragmentDirections.actionGlobalAccountProblemFragment()
AccountState.NO_ACCOUNT ->
BrowserFragmentDirections.actionGlobalTurnOnSync()
}
nav(
R.id.homeFragment,
directions
)
}
HomeMenu.Item.Bookmarks -> {
nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
)
}
HomeMenu.Item.History -> {
nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalHistoryFragment()
)
}
HomeMenu.Item.Downloads -> {
nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalDownloadsFragment()
)
}
HomeMenu.Item.Help -> {
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getSumoURLForTopic(context, HELP),
newTab = true,
from = BrowserDirection.FromHome
)
}
HomeMenu.Item.WhatsNew -> {
WhatsNew.userViewedWhatsNew(context)
Events.whatsNewTapped.record(NoExtras())
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getWhatsNewUrl(context),
newTab = true,
from = BrowserDirection.FromHome
)
}
// We need to show the snackbar while the browsing data is deleting(if "Delete
// browsing data on quit" is activated). After the deletion is over, the snackbar
// is dismissed.
HomeMenu.Item.Quit -> activity?.let { activity ->
deleteAndQuit(
activity,
viewLifecycleOwner.lifecycleScope,
view?.let { view ->
FenixSnackbar.make(
view = view,
isDisplayedWithBrowserToolbar = false
)
}
)
}
HomeMenu.Item.ReconnectSync -> {
nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalAccountProblemFragment()
)
}
HomeMenu.Item.Extensions -> {
nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalAddonsManagementFragment()
)
}
is HomeMenu.Item.DesktopMode -> {
context.settings().openNextTabInDesktopMode = it.checked
}
}
},
onHighlightPresent = { menuButtonView.get()?.setHighlight(it) },
onMenuBuilderChanged = { menuButtonView.get()?.menuBuilder = it }
)
private fun subscribeToTabCollections(): Observer<List<TabCollection>> {
return Observer<List<TabCollection>> {
requireComponents.core.tabCollectionStorage.cachedTabCollections = it

@ -0,0 +1,185 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home
import android.content.Context
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.menu.view.MenuButton
import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.HomeScreen
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.accounts.AccountState
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.whatsnew.WhatsNew
import java.lang.ref.WeakReference
import org.mozilla.fenix.GleanMetrics.HomeMenu as HomeMenuMetrics
/**
* Helper class for building the [HomeMenu].
*
* @property view The [View] to attach the snackbar to.
* @property context An Android [Context].
* @property lifecycleOwner [LifecycleOwner] for the view.
* @property homeActivity [HomeActivity] used to open URLs in a new tab.
* @property navController [NavController] used for navigation.
* @property menuButton The [MenuButton] that will be used to create a menu when the button is
* clicked.
* @property hideOnboardingIfNeeded Lambda invoked to dismiss onboarding.
*/
@Suppress("LongParameterList")
class HomeMenuBuilder(
private val view: View,
private val context: Context,
private val lifecycleOwner: LifecycleOwner,
private val homeActivity: HomeActivity,
private val navController: NavController,
private val menuButton: WeakReference<MenuButton>,
private val hideOnboardingIfNeeded: () -> Unit,
) {
/**
* Builds the [HomeMenu].
*/
fun build() {
HomeMenu(
lifecycleOwner = lifecycleOwner,
context = context,
onItemTapped = ::onItemTapped,
onHighlightPresent = { menuButton.get()?.setHighlight(it) },
onMenuBuilderChanged = { menuButton.get()?.menuBuilder = it }
)
menuButton.get()?.setColorFilter(
ContextCompat.getColor(
context,
ThemeManager.resolveAttribute(R.attr.textPrimary, context)
)
)
}
/**
* Callback invoked when a menu item is tapped on.
*/
@Suppress("LongMethod")
@VisibleForTesting(otherwise = PRIVATE)
internal fun onItemTapped(item: HomeMenu.Item) {
if (item !is HomeMenu.Item.DesktopMode) {
hideOnboardingIfNeeded()
}
when (item) {
HomeMenu.Item.Settings -> {
HomeMenuMetrics.settingsItemClicked.record(NoExtras())
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalSettingsFragment()
)
}
HomeMenu.Item.CustomizeHome -> {
HomeScreen.customizeHomeClicked.record(NoExtras())
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalHomeSettingsFragment()
)
}
is HomeMenu.Item.SyncAccount -> {
navController.nav(
R.id.homeFragment,
when (item.accountState) {
AccountState.AUTHENTICATED ->
HomeFragmentDirections.actionGlobalAccountSettingsFragment()
AccountState.NEEDS_REAUTHENTICATION ->
HomeFragmentDirections.actionGlobalAccountProblemFragment()
AccountState.NO_ACCOUNT ->
HomeFragmentDirections.actionGlobalTurnOnSync()
}
)
}
HomeMenu.Item.Bookmarks -> {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
)
}
HomeMenu.Item.History -> {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalHistoryFragment()
)
}
HomeMenu.Item.Downloads -> {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalDownloadsFragment()
)
}
HomeMenu.Item.Help -> {
homeActivity.openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getSumoURLForTopic(
context = context,
topic = SupportUtils.SumoTopic.HELP
),
newTab = true,
from = BrowserDirection.FromHome
)
}
HomeMenu.Item.WhatsNew -> {
WhatsNew.userViewedWhatsNew(context)
Events.whatsNewTapped.record(NoExtras())
homeActivity.openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getWhatsNewUrl(context),
newTab = true,
from = BrowserDirection.FromHome
)
}
HomeMenu.Item.Quit -> {
// We need to show the snackbar while the browsing data is deleting (if "Delete
// browsing data on quit" is activated). After the deletion is over, the snackbar
// is dismissed.
deleteAndQuit(
activity = homeActivity,
coroutineScope = lifecycleOwner.lifecycleScope,
snackbar = FenixSnackbar.make(
view = view,
isDisplayedWithBrowserToolbar = false
)
)
}
HomeMenu.Item.ReconnectSync -> {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalAccountProblemFragment()
)
}
HomeMenu.Item.Extensions -> {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalAddonsManagementFragment()
)
}
is HomeMenu.Item.DesktopMode -> {
context.settings().openNextTabInDesktopMode = item.checked
}
}
}
}

@ -0,0 +1,235 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home
import android.view.View
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.menu.view.MenuButton
import mozilla.components.support.test.robolectric.testContext
import mozilla.telemetry.glean.testing.GleanTestRule
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.HomeScreen
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.accounts.AccountState
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.whatsnew.WhatsNew
import java.lang.ref.WeakReference
import org.mozilla.fenix.GleanMetrics.HomeMenu as HomeMenuMetrics
@RunWith(FenixRobolectricTestRunner::class)
class HomeMenuBuilderTest {
@get:Rule
val gleanTestRule = GleanTestRule(testContext)
private lateinit var view: View
private lateinit var lifecycleOwner: LifecycleOwner
private lateinit var homeActivity: HomeActivity
private lateinit var navController: NavController
private lateinit var menuButton: WeakReference<MenuButton>
private lateinit var homeMenuBuilder: HomeMenuBuilder
@Before
fun setup() {
view = mockk(relaxed = true)
lifecycleOwner = mockk(relaxed = true)
homeActivity = mockk(relaxed = true)
menuButton = mockk(relaxed = true)
navController = mockk(relaxed = true)
homeMenuBuilder = HomeMenuBuilder(
view = view,
context = testContext,
lifecycleOwner = lifecycleOwner,
homeActivity = homeActivity,
navController = navController,
menuButton = menuButton,
hideOnboardingIfNeeded = {},
)
}
@Test
fun `WHEN Settings menu item is tapped THEN navigate to settings fragment and record metrics`() {
assertFalse(HomeMenuMetrics.settingsItemClicked.testHasValue())
homeMenuBuilder.onItemTapped(HomeMenu.Item.Settings)
assertTrue(HomeMenuMetrics.settingsItemClicked.testHasValue())
verify {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalSettingsFragment()
)
}
}
@Test
fun `WHEN Customize Home menu item is tapped THEN navigate to home settings fragment and record metrics`() {
assertFalse(HomeScreen.customizeHomeClicked.testHasValue())
homeMenuBuilder.onItemTapped(HomeMenu.Item.CustomizeHome)
assertTrue(HomeScreen.customizeHomeClicked.testHasValue())
verify {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalHomeSettingsFragment()
)
}
}
@Test
fun `GIVEN various sync account state WHEN Sync Account menu item is tapped THEN navigate to the appropriate sync fragment`() {
homeMenuBuilder.onItemTapped(HomeMenu.Item.SyncAccount(AccountState.AUTHENTICATED))
verify {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalAccountSettingsFragment()
)
}
homeMenuBuilder.onItemTapped(HomeMenu.Item.SyncAccount(AccountState.NEEDS_REAUTHENTICATION))
verify {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalAccountProblemFragment()
)
}
homeMenuBuilder.onItemTapped(HomeMenu.Item.SyncAccount(AccountState.NO_ACCOUNT))
verify {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalTurnOnSync()
)
}
}
@Test
fun `WHEN Bookmarks menu item is tapped THEN navigate to the bookmarks fragment`() {
homeMenuBuilder.onItemTapped(HomeMenu.Item.Bookmarks)
verify {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
)
}
}
@Test
fun `WHEN History menu item is tapped THEN navigate to the history fragment`() {
homeMenuBuilder.onItemTapped(HomeMenu.Item.History)
verify {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalHistoryFragment()
)
}
}
@Test
fun `WHEN Downloads menu item is tapped THEN navigate to the downloads fragment`() {
homeMenuBuilder.onItemTapped(HomeMenu.Item.Downloads)
verify {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalDownloadsFragment()
)
}
}
@Test
fun `WHEN Help menu item is tapped THEN open the browser to the SUMO help page`() {
homeMenuBuilder.onItemTapped(HomeMenu.Item.Help)
verify {
homeActivity.openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getSumoURLForTopic(
context = testContext,
topic = SupportUtils.SumoTopic.HELP
),
newTab = true,
from = BrowserDirection.FromHome
)
}
}
@Test
fun `WHEN Whats New menu item is tapped THEN open the browser to the SUMO whats new page and record metrics`() {
assertFalse(Events.whatsNewTapped.testHasValue())
homeMenuBuilder.onItemTapped(HomeMenu.Item.WhatsNew)
assertTrue(Events.whatsNewTapped.testHasValue())
verify {
WhatsNew.userViewedWhatsNew(testContext)
homeActivity.openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getWhatsNewUrl(testContext),
newTab = true,
from = BrowserDirection.FromHome
)
}
}
@Test
fun `WHEN Reconnect Sync menu item is tapped THEN navigate to the account problem fragment`() {
homeMenuBuilder.onItemTapped(HomeMenu.Item.ReconnectSync)
verify {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalAccountProblemFragment()
)
}
}
@Test
fun `WHEN Extensions menu item is tapped THEN navigate to the addons management fragment`() {
homeMenuBuilder.onItemTapped(HomeMenu.Item.Extensions)
verify {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalAddonsManagementFragment()
)
}
}
@Test
fun `WHEN Desktop Mode menu item is tapped THEN set the desktop mode settings`() {
every { testContext.settings() } returns Settings(testContext)
homeMenuBuilder.onItemTapped(HomeMenu.Item.DesktopMode(checked = true))
assertTrue(testContext.settings().openNextTabInDesktopMode)
}
}
Loading…
Cancel
Save