[fenix] For https://github.com/mozilla-mobile/fenix/issues/28264 - Refactor synced tabs handling out of `NavigationInteractor`

pull/600/head
Noah Bond 2 years ago committed by mergify[bot]
parent 6cb2f9da59
commit 721316f905

@ -15,11 +15,9 @@ import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.service.glean.private.NoExtras import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Collections import org.mozilla.fenix.GleanMetrics.Collections
import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.TabsTray import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.collections.CollectionsDialog import org.mozilla.fenix.collections.CollectionsDialog
import org.mozilla.fenix.collections.show import org.mozilla.fenix.collections.show
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
@ -28,7 +26,6 @@ import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.tabstray.ext.getTabSessionState import org.mozilla.fenix.tabstray.ext.getTabSessionState
import org.mozilla.fenix.tabstray.ext.isActiveDownload import org.mozilla.fenix.tabstray.ext.isActiveDownload
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import mozilla.components.browser.storage.sync.Tab as SyncTab
/** /**
* An interactor that helps with navigating to different parts of the app from the tabs tray. * An interactor that helps with navigating to different parts of the app from the tabs tray.
@ -85,11 +82,6 @@ interface NavigationInteractor {
* Used when adding [TabSessionState]s as bookmarks. * Used when adding [TabSessionState]s as bookmarks.
*/ */
fun onSaveToBookmarks(tabs: Collection<TabSessionState>) fun onSaveToBookmarks(tabs: Collection<TabSessionState>)
/**
* Called when clicking on a SyncedTab item.
*/
fun onSyncedTabClicked(tab: SyncTab)
} }
/** /**
@ -98,7 +90,6 @@ interface NavigationInteractor {
@Suppress("LongParameterList", "TooManyFunctions") @Suppress("LongParameterList", "TooManyFunctions")
class DefaultNavigationInteractor( class DefaultNavigationInteractor(
private val context: Context, private val context: Context,
private val activity: HomeActivity,
private val browserStore: BrowserStore, private val browserStore: BrowserStore,
private val navController: NavController, private val navController: NavController,
private val dismissTabTray: () -> Unit, private val dismissTabTray: () -> Unit,
@ -244,15 +235,4 @@ class DefaultNavigationInteractor(
showBookmarkSnackbar(tabs.size) showBookmarkSnackbar(tabs.size)
} }
override fun onSyncedTabClicked(tab: SyncTab) {
Events.syncedTabOpened.record(NoExtras())
dismissTabTray()
activity.openToBrowserAndLoad(
searchTermOrURL = tab.active().url,
newTab = true,
from = BrowserDirection.FromTabsTray,
)
}
} }

@ -0,0 +1,19 @@
/* 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.tabstray
import mozilla.components.browser.storage.sync.Tab
/**
* Controller for handling any actions on synced tabs in the tabs tray.
*/
interface SyncedTabsController {
/**
* Handles a synced tab item click.
*
* @param tab The synced [Tab] that was clicked.
*/
fun handleSyncedTabClicked(tab: Tab)
}

@ -0,0 +1,19 @@
/* 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.tabstray
import mozilla.components.browser.storage.sync.Tab
/**
* Interactor for responding to any actions on synced tabs in the tabs tray.
*/
interface SyncedTabsInteractor {
/**
* Invoked when the user clicks on a synced [Tab].
*
* @param tab The synced [Tab] that was clicked.
*/
fun onSyncedTabClicked(tab: Tab)
}

@ -10,16 +10,20 @@ import mozilla.components.browser.state.action.DebugAction
import mozilla.components.browser.state.action.LastAccessAction import mozilla.components.browser.state.action.LastAccessAction
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.concept.base.profiler.Profiler import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.concept.engine.mediasession.MediaSession.PlaybackState import mozilla.components.concept.engine.mediasession.MediaSession.PlaybackState
import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.lib.state.DelicateAction import mozilla.components.lib.state.DelicateAction
import mozilla.telemetry.glean.private.NoExtras import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.Tab import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.TabsTray import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
@ -28,8 +32,12 @@ import org.mozilla.fenix.ext.DEFAULT_ACTIVE_DAYS
import org.mozilla.fenix.home.HomeFragment import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.tabstray.ext.isActiveDownload import org.mozilla.fenix.tabstray.ext.isActiveDownload
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import org.mozilla.fenix.GleanMetrics.Tab as GleanTab
interface TabsTrayController { /**
* Controller for handling any actions in the tabs tray.
*/
interface TabsTrayController : SyncedTabsController {
/** /**
* Called to open a new tab. * Called to open a new tab.
@ -107,8 +115,25 @@ interface TabsTrayController {
fun handleMediaClicked(tab: SessionState) fun handleMediaClicked(tab: SessionState)
} }
/**
* Default implementation of [TabsTrayController].
*
* @property activity [HomeActivity] used to perform top-level app actions.
* @property trayStore [TabsTrayStore] used to read/update the [TabsTrayState].
* @property browserStore [BrowserStore] used to read/update the current [BrowserState].
* @property browsingModeManager [BrowsingModeManager] used to read/update the current [BrowsingMode].
* @property navController [NavController] used to navigate away from the tabs tray.
* @property navigateToHomeAndDeleteSession Lambda used to return to the Homescreen and delete the current session.
* @property navigationInteractor [NavigationInteractor] used to perform navigation actions with side effects.
* @property tabsUseCases Use case wrapper for interacting with tabs.
* @property selectTabPosition Lambda used to scroll the tabs tray to the desired position.
* @property dismissTray Lambda used to dismiss/minimize the tabs tray.
* @property showUndoSnackbarForTab Lambda used to display an UNDO Snackbar.
* @property showCancelledDownloadWarning Lambda used to display a cancelled download warning.
*/
@Suppress("TooManyFunctions", "LongParameterList") @Suppress("TooManyFunctions", "LongParameterList")
class DefaultTabsTrayController( class DefaultTabsTrayController(
private val activity: HomeActivity,
private val trayStore: TabsTrayStore, private val trayStore: TabsTrayStore,
private val browserStore: BrowserStore, private val browserStore: BrowserStore,
private val browsingModeManager: BrowsingModeManager, private val browsingModeManager: BrowsingModeManager,
@ -121,7 +146,6 @@ class DefaultTabsTrayController(
private val dismissTray: () -> Unit, private val dismissTray: () -> Unit,
private val showUndoSnackbarForTab: (Boolean) -> Unit, private val showUndoSnackbarForTab: (Boolean) -> Unit,
internal val showCancelledDownloadWarning: (downloadCount: Int, tabId: String?, source: String?) -> Unit, internal val showCancelledDownloadWarning: (downloadCount: Int, tabId: String?, source: String?) -> Unit,
) : TabsTrayController { ) : TabsTrayController {
override fun handleOpeningNewTab(isPrivate: Boolean) { override fun handleOpeningNewTab(isPrivate: Boolean) {
@ -279,12 +303,12 @@ class DefaultTabsTrayController(
override fun handleMediaClicked(tab: SessionState) { override fun handleMediaClicked(tab: SessionState) {
when (tab.mediaSessionState?.playbackState) { when (tab.mediaSessionState?.playbackState) {
PlaybackState.PLAYING -> { PlaybackState.PLAYING -> {
Tab.mediaPause.record(NoExtras()) GleanTab.mediaPause.record(NoExtras())
tab.mediaSessionState?.controller?.pause() tab.mediaSessionState?.controller?.pause()
} }
PlaybackState.PAUSED -> { PlaybackState.PAUSED -> {
Tab.mediaPlay.record(NoExtras()) GleanTab.mediaPlay.record(NoExtras())
tab.mediaSessionState?.controller?.play() tab.mediaSessionState?.controller?.play()
} }
else -> throw AssertionError( else -> throw AssertionError(
@ -292,4 +316,15 @@ class DefaultTabsTrayController(
) )
} }
} }
override fun handleSyncedTabClicked(tab: Tab) {
Events.syncedTabOpened.record(NoExtras())
dismissTray()
activity.openToBrowserAndLoad(
searchTermOrURL = tab.active().url,
newTab = true,
from = BrowserDirection.FromTabsTray,
)
}
} }

@ -197,7 +197,6 @@ class TabsTrayFragment : AppCompatDialogFragment() {
navigationInteractor = navigationInteractor =
DefaultNavigationInteractor( DefaultNavigationInteractor(
context = requireContext(), context = requireContext(),
activity = activity,
tabsTrayStore = tabsTrayStore, tabsTrayStore = tabsTrayStore,
browserStore = requireComponents.core.store, browserStore = requireComponents.core.store,
navController = findNavController(), navController = findNavController(),
@ -213,6 +212,7 @@ class TabsTrayFragment : AppCompatDialogFragment() {
) )
tabsTrayController = DefaultTabsTrayController( tabsTrayController = DefaultTabsTrayController(
activity = activity,
trayStore = tabsTrayStore, trayStore = tabsTrayStore,
browserStore = requireComponents.core.store, browserStore = requireComponents.core.store,
browsingModeManager = activity.browsingModeManager, browsingModeManager = activity.browsingModeManager,

@ -5,8 +5,12 @@
package org.mozilla.fenix.tabstray package org.mozilla.fenix.tabstray
import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.storage.sync.Tab
interface TabsTrayInteractor { /**
* Interactor for responding to all user actions in the tabs tray.
*/
interface TabsTrayInteractor : SyncedTabsInteractor {
/** /**
* Set the current tray item to the clamped [position]. * Set the current tray item to the clamped [position].
* *
@ -53,7 +57,7 @@ interface TabsTrayInteractor {
} }
/** /**
* Interactor to be called for any tabs tray user actions. * Default implementation of [TabsTrayInteractor].
* *
* @property controller [TabsTrayController] to which user actions can be delegated for actual app update. * @property controller [TabsTrayController] to which user actions can be delegated for actual app update.
*/ */
@ -91,4 +95,8 @@ class DefaultTabsTrayInteractor(
override fun onInactiveDebugClicked(tabs: Collection<TabSessionState>) { override fun onInactiveDebugClicked(tabs: Collection<TabSessionState>) {
controller.forceTabsAsInactive(tabs) controller.forceTabsAsInactive(tabs)
} }
override fun onSyncedTabClicked(tab: Tab) {
controller.handleSyncedTabClicked(tab)
}
} }

@ -91,7 +91,7 @@ class TrayPagerAdapter(
) )
}, },
tabsTrayStore = tabsTrayStore, tabsTrayStore = tabsTrayStore,
navigationInteractor = navInteractor, interactor = tabsTrayInteractor,
) )
} }
else -> throw IllegalStateException("Unknown viewType.") else -> throw IllegalStateException("Unknown viewType.")

@ -9,7 +9,7 @@ import androidx.compose.ui.platform.ComposeView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import mozilla.components.lib.state.ext.observeAsComposableState import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.tabstray.NavigationInteractor import org.mozilla.fenix.tabstray.SyncedTabsInteractor
import org.mozilla.fenix.tabstray.TabsTrayState import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsList import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsList
@ -21,12 +21,12 @@ import org.mozilla.fenix.theme.Theme
* *
* @param composeView Root ComposeView passed-in from TrayPagerAdapter. * @param composeView Root ComposeView passed-in from TrayPagerAdapter.
* @param tabsTrayStore Store used as a Composable State to listen for changes to [TabsTrayState.syncedTabs]. * @param tabsTrayStore Store used as a Composable State to listen for changes to [TabsTrayState.syncedTabs].
* @param navigationInteractor The lambda for handling clicks on synced tabs. * @param interactor [SyncedTabsInteractor] used to respond to interactions with synced tabs.
*/ */
class SyncedTabsPageViewHolder( class SyncedTabsPageViewHolder(
private val composeView: ComposeView, private val composeView: ComposeView,
private val tabsTrayStore: TabsTrayStore, private val tabsTrayStore: TabsTrayStore,
private val navigationInteractor: NavigationInteractor, private val interactor: SyncedTabsInteractor,
) : AbstractPageViewHolder(composeView) { ) : AbstractPageViewHolder(composeView) {
fun bind() { fun bind() {
@ -36,7 +36,7 @@ class SyncedTabsPageViewHolder(
SyncedTabsList( SyncedTabsList(
syncedTabs = tabs ?: emptyList(), syncedTabs = tabs ?: emptyList(),
taskContinuityEnabled = composeView.context.settings().enableTaskContinuityEnhancements, taskContinuityEnabled = composeView.context.settings().enableTaskContinuityEnhancements,
onTabClick = navigationInteractor::onSyncedTabClicked, onTabClick = interactor::onSyncedTabClicked,
) )
} }
} }

@ -22,6 +22,8 @@ import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.browser.storage.sync.TabEntry
import mozilla.components.concept.base.profiler.Profiler import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.service.glean.testing.GleanTestRule import mozilla.components.service.glean.testing.GleanTestRule
@ -35,7 +37,10 @@ import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.TabsTray import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ -64,6 +69,9 @@ class DefaultTabsTrayControllerTest {
@MockK(relaxed = true) @MockK(relaxed = true)
private lateinit var tabsUseCases: TabsUseCases private lateinit var tabsUseCases: TabsUseCases
@MockK(relaxed = true)
private lateinit var activity: HomeActivity
@get:Rule @get:Rule
val gleanTestRule = GleanTestRule(testContext) val gleanTestRule = GleanTestRule(testContext)
@ -475,6 +483,34 @@ class DefaultTabsTrayControllerTest {
assertTrue(navigateToHomeAndDeleteSessionInvoked) assertTrue(navigateToHomeAndDeleteSessionInvoked)
} }
@Test
fun `WHEN a synced tab is clicked THEN the metrics are reported and the tab is opened`() {
val tab = mockk<Tab>()
val entry = mockk<TabEntry>()
assertNull(Events.syncedTabOpened.testGetValue())
every { tab.active() }.answers { entry }
every { entry.url }.answers { "https://mozilla.org" }
var dismissTabTrayInvoked = false
createController(
dismissTray = {
dismissTabTrayInvoked = true
},
).handleSyncedTabClicked(tab)
assertTrue(dismissTabTrayInvoked)
assertNotNull(Events.syncedTabOpened.testGetValue())
verify {
activity.openToBrowserAndLoad(
searchTermOrURL = "https://mozilla.org",
newTab = true,
from = BrowserDirection.FromTabsTray,
)
}
}
private fun createController( private fun createController(
navigateToHomeAndDeleteSession: (String) -> Unit = { }, navigateToHomeAndDeleteSession: (String) -> Unit = { },
selectTabPosition: (Int, Boolean) -> Unit = { _, _ -> }, selectTabPosition: (Int, Boolean) -> Unit = { _, _ -> },
@ -483,6 +519,7 @@ class DefaultTabsTrayControllerTest {
showCancelledDownloadWarning: (Int, String?, String?) -> Unit = { _, _, _ -> }, showCancelledDownloadWarning: (Int, String?, String?) -> Unit = { _, _, _ -> },
): DefaultTabsTrayController { ): DefaultTabsTrayController {
return DefaultTabsTrayController( return DefaultTabsTrayController(
activity,
trayStore, trayStore,
browserStore, browserStore,
browsingModeManager, browsingModeManager,

@ -20,7 +20,6 @@ import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.storage.sync.TabEntry
import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.service.glean.testing.GleanTestRule import mozilla.components.service.glean.testing.GleanTestRule
import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.robolectric.testContext
@ -35,17 +34,14 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.RuleChain import org.junit.rules.RuleChain
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.TabsTray import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.collections.CollectionsDialog import org.mozilla.fenix.collections.CollectionsDialog
import org.mozilla.fenix.collections.show import org.mozilla.fenix.collections.show
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.bookmarks.BookmarksUseCase import org.mozilla.fenix.components.bookmarks.BookmarksUseCase
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import mozilla.components.browser.state.state.createTab as createStateTab import mozilla.components.browser.state.state.createTab as createStateTab
import mozilla.components.browser.storage.sync.Tab as SyncTab
@RunWith(FenixRobolectricTestRunner::class) // for gleanTestRule @RunWith(FenixRobolectricTestRunner::class) // for gleanTestRule
class NavigationInteractorTest { class NavigationInteractorTest {
@ -57,7 +53,6 @@ class NavigationInteractorTest {
private val context: Context = mockk(relaxed = true) private val context: Context = mockk(relaxed = true)
private val collectionStorage: TabCollectionStorage = mockk(relaxed = true) private val collectionStorage: TabCollectionStorage = mockk(relaxed = true)
private val accountManager: FxaAccountManager = mockk(relaxed = true) private val accountManager: FxaAccountManager = mockk(relaxed = true)
private val activity: HomeActivity = mockk(relaxed = true)
val coroutinesTestRule: MainCoroutineRule = MainCoroutineRule() val coroutinesTestRule: MainCoroutineRule = MainCoroutineRule()
val gleanTestRule = GleanTestRule(testContext) val gleanTestRule = GleanTestRule(testContext)
@ -220,34 +215,6 @@ class NavigationInteractorTest {
Assert.assertEquals("1", snapshot.single().extra?.getValue("tab_count")) Assert.assertEquals("1", snapshot.single().extra?.getValue("tab_count"))
} }
@Test
fun `onSyncedTabsClicked sets metrics and opens browser`() {
val tab = mockk<SyncTab>()
val entry = mockk<TabEntry>()
assertNull(Events.syncedTabOpened.testGetValue())
every { tab.active() }.answers { entry }
every { entry.url }.answers { "https://mozilla.org" }
var dismissTabTrayInvoked = false
createInteractor(
dismissTabTray = {
dismissTabTrayInvoked = true
},
).onSyncedTabClicked(tab)
assertTrue(dismissTabTrayInvoked)
assertNotNull(Events.syncedTabOpened.testGetValue())
verify {
activity.openToBrowserAndLoad(
searchTermOrURL = "https://mozilla.org",
newTab = true,
from = BrowserDirection.FromTabsTray,
)
}
}
@Suppress("LongParameterList") @Suppress("LongParameterList")
private fun createInteractor( private fun createInteractor(
browserStore: BrowserStore = store, browserStore: BrowserStore = store,
@ -259,7 +226,6 @@ class NavigationInteractorTest {
): NavigationInteractor { ): NavigationInteractor {
return DefaultNavigationInteractor( return DefaultNavigationInteractor(
context, context,
activity,
browserStore, browserStore,
navController, navController,
dismissTabTray, dismissTabTray,

Loading…
Cancel
Save