1
0
Fork 0

For #4124: Migrate BrowserToolbar to Libstate (#4279)

* For #4124: Migrate BrowserToolbar to Libstate

* Restores QuickActionSheetReducer

* Improve tests

* Make QuickActionSheetController

* Finalize tests

* Breaks out QuickActionSheetState

* Fix comments

* Adds BrowserStoreTest
master
Sawyer Blatz 2019-07-29 12:39:36 -07:00 committed by GitHub
parent 87d8f3b037
commit 6fa022c2f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 999 additions and 883 deletions

View File

@ -16,13 +16,12 @@ import android.widget.RadioButton
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.NavHostFragment.findNavController import androidx.navigation.fragment.NavHostFragment.findNavController
import com.google.android.material.bottomsheet.BottomSheetBehavior import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.component_search.* import kotlinx.android.synthetic.main.component_search.*
import kotlinx.android.synthetic.main.fragment_browser.* import kotlinx.android.synthetic.main.fragment_browser.*
@ -55,56 +54,46 @@ import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.support.base.feature.BackHandler import mozilla.components.support.base.feature.BackHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded
import org.mozilla.fenix.BrowsingModeManager import mozilla.components.support.ktx.kotlin.toUri
import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
import org.mozilla.fenix.collections.CreateCollectionViewModel import org.mozilla.fenix.collections.CreateCollectionViewModel
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.collections.getStepForCollectionsSize
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.FindInPageIntegration import org.mozilla.fenix.components.FindInPageIntegration
import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.toolbar.SearchAction import org.mozilla.fenix.components.toolbar.BrowserInteractor
import org.mozilla.fenix.components.toolbar.SearchState import org.mozilla.fenix.components.toolbar.BrowserState
import org.mozilla.fenix.components.toolbar.ToolbarComponent import org.mozilla.fenix.components.toolbar.BrowserStore
import org.mozilla.fenix.components.toolbar.BrowserToolbarView
import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController
import org.mozilla.fenix.components.toolbar.QuickActionSheetAction
import org.mozilla.fenix.components.toolbar.QuickActionSheetState
import org.mozilla.fenix.components.toolbar.ToolbarIntegration import org.mozilla.fenix.components.toolbar.ToolbarIntegration
import org.mozilla.fenix.components.toolbar.ToolbarMenu
import org.mozilla.fenix.components.toolbar.ToolbarUIView
import org.mozilla.fenix.components.toolbar.ToolbarViewModel
import org.mozilla.fenix.components.toolbar.trackToolbarItemInteraction
import org.mozilla.fenix.customtabs.CustomTabsIntegration import org.mozilla.fenix.customtabs.CustomTabsIntegration
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.enterToImmersiveMode import org.mozilla.fenix.ext.enterToImmersiveMode
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.toTab
import org.mozilla.fenix.home.sessioncontrol.SessionControlChange import org.mozilla.fenix.home.sessioncontrol.SessionControlChange
import org.mozilla.fenix.lib.Do
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.mvi.getManagedEmitter import org.mozilla.fenix.mvi.getManagedEmitter
import org.mozilla.fenix.quickactionsheet.QuickActionInteractor import org.mozilla.fenix.quickactionsheet.DefaultQuickActionSheetController
import org.mozilla.fenix.quickactionsheet.QuickActionSheetAction import org.mozilla.fenix.quickactionsheet.QuickActionSheetView
import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior
import org.mozilla.fenix.quickactionsheet.QuickActionSheetState
import org.mozilla.fenix.quickactionsheet.QuickActionSheetStore
import org.mozilla.fenix.quickactionsheet.QuickActionView
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
@SuppressWarnings("TooManyFunctions", "LargeClass") @SuppressWarnings("TooManyFunctions", "LargeClass")
class BrowserFragment : Fragment(), BackHandler { class BrowserFragment : Fragment(), BackHandler {
private lateinit var toolbarComponent: ToolbarComponent private lateinit var browserStore: BrowserStore
private lateinit var quickActionSheetStore: QuickActionSheetStore private lateinit var browserInteractor: BrowserInteractor
private lateinit var browserToolbarView: BrowserToolbarView
private lateinit var quickActionSheetView: QuickActionSheetView
private var tabCollectionObserver: Observer<List<TabCollection>>? = null private var tabCollectionObserver: Observer<List<TabCollection>>? = null
private var sessionObserver: Session.Observer? = null private var sessionObserver: Session.Observer? = null
@ -151,24 +140,26 @@ class BrowserFragment : Fragment(), BackHandler {
val view = inflater.inflate(R.layout.fragment_browser, container, false) val view = inflater.inflate(R.layout.fragment_browser, container, false)
view.browserLayout.transitionName = "$TAB_ITEM_TRANSITION_NAME${getSessionById()?.id}" view.browserLayout.transitionName = "$TAB_ITEM_TRANSITION_NAME${getSessionById()?.id}"
toolbarComponent = ToolbarComponent(
view.browserLayout,
ActionBusFactory.get(this),
customTabSessionId,
(activity as HomeActivity).browsingModeManager.isPrivate,
FenixViewModelProvider.create(
this,
ToolbarViewModel::class.java
) {
ToolbarViewModel(SearchState())
}
)
startPostponedEnterTransition() startPostponedEnterTransition()
val activity = activity as HomeActivity val activity = activity as HomeActivity
ThemeManager.applyStatusBarTheme(activity.window, activity.themeManager, activity) ThemeManager.applyStatusBarTheme(activity.window, activity.themeManager, activity)
val appLink = requireComponents.useCases.appLinksUseCases.appLinkRedirect
browserStore = StoreProvider.get(this) {
BrowserStore(
BrowserState(
quickActionSheetState = QuickActionSheetState(
readable = getSessionById()?.readerable ?: false,
bookmarked = findBookmarkedURL(getSessionById()),
readerActive = getSessionById()?.readerMode ?: false,
bounceNeeded = false,
isAppLink = getSessionById()?.let { appLink.invoke(it.url).hasExternalApp() } ?: false
)
)
)
}
return view return view
} }
@ -176,13 +167,62 @@ class BrowserFragment : Fragment(), BackHandler {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val sessionManager = requireComponents.core.sessionManager
val viewModel = activity!!.run {
ViewModelProviders.of(this).get(CreateCollectionViewModel::class.java)
}
browserInteractor = BrowserInteractor(
context = context!!,
store = browserStore,
browserToolbarController = DefaultBrowserToolbarController(
context!!,
findNavController(),
findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } },
nestedScrollQuickActionView = nestedScrollQuickAction,
engineView = engineView,
currentSession = getSessionById() ?: requireComponents.core.sessionManager.selectedSessionOrThrow,
viewModel = viewModel
),
quickActionSheetController = DefaultQuickActionSheetController(
context = context!!,
navController = findNavController(),
currentSession = getSessionById() ?: requireComponents.core.sessionManager.selectedSessionOrThrow,
appLinksUseCases = requireComponents.useCases.appLinksUseCases,
bookmarkTapped = {
lifecycleScope.launch { bookmarkTapped(it) }
}
),
readerModeController = DefaultReaderModeController(readerViewFeature),
currentSession = getSessionById() ?: requireComponents.core.sessionManager.selectedSessionOrThrow
)
browserToolbarView = BrowserToolbarView(
container = view.browserLayout,
interactor = browserInteractor,
currentSession = getSessionById() ?: requireComponents.core.sessionManager.selectedSessionOrThrow
)
toolbarIntegration.set( toolbarIntegration.set(
feature = (toolbarComponent.uiView as ToolbarUIView).toolbarIntegration, feature = browserToolbarView.toolbarIntegration,
owner = this, owner = this,
view = view view = view
) )
val sessionManager = requireComponents.core.sessionManager findInPageIntegration.set(
feature = FindInPageIntegration(
requireComponents.core.sessionManager, customTabSessionId, view.findInPageView, view.engineView, toolbar
),
owner = this,
view = view
)
quickActionSheetView = QuickActionSheetView(view.nestedScrollQuickAction, browserInteractor)
browserToolbarView.view.setOnSiteSecurityClickedListener {
showQuickSettingsDialog()
}
contextMenuFeature.set( contextMenuFeature.set(
feature = ContextMenuFeature( feature = ContextMenuFeature(
@ -252,14 +292,6 @@ class BrowserFragment : Fragment(), BackHandler {
view = view view = view
) )
findInPageIntegration.set(
feature = FindInPageIntegration(
requireComponents.core.sessionManager, customTabSessionId, view.findInPageView, view.engineView, toolbar
),
owner = this,
view = view
)
val accentHighContrastColor = ThemeManager.resolveAttribute(R.attr.accentHighContrast, requireContext()) val accentHighContrastColor = ThemeManager.resolveAttribute(R.attr.accentHighContrast, requireContext())
sitePermissionsFeature.set( sitePermissionsFeature.set(
@ -357,7 +389,7 @@ class BrowserFragment : Fragment(), BackHandler {
) { available -> ) { available ->
if (available) { requireComponents.analytics.metrics.track(Event.ReaderModeAvailable) } if (available) { requireComponents.analytics.metrics.track(Event.ReaderModeAvailable) }
quickActionSheetStore.apply { browserStore.apply {
dispatch(QuickActionSheetAction.ReadableStateChange(available)) dispatch(QuickActionSheetAction.ReadableStateChange(available))
dispatch(QuickActionSheetAction.ReaderActiveStateChange( dispatch(QuickActionSheetAction.ReaderActiveStateChange(
sessionManager.selectedSession?.readerMode ?: false sessionManager.selectedSession?.readerMode ?: false
@ -368,8 +400,6 @@ class BrowserFragment : Fragment(), BackHandler {
view = view view = view
) )
val actionEmitter = ActionBusFactory.get(this).getManagedEmitter(SearchAction::class.java)
customTabSessionId?.let { customTabSessionId?.let {
customTabsIntegration.set( customTabsIntegration.set(
feature = CustomTabsIntegration( feature = CustomTabsIntegration(
@ -380,46 +410,19 @@ class BrowserFragment : Fragment(), BackHandler {
activity, activity,
view.nestedScrollQuickAction, view.nestedScrollQuickAction,
view.swipeRefresh, view.swipeRefresh,
onItemTapped = { actionEmitter.onNext(SearchAction.ToolbarMenuItemTapped(it)) } onItemTapped = { browserInteractor.onBrowserToolbarMenuItemTapped(it) }
), ),
owner = this, owner = this,
view = view) view = view)
} }
toolbarComponent.setOnSiteSecurityClickedListener { browserToolbarView.view.setOnSiteSecurityClickedListener {
showQuickSettingsDialog() showQuickSettingsDialog()
} }
val appLink = requireComponents.useCases.appLinksUseCases.appLinkRedirect consumeFrom(browserStore) {
quickActionSheetStore = StoreProvider.get(this) {
QuickActionSheetStore(
QuickActionSheetState(
readable = getSessionById()?.readerable ?: false,
bookmarked = findBookmarkedURL(getSessionById()),
readerActive = getSessionById()?.readerMode ?: false,
bounceNeeded = false,
isAppLink = getSessionById()?.let { appLink.invoke(it.url).hasExternalApp() } ?: false
)
)
}
val quickActionSheetView = QuickActionView(
view.nestedScrollQuickAction,
QuickActionInteractor(
context!!,
DefaultReaderModeController(readerViewFeature),
quickActionSheetStore,
shareUrl = ::shareUrl,
bookmarkTapped = {
lifecycleScope.launch { bookmarkTapped(it) }
},
appLinksUseCases = requireComponents.useCases.appLinksUseCases
)
)
consumeFrom(quickActionSheetStore) {
quickActionSheetView.update(it) quickActionSheetView.update(it)
browserToolbarView.update(it)
} }
} }
@ -484,28 +487,6 @@ class BrowserFragment : Fragment(), BackHandler {
getSessionById()?.let { (activity as HomeActivity).updateThemeForSession(it) } getSessionById()?.let { (activity as HomeActivity).updateThemeForSession(it) }
(activity as AppCompatActivity).supportActionBar?.hide() (activity as AppCompatActivity).supportActionBar?.hide()
getAutoDisposeObservable<SearchAction>()
.subscribe {
when (it) {
is SearchAction.ToolbarClicked -> {
nav(
R.id.browserFragment,
BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
getSessionById()?.id
)
)
requireComponents.analytics.metrics.track(
Event.SearchBarTapped(Event.SearchBarTapped.Source.BROWSER)
)
}
is SearchAction.ToolbarMenuItemTapped -> {
val metrics = requireComponents.analytics.metrics
trackToolbarItemInteraction(metrics, it)
handleToolbarItemInteraction(it)
}
}
}
assignSitePermissionsRules() assignSitePermissionsRules()
} }
@ -530,14 +511,14 @@ class BrowserFragment : Fragment(), BackHandler {
) )
withContext(Main) { withContext(Main) {
quickActionSheetStore.dispatch( browserStore.dispatch(
QuickActionSheetAction.BookmarkedStateChange(bookmarked = true) QuickActionSheetAction.BookmarkedStateChange(bookmarked = true)
) )
requireComponents.analytics.metrics.track(Event.AddBookmark) requireComponents.analytics.metrics.track(Event.AddBookmark)
view?.let { view?.let {
FenixSnackbar.make(it.rootView, Snackbar.LENGTH_LONG) FenixSnackbar.make(it.rootView, Snackbar.LENGTH_LONG)
.setAnchorView(toolbarComponent.uiView.view) .setAnchorView(browserToolbarView.view)
.setAction(getString(R.string.edit_bookmark_snackbar_action)) { .setAction(getString(R.string.edit_bookmark_snackbar_action)) {
nav( nav(
R.id.browserFragment, R.id.browserFragment,
@ -624,110 +605,6 @@ class BrowserFragment : Fragment(), BackHandler {
promptsFeature.withFeature { it.onActivityResult(requestCode, resultCode, data) } promptsFeature.withFeature { it.onActivityResult(requestCode, resultCode, data) }
} }
// This method triggers the complexity warning. However it's actually not that hard to understand.
@SuppressWarnings("ComplexMethod")
private fun handleToolbarItemInteraction(action: SearchAction.ToolbarMenuItemTapped) {
val sessionUseCases = requireComponents.useCases.sessionUseCases
Do exhaustive when (action.item) {
ToolbarMenu.Item.Back -> sessionUseCases.goBack.invoke(getSessionById())
ToolbarMenu.Item.Forward -> sessionUseCases.goForward.invoke(getSessionById())
ToolbarMenu.Item.Reload -> sessionUseCases.reload.invoke(getSessionById())
ToolbarMenu.Item.Stop -> sessionUseCases.stopLoading.invoke(getSessionById())
ToolbarMenu.Item.Settings -> nav(
R.id.browserFragment,
BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
)
ToolbarMenu.Item.Library -> nav(
R.id.browserFragment,
BrowserFragmentDirections.actionBrowserFragmentToLibraryFragment()
)
is ToolbarMenu.Item.RequestDesktop -> getSessionById()?.let { session ->
sessionUseCases.requestDesktopSite.invoke(action.item.isChecked, session)
}
ToolbarMenu.Item.Share -> getSessionById()?.let { session ->
session.url.apply {
shareUrl(this)
}
}
ToolbarMenu.Item.NewPrivateTab -> {
val directions = BrowserFragmentDirections
.actionBrowserFragmentToSearchFragment(null)
nav(R.id.browserFragment, directions)
(activity as HomeActivity).browsingModeManager.mode = BrowsingModeManager.Mode.Private
}
ToolbarMenu.Item.FindInPage -> {
(BottomSheetBehavior.from(nestedScrollQuickAction as View) as QuickActionSheetBehavior).apply {
state = BottomSheetBehavior.STATE_COLLAPSED
}
findInPageIntegration.get()?.launch()
requireComponents.analytics.metrics.track(Event.FindInPageOpened)
}
ToolbarMenu.Item.ReportIssue -> getSessionById()?.let { session ->
session.url.apply {
val reportUrl = String.format(REPORT_SITE_ISSUE_URL, this)
requireComponents.useCases.tabsUseCases.addTab.invoke(reportUrl)
}
}
ToolbarMenu.Item.Help -> {
requireComponents.useCases.tabsUseCases.addTab.invoke(
SupportUtils.getSumoURLForTopic(
requireContext(),
SupportUtils.SumoTopic.HELP
)
)
}
ToolbarMenu.Item.NewTab -> {
val directions = BrowserFragmentDirections
.actionBrowserFragmentToSearchFragment(null)
nav(R.id.browserFragment, directions)
(activity as HomeActivity).browsingModeManager.mode =
BrowsingModeManager.Mode.Normal
}
ToolbarMenu.Item.SaveToCollection -> showSaveToCollection()
ToolbarMenu.Item.OpenInFenix -> {
// Release the session from this view so that it can immediately be rendered by a different view
engineView.release()
// Strip the CustomTabConfig to turn this Session into a regular tab and then select it
getSessionById()?.let {
it.customTabConfig = null
requireComponents.core.sessionManager.select(it)
}
// Switch to the actual browser which should now display our new selected session
startActivity(Intent(context, IntentReceiverActivity::class.java).also {
it.action = Intent.ACTION_VIEW
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
// Close this activity since it is no longer displaying any session
activity?.finish()
}
}
}
private fun showSaveToCollection() {
val context = context ?: return
getSessionById()?.let {
val tabs = it.toTab(context)
val viewModel = activity?.run {
ViewModelProviders.of(this).get(CreateCollectionViewModel::class.java)
}
viewModel?.tabs = listOf(tabs)
val selectedSet = mutableSetOf(tabs)
viewModel?.selectedTabs = selectedSet
viewModel?.tabCollections = requireComponents.core.tabCollectionStorage.cachedTabCollections.reversed()
viewModel?.saveCollectionStep =
viewModel?.tabCollections?.getStepForCollectionsSize() ?: SaveCollectionStep.SelectCollection
viewModel?.snackbarAnchorView = nestedScrollQuickAction
viewModel?.previousFragmentId = R.id.browserFragment
view?.let {
val directions = BrowserFragmentDirections.actionBrowserFragmentToCreateCollectionFragment()
nav(R.id.browserFragment, directions)
}
}
}
private fun assignSitePermissionsRules() { private fun assignSitePermissionsRules() {
val settings = Settings.getInstance(requireContext()) val settings = Settings.getInstance(requireContext())
@ -787,7 +664,7 @@ class BrowserFragment : Fragment(), BackHandler {
override fun onLoadingStateChanged(session: Session, loading: Boolean) { override fun onLoadingStateChanged(session: Session, loading: Boolean) {
if (!loading) { if (!loading) {
updateBookmarkState(session) updateBookmarkState(session)
quickActionSheetStore.dispatch(QuickActionSheetAction.BounceNeededChange) browserStore.dispatch(QuickActionSheetAction.BounceNeededChange)
} }
} }
@ -827,7 +704,7 @@ class BrowserFragment : Fragment(), BackHandler {
findBookmarkJob = lifecycleScope.launch(IO) { findBookmarkJob = lifecycleScope.launch(IO) {
val found = findBookmarkedURL(session) val found = findBookmarkedURL(session)
withContext(Main) { withContext(Main) {
quickActionSheetStore.dispatch(QuickActionSheetAction.BookmarkedStateChange(found)) browserStore.dispatch(QuickActionSheetAction.BookmarkedStateChange(found))
} }
} }
} }
@ -835,7 +712,7 @@ class BrowserFragment : Fragment(), BackHandler {
private fun updateAppLinksState(session: Session) { private fun updateAppLinksState(session: Session) {
val url = session.url val url = session.url
val appLinks = requireComponents.useCases.appLinksUseCases.appLinkRedirect val appLinks = requireComponents.useCases.appLinksUseCases.appLinkRedirect
quickActionSheetStore.dispatch(QuickActionSheetAction.AppLinkStateChange(appLinks.invoke(url).hasExternalApp())) browserStore.dispatch(QuickActionSheetAction.AppLinkStateChange(appLinks.invoke(url).hasExternalApp()))
} }
private val collectionStorageObserver = object : TabCollectionStorage.Observer { private val collectionStorageObserver = object : TabCollectionStorage.Observer {
@ -852,7 +729,7 @@ class BrowserFragment : Fragment(), BackHandler {
view?.let { view -> view?.let { view ->
FenixSnackbar.make(view, Snackbar.LENGTH_SHORT) FenixSnackbar.make(view, Snackbar.LENGTH_SHORT)
.setText(view.context.getString(R.string.create_collection_tab_saved)) .setText(view.context.getString(R.string.create_collection_tab_saved))
.setAnchorView(toolbarComponent.uiView.view) .setAnchorView(browserToolbarView.view)
.show() .show()
} }
} }

View File

@ -0,0 +1,71 @@
/* 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.components.toolbar
import android.content.Context
import mozilla.components.browser.session.Session
import org.mozilla.fenix.browser.readermode.ReaderModeController
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.quickactionsheet.QuickActionSheetController
import org.mozilla.fenix.quickactionsheet.QuickActionSheetViewInteractor
class BrowserInteractor(
private val context: Context,
private val store: BrowserStore,
private val browserToolbarController: BrowserToolbarController,
private val quickActionSheetController: QuickActionSheetController,
private val readerModeController: ReaderModeController,
private val currentSession: Session
) : BrowserToolbarViewInteractor, QuickActionSheetViewInteractor {
override fun onBrowserToolbarClicked() {
browserToolbarController.handleToolbarClick()
}
override fun onBrowserToolbarMenuItemTapped(item: ToolbarMenu.Item) {
browserToolbarController.handleToolbarItemInteraction(item)
}
override fun onQuickActionSheetOpened() {
context.metrics.track(Event.QuickActionSheetOpened)
}
override fun onQuickActionSheetClosed() {
context.metrics.track(Event.QuickActionSheetClosed)
}
override fun onQuickActionSheetSharePressed() {
quickActionSheetController.handleShare()
}
override fun onQuickActionSheetDownloadPressed() {
quickActionSheetController.handleDownload()
}
override fun onQuickActionSheetBookmarkPressed() {
quickActionSheetController.handleBookmark()
}
override fun onQuickActionSheetReadPressed() {
context.metrics.track(Event.QuickActionSheetReadTapped)
val enabled = currentSession.readerMode
if (enabled) {
readerModeController.hideReaderView()
} else {
readerModeController.showReaderView()
}
store.dispatch(QuickActionSheetAction.ReaderActiveStateChange(!enabled))
}
override fun onQuickActionSheetOpenLinkPressed() {
quickActionSheetController.handleOpenLink()
}
override fun onQuickActionSheetAppearancePressed() {
// TODO telemetry: https://github.com/mozilla-mobile/fenix/issues/2267
readerModeController.showControls()
}
}

View File

@ -0,0 +1,86 @@
/* 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.components.toolbar
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
class BrowserStore(initialState: BrowserState) :
Store<BrowserState, BrowserAction>(initialState, ::browserStateReducer)
/**
* The state for the Browser Screen
* @property quickActionSheetState: state of the quick action sheet
*/
data class BrowserState(
val quickActionSheetState: QuickActionSheetState
) : State
/**
* The state for the QuickActionSheet
* @property readable Whether or not the current session can display a reader view
* @property bookmarked Whether or not the current session is already bookmarked
* @property readerActive Whether or not the current session is in reader mode
* @property bounceNeeded Whether or not the quick action sheet should bounce
*/
data class QuickActionSheetState(
val readable: Boolean,
val bookmarked: Boolean,
val readerActive: Boolean,
val bounceNeeded: Boolean,
val isAppLink: Boolean
) : State
sealed class BrowserAction : Action
/**
* Actions to dispatch through the [QuickActionSheetStore] to modify [QuickActionSheetState] through the reducer.
*/
sealed class QuickActionSheetAction : BrowserAction() {
data class BookmarkedStateChange(val bookmarked: Boolean) : QuickActionSheetAction()
data class ReadableStateChange(val readable: Boolean) : QuickActionSheetAction()
data class ReaderActiveStateChange(val active: Boolean) : QuickActionSheetAction()
data class AppLinkStateChange(val isAppLink: Boolean) : QuickActionSheetAction()
object BounceNeededChange : QuickActionSheetAction()
}
/**
* Reducers for [BrowserStore].
*
* A top level reducer that receives the current [BrowserState] and an [Action] and then delegates to the proper child
*
*/
fun browserStateReducer(
state: BrowserState,
action: BrowserAction
): BrowserState {
return when (action) {
is QuickActionSheetAction -> {
QuickActionSheetStateReducer.reduce(state, action)
}
}
}
/**
* Reduces [QuickActionSheetAction]s to update [BrowserState].
*/
internal object QuickActionSheetStateReducer {
fun reduce(state: BrowserState, action: QuickActionSheetAction): BrowserState {
return when (action) {
is QuickActionSheetAction.BookmarkedStateChange ->
state.copy(quickActionSheetState = state.quickActionSheetState.copy(bookmarked = action.bookmarked))
is QuickActionSheetAction.ReadableStateChange ->
state.copy(quickActionSheetState = state.quickActionSheetState.copy(readable = action.readable))
is QuickActionSheetAction.ReaderActiveStateChange ->
state.copy(quickActionSheetState = state.quickActionSheetState.copy(readerActive = action.active))
is QuickActionSheetAction.BounceNeededChange ->
state.copy(quickActionSheetState = state.quickActionSheetState.copy(bounceNeeded = true))
is QuickActionSheetAction.AppLinkStateChange -> {
state.copy(quickActionSheetState = state.quickActionSheetState.copy(isAppLink = action.isAppLink))
}
}
}
}

View File

@ -0,0 +1,187 @@
/* 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.components.toolbar
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.core.widget.NestedScrollView
import androidx.navigation.NavController
import com.google.android.material.bottomsheet.BottomSheetBehavior
import mozilla.components.browser.session.Session
import mozilla.components.concept.engine.EngineView
import org.mozilla.fenix.BrowsingModeManager
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragment
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.collections.CreateCollectionViewModel
import org.mozilla.fenix.collections.getStepForCollectionsSize
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.toTab
import org.mozilla.fenix.lib.Do
import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior
import org.mozilla.fenix.settings.SupportUtils
/**
* An interface that handles the view manipulation of the BrowserToolbar, triggered by the Interactor
*/
interface BrowserToolbarController {
fun handleToolbarItemInteraction(item: ToolbarMenu.Item)
fun handleToolbarClick()
}
class DefaultBrowserToolbarController(
private val context: Context,
private val navController: NavController,
private val findInPageLauncher: () -> Unit,
private val nestedScrollQuickActionView: NestedScrollView,
private val engineView: EngineView,
private val currentSession: Session,
private val viewModel: CreateCollectionViewModel
) : BrowserToolbarController {
override fun handleToolbarClick() {
context.components.analytics.metrics.track(
Event.SearchBarTapped(Event.SearchBarTapped.Source.BROWSER)
)
navController.nav(
R.id.browserFragment,
BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
currentSession.id
)
)
}
@SuppressWarnings("ComplexMethod")
override fun handleToolbarItemInteraction(item: ToolbarMenu.Item) {
val sessionUseCases = context.components.useCases.sessionUseCases
trackToolbarItemInteraction(item)
Do exhaustive when (item) {
ToolbarMenu.Item.Back -> sessionUseCases.goBack.invoke(currentSession)
ToolbarMenu.Item.Forward -> sessionUseCases.goForward.invoke(currentSession)
ToolbarMenu.Item.Reload -> sessionUseCases.reload.invoke(currentSession)
ToolbarMenu.Item.Stop -> sessionUseCases.stopLoading.invoke(currentSession)
ToolbarMenu.Item.Settings -> navController.nav(
R.id.browserFragment,
BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
)
ToolbarMenu.Item.Library -> navController.nav(
R.id.browserFragment,
BrowserFragmentDirections.actionBrowserFragmentToLibraryFragment()
)
is ToolbarMenu.Item.RequestDesktop -> sessionUseCases.requestDesktopSite.invoke(
item.isChecked,
currentSession
)
ToolbarMenu.Item.Share -> {
currentSession.url.apply {
val directions = BrowserFragmentDirections.actionBrowserFragmentToShareFragment(this)
navController.nav(R.id.browserFragment, directions)
}
}
ToolbarMenu.Item.NewPrivateTab -> {
val directions = BrowserFragmentDirections
.actionBrowserFragmentToSearchFragment(null)
navController.nav(R.id.browserFragment, directions)
(context as HomeActivity).browsingModeManager.mode = BrowsingModeManager.Mode.Private
}
ToolbarMenu.Item.FindInPage -> {
(BottomSheetBehavior.from(nestedScrollQuickActionView) as QuickActionSheetBehavior).apply {
state = BottomSheetBehavior.STATE_COLLAPSED
}
findInPageLauncher()
context.components.analytics.metrics.track(Event.FindInPageOpened)
}
ToolbarMenu.Item.ReportIssue -> {
currentSession.url.apply {
val reportUrl = String.format(BrowserFragment.REPORT_SITE_ISSUE_URL, this)
context.components.useCases.tabsUseCases.addTab.invoke(reportUrl)
}
}
ToolbarMenu.Item.Help -> {
context.components.useCases.tabsUseCases.addTab.invoke(
SupportUtils.getSumoURLForTopic(
context,
SupportUtils.SumoTopic.HELP
)
)
}
ToolbarMenu.Item.NewTab -> {
val directions = BrowserFragmentDirections
.actionBrowserFragmentToSearchFragment(null)
navController.nav(R.id.browserFragment, directions)
(context as HomeActivity).browsingModeManager.mode =
BrowsingModeManager.Mode.Normal
}
ToolbarMenu.Item.SaveToCollection -> {
currentSession.let {
val tab = it.toTab(context)
viewModel.tabs = listOf(tab)
val selectedSet = mutableSetOf(tab)
viewModel.selectedTabs = selectedSet
viewModel.tabCollections =
context.components.core.tabCollectionStorage.cachedTabCollections.reversed()
viewModel.saveCollectionStep = viewModel.tabCollections.getStepForCollectionsSize()
viewModel.snackbarAnchorView = nestedScrollQuickActionView
viewModel.previousFragmentId = R.id.browserFragment
val directions = BrowserFragmentDirections.actionBrowserFragmentToCreateCollectionFragment()
navController.nav(R.id.browserFragment, directions)
}
}
ToolbarMenu.Item.OpenInFenix -> {
// Release the session from this view so that it can immediately be rendered by a different view
engineView.release()
// Strip the CustomTabConfig to turn this Session into a regular tab and then select it
currentSession.customTabConfig = null
context.components.core.sessionManager.select(currentSession)
// Switch to the actual browser which should now display our new selected session
context.startActivity(Intent(context, IntentReceiverActivity::class.java).also {
it.action = Intent.ACTION_VIEW
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
// Close this activity since it is no longer displaying any session
(context as Activity).finish()
}
}
}
@SuppressWarnings("ComplexMethod")
private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) {
val eventItem = when (item) {
ToolbarMenu.Item.Back -> Event.BrowserMenuItemTapped.Item.BACK
ToolbarMenu.Item.Forward -> Event.BrowserMenuItemTapped.Item.FORWARD
ToolbarMenu.Item.Reload -> Event.BrowserMenuItemTapped.Item.RELOAD
ToolbarMenu.Item.Stop -> Event.BrowserMenuItemTapped.Item.STOP
ToolbarMenu.Item.Settings -> Event.BrowserMenuItemTapped.Item.SETTINGS
ToolbarMenu.Item.Library -> Event.BrowserMenuItemTapped.Item.LIBRARY
is ToolbarMenu.Item.RequestDesktop ->
if (item.isChecked) {
Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_ON
} else {
Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_OFF
}
ToolbarMenu.Item.NewPrivateTab -> Event.BrowserMenuItemTapped.Item.NEW_PRIVATE_TAB
ToolbarMenu.Item.FindInPage -> Event.BrowserMenuItemTapped.Item.FIND_IN_PAGE
ToolbarMenu.Item.ReportIssue -> Event.BrowserMenuItemTapped.Item.REPORT_SITE_ISSUE
ToolbarMenu.Item.Help -> Event.BrowserMenuItemTapped.Item.HELP
ToolbarMenu.Item.NewTab -> Event.BrowserMenuItemTapped.Item.NEW_TAB
ToolbarMenu.Item.OpenInFenix -> Event.BrowserMenuItemTapped.Item.OPEN_IN_FENIX
ToolbarMenu.Item.Share -> Event.BrowserMenuItemTapped.Item.SHARE
ToolbarMenu.Item.SaveToCollection -> Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION
}
context.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem))
}
}

View File

@ -0,0 +1,124 @@
package org.mozilla.fenix.components.toolbar
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import kotlinx.android.extensions.LayoutContainer
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.session.Session
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.support.ktx.android.util.dpToFloat
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.customtabs.CustomTabToolbarMenu
import org.mozilla.fenix.ext.components
interface BrowserToolbarViewInteractor {
fun onBrowserToolbarClicked()
fun onBrowserToolbarMenuItemTapped(item: ToolbarMenu.Item)
}
class BrowserToolbarView(
private val container: ViewGroup,
private val interactor: BrowserToolbarViewInteractor,
private val currentSession: Session
) : LayoutContainer {
override val containerView: View?
get() = container
private val urlBackground = LayoutInflater.from(container.context)
.inflate(R.layout.layout_url_background, container, false)
val view: BrowserToolbar = LayoutInflater.from(container.context)
.inflate(R.layout.component_search, container, true)
.findViewById(R.id.toolbar)
val toolbarIntegration: ToolbarIntegration
init {
// We need access to the customSessionId. We don't directly have access since we aren't passing session id in
// So we need to access it through the store...?
with(container.context) {
val sessionManager = components.core.sessionManager
val isCustomTabSession = currentSession.isCustomTabSession()
view.apply {
elevation = TOOLBAR_ELEVATION.dpToFloat(resources.displayMetrics)
onUrlClicked = {
interactor.onBrowserToolbarClicked()
false
}
browserActionMargin = browserActionMarginDp.dpToPx(resources.displayMetrics)
urlBoxView = if (isCustomTabSession) null else urlBackground
progressBarGravity = if (isCustomTabSession) PROGRESS_BOTTOM else PROGRESS_TOP
textColor = ContextCompat.getColor(context, R.color.photonGrey30)
hint = context.getString(R.string.search_hint)
suggestionBackgroundColor = ContextCompat.getColor(
container.context,
R.color.suggestion_highlight_color
)
textColor = ContextCompat.getColor(
container.context,
ThemeManager.resolveAttribute(R.attr.primaryText, container.context)
)
hintColor = ContextCompat.getColor(
container.context,
ThemeManager.resolveAttribute(R.attr.secondaryText, container.context)
)
}
val menuToolbar = if (isCustomTabSession) {
CustomTabToolbarMenu(
this,
sessionManager,
currentSession.id,
onItemTapped = {
interactor.onBrowserToolbarMenuItemTapped(it)
}
)
} else {
DefaultToolbarMenu(
this,
hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(),
requestDesktopStateProvider = { currentSession.desktopMode },
onItemTapped = { interactor.onBrowserToolbarMenuItemTapped(it) }
)
}
toolbarIntegration = ToolbarIntegration(
this,
view,
container,
menuToolbar,
ShippedDomainsProvider().also { it.initialize(this) },
components.core.historyStorage,
components.core.sessionManager,
currentSession.id,
currentSession.private
)
}
}
fun update(state: BrowserState) {
// TODO Leaving this as a stub for now since we don't actually have anything to update ever...?
}
companion object {
private const val TOOLBAR_ELEVATION = 16
private const val PROGRESS_BOTTOM = 0
private const val PROGRESS_TOP = 1
const val browserActionMarginDp = 8
}
}

View File

@ -1,63 +0,0 @@
/* 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.components.toolbar
import android.view.ViewGroup
import kotlinx.android.synthetic.main.component_search.*
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModelBase
import org.mozilla.fenix.mvi.UIComponentViewModelProvider
import org.mozilla.fenix.mvi.ViewState
class ToolbarComponent(
private val container: ViewGroup,
bus: ActionBusFactory,
private val sessionId: String?,
private val isPrivate: Boolean,
viewModelProvider: UIComponentViewModelProvider<SearchState, SearchChange>
) :
UIComponent<SearchState, SearchAction, SearchChange>(
bus.getManagedEmitter(SearchAction::class.java),
bus.getSafeManagedObservable(SearchChange::class.java),
viewModelProvider
) {
override fun initView() = ToolbarUIView(
sessionId,
isPrivate,
container,
actionEmitter,
changesObservable
)
init {
bind()
}
fun setOnSiteSecurityClickedListener(listener: () -> Unit) {
uiView.toolbar.setOnSiteSecurityClickedListener(listener)
}
}
class SearchState : ViewState
sealed class SearchAction : Action {
object ToolbarClicked : SearchAction()
data class ToolbarMenuItemTapped(val item: ToolbarMenu.Item) : SearchAction()
}
sealed class SearchChange : Change
class ToolbarViewModel(initialState: SearchState) :
UIComponentViewModelBase<SearchState, SearchChange>(initialState, reducer) {
companion object {
val reducer: Reducer<SearchState, SearchChange> = { state, _ -> state }
}
}

View File

@ -1,36 +0,0 @@
/* 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.components.toolbar
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
// This method triggers the complexity warning. However it's actually not that hard to understand.
@SuppressWarnings("ComplexMethod")
fun trackToolbarItemInteraction(metrics: MetricController, action: SearchAction.ToolbarMenuItemTapped) {
val item = when (action.item) {
ToolbarMenu.Item.Back -> Event.BrowserMenuItemTapped.Item.BACK
ToolbarMenu.Item.Forward -> Event.BrowserMenuItemTapped.Item.FORWARD
ToolbarMenu.Item.Reload -> Event.BrowserMenuItemTapped.Item.RELOAD
ToolbarMenu.Item.Stop -> Event.BrowserMenuItemTapped.Item.STOP
ToolbarMenu.Item.Settings -> Event.BrowserMenuItemTapped.Item.SETTINGS
ToolbarMenu.Item.Library -> Event.BrowserMenuItemTapped.Item.LIBRARY
is ToolbarMenu.Item.RequestDesktop -> if (action.item.isChecked) {
Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_ON
} else {
Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_OFF
}
ToolbarMenu.Item.NewPrivateTab -> Event.BrowserMenuItemTapped.Item.NEW_PRIVATE_TAB
ToolbarMenu.Item.FindInPage -> Event.BrowserMenuItemTapped.Item.FIND_IN_PAGE
ToolbarMenu.Item.ReportIssue -> Event.BrowserMenuItemTapped.Item.REPORT_SITE_ISSUE
ToolbarMenu.Item.Help -> Event.BrowserMenuItemTapped.Item.HELP
ToolbarMenu.Item.NewTab -> Event.BrowserMenuItemTapped.Item.NEW_TAB
ToolbarMenu.Item.OpenInFenix -> Event.BrowserMenuItemTapped.Item.OPEN_IN_FENIX
ToolbarMenu.Item.Share -> Event.BrowserMenuItemTapped.Item.SHARE
ToolbarMenu.Item.SaveToCollection -> Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION
}
metrics.track(Event.BrowserMenuItemTapped(item))
}

View File

@ -1,107 +0,0 @@
/* 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.components.toolbar
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.support.ktx.android.util.dpToFloat
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.R
import org.mozilla.fenix.customtabs.CustomTabToolbarMenu
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getColorFromAttr
import org.mozilla.fenix.mvi.UIView
class ToolbarUIView(
sessionId: String?,
isPrivate: Boolean,
container: ViewGroup,
actionEmitter: Observer<SearchAction>,
changesObservable: Observable<SearchChange>
) :
UIView<SearchState, SearchAction, SearchChange>(container, actionEmitter, changesObservable) {
val toolbarIntegration: ToolbarIntegration
override val view: BrowserToolbar = LayoutInflater.from(container.context)
.inflate(R.layout.component_search, container, true)
.findViewById(R.id.toolbar)
private val urlBackground = LayoutInflater.from(container.context)
.inflate(R.layout.layout_url_background, container, false)
init {
val sessionManager = view.context.components.core.sessionManager
val session = sessionId?.let { sessionManager.findSessionById(it) }
?: sessionManager.selectedSession
val isCustomTabSession = session?.isCustomTabSession() == true
view.apply {
elevation = TOOLBAR_ELEVATION.dpToFloat(resources.displayMetrics)
onUrlClicked = {
actionEmitter.onNext(SearchAction.ToolbarClicked)
false
}
browserActionMargin = browserActionMarginDp.dpToPx(resources.displayMetrics)
urlBoxView = if (isCustomTabSession) null else urlBackground
progressBarGravity = if (isCustomTabSession) PROGRESS_BOTTOM else PROGRESS_TOP
textColor = ContextCompat.getColor(context, R.color.photonGrey30)
hint = context.getString(R.string.search_hint)
suggestionBackgroundColor = ContextCompat.getColor(context, R.color.suggestion_highlight_color)
textColor = context.getColorFromAttr(R.attr.primaryText)
hintColor = context.getColorFromAttr(R.attr.secondaryText)
}
with(view.context) {
val menuToolbar = if (isCustomTabSession) {
CustomTabToolbarMenu(
this,
sessionManager,
sessionId,
onItemTapped = { actionEmitter.onNext(SearchAction.ToolbarMenuItemTapped(it)) }
)
} else {
DefaultToolbarMenu(this,
hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(),
requestDesktopStateProvider = { session?.desktopMode ?: false },
onItemTapped = { actionEmitter.onNext(SearchAction.ToolbarMenuItemTapped(it)) }
)
}
toolbarIntegration = ToolbarIntegration(
this,
view,
container,
menuToolbar,
ShippedDomainsProvider().also { it.initialize(this) },
components.core.historyStorage,
components.core.sessionManager,
sessionId,
isPrivate
)
}
}
override fun updateView() = Consumer<SearchState> {}
companion object {
private const val TOOLBAR_ELEVATION = 16
private const val PROGRESS_BOTTOM = 0
private const val PROGRESS_TOP = 1
const val browserActionMarginDp = 8
}
}

View File

@ -1,89 +0,0 @@
/* 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.quickactionsheet
import android.content.Context
import android.content.Intent
import androidx.annotation.CallSuper
import mozilla.components.browser.session.Session
import mozilla.components.feature.app.links.AppLinksUseCases
import org.mozilla.fenix.browser.readermode.ReaderModeController
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.utils.ItsNotBrokenSnack
/**
* Interactor for the QuickActionSheet
*/
class QuickActionInteractor(
private val context: Context,
private val readerModeController: ReaderModeController,
private val quickActionStore: QuickActionSheetStore,
private val shareUrl: (String) -> Unit,
private val bookmarkTapped: (Session) -> Unit,
private val appLinksUseCases: AppLinksUseCases
) : QuickActionSheetInteractor {
private val selectedSession
inline get() = context.components.core.sessionManager.selectedSession
@CallSuper
override fun onOpened() {
context.metrics.track(Event.QuickActionSheetOpened)
}
@CallSuper
override fun onClosed() {
context.metrics.track(Event.QuickActionSheetClosed)
}
@CallSuper
override fun onSharedPressed() {
context.metrics.track(Event.QuickActionSheetShareTapped)
selectedSession?.url?.let(shareUrl)
}
@CallSuper
override fun onDownloadsPressed() {
context.metrics.track(Event.QuickActionSheetDownloadTapped)
ItsNotBrokenSnack(context).showSnackbar(issueNumber = "348")
}
@CallSuper
override fun onBookmarkPressed() {
context.metrics.track(Event.QuickActionSheetBookmarkTapped)
selectedSession?.let(bookmarkTapped)
}
@CallSuper
override fun onReadPressed() {
context.metrics.track(Event.QuickActionSheetReadTapped)
val enabled = selectedSession?.readerMode ?: false
if (enabled) {
readerModeController.hideReaderView()
} else {
readerModeController.showReaderView()
}
quickActionStore.dispatch(QuickActionSheetAction.ReaderActiveStateChange(!enabled))
}
@CallSuper
override fun onOpenAppLinkPressed() {
val getRedirect = appLinksUseCases.appLinkRedirect
val redirect = selectedSession?.let {
getRedirect.invoke(it.url)
} ?: return
redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
appLinksUseCases.openAppLink.invoke(redirect)
}
@CallSuper
override fun onAppearancePressed() {
// TODO telemetry: https://github.com/mozilla-mobile/fenix/issues/2267
readerModeController.showControls()
}
}

View File

@ -0,0 +1,64 @@
/* 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.quickactionsheet
import android.content.Context
import android.content.Intent
import androidx.navigation.NavController
import mozilla.components.browser.session.Session
import mozilla.components.feature.app.links.AppLinksUseCases
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.utils.ItsNotBrokenSnack
/**
* An interface that handles the view manipulation of the QuickActionSheet, triggered by the Interactor
*/
interface QuickActionSheetController {
fun handleShare()
fun handleDownload()
fun handleBookmark()
fun handleOpenLink()
}
class DefaultQuickActionSheetController(
private val context: Context,
private val navController: NavController,
private val currentSession: Session,
private val appLinksUseCases: AppLinksUseCases,
private val bookmarkTapped: (Session) -> Unit
) : QuickActionSheetController {
override fun handleShare() {
context.metrics.track(Event.QuickActionSheetShareTapped)
currentSession.url.let {
val directions = BrowserFragmentDirections.actionBrowserFragmentToShareFragment(it)
navController.nav(R.id.browserFragment, directions)
}
}
override fun handleDownload() {
context.metrics.track(Event.QuickActionSheetDownloadTapped)
ItsNotBrokenSnack(context).showSnackbar(issueNumber = "348")
}
override fun handleBookmark() {
context.metrics.track(Event.QuickActionSheetBookmarkTapped)
bookmarkTapped(currentSession)
}
override fun handleOpenLink() {
val getRedirect = appLinksUseCases.appLinkRedirect
val redirect = currentSession.let {
getRedirect.invoke(it.url)
}
redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
appLinksUseCases.openAppLink.invoke(redirect)
}
}

View File

@ -1,63 +0,0 @@
/* 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.quickactionsheet
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
/**
* The [Store] for holding the [QuickActionSheetState] and applying [QuickActionSheetAction]s.
*/
class QuickActionSheetStore(initialState: QuickActionSheetState) :
Store<QuickActionSheetState, QuickActionSheetAction>(initialState, ::quickActionSheetStateReducer)
/**
* The state for the QuickActionSheet found in the Browser Fragment
* @property readable Whether or not the current session can display a reader view
* @property bookmarked Whether or not the current session is already bookmarked
* @property readerActive Whether or not the current session is in reader mode
* @property bounceNeeded Whether or not the quick action sheet should bounce
*/
data class QuickActionSheetState(
val readable: Boolean,
val bookmarked: Boolean,
val readerActive: Boolean,
val bounceNeeded: Boolean,
val isAppLink: Boolean
) : State
/**
* Actions to dispatch through the [QuickActionSheetStore] to modify [QuickActionSheetState] through the reducer.
*/
sealed class QuickActionSheetAction : Action {
data class BookmarkedStateChange(val bookmarked: Boolean) : QuickActionSheetAction()
data class ReadableStateChange(val readable: Boolean) : QuickActionSheetAction()
data class ReaderActiveStateChange(val active: Boolean) : QuickActionSheetAction()
data class AppLinkStateChange(val isAppLink: Boolean) : QuickActionSheetAction()
object BounceNeededChange : QuickActionSheetAction()
}
/**
* Reduces [QuickActionSheetAction]s to update [QuickActionSheetState].
*/
fun quickActionSheetStateReducer(
state: QuickActionSheetState,
action: QuickActionSheetAction
): QuickActionSheetState {
return when (action) {
is QuickActionSheetAction.BookmarkedStateChange ->
state.copy(bookmarked = action.bookmarked)
is QuickActionSheetAction.ReadableStateChange ->
state.copy(readable = action.readable)
is QuickActionSheetAction.ReaderActiveStateChange ->
state.copy(readerActive = action.active)
is QuickActionSheetAction.BounceNeededChange ->
state.copy(bounceNeeded = true)
is QuickActionSheetAction.AppLinkStateChange -> {
state.copy(isAppLink = action.isAppLink)
}
}
}

View File

@ -18,24 +18,25 @@ import kotlinx.android.synthetic.main.layout_quick_action_sheet.*
import kotlinx.android.synthetic.main.layout_quick_action_sheet.view.* import kotlinx.android.synthetic.main.layout_quick_action_sheet.view.*
import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.toolbar.BrowserState
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
interface QuickActionSheetInteractor { interface QuickActionSheetViewInteractor {
fun onOpened() fun onQuickActionSheetOpened()
fun onClosed() fun onQuickActionSheetClosed()
fun onSharedPressed() fun onQuickActionSheetSharePressed()
fun onDownloadsPressed() fun onQuickActionSheetDownloadPressed()
fun onBookmarkPressed() fun onQuickActionSheetBookmarkPressed()
fun onReadPressed() fun onQuickActionSheetReadPressed()
fun onAppearancePressed() fun onQuickActionSheetAppearancePressed()
fun onOpenAppLinkPressed() fun onQuickActionSheetOpenLinkPressed()
} }
/** /**
* View for the quick action sheet that slides out from the toolbar. * View for the quick action sheet that slides out from the toolbar.
*/ */
class QuickActionView( class QuickActionSheetView(
override val containerView: ViewGroup, override val containerView: ViewGroup,
private val interactor: QuickActionSheetInteractor private val interactor: QuickActionSheetViewInteractor
) : LayoutContainer, View.OnClickListener { ) : LayoutContainer, View.OnClickListener {
val view: NestedScrollView = LayoutInflater.from(containerView.context) val view: NestedScrollView = LayoutInflater.from(containerView.context)
@ -51,9 +52,9 @@ class QuickActionView(
updateImportantForAccessibility(state) updateImportantForAccessibility(state)
if (state == BottomSheetBehavior.STATE_EXPANDED) { if (state == BottomSheetBehavior.STATE_EXPANDED) {
interactor.onOpened() interactor.onQuickActionSheetOpened()
} else if (state == BottomSheetBehavior.STATE_COLLAPSED) { } else if (state == BottomSheetBehavior.STATE_COLLAPSED) {
interactor.onClosed() interactor.onQuickActionSheetClosed()
} }
} }
@ -77,12 +78,12 @@ class QuickActionView(
*/ */
override fun onClick(button: View) { override fun onClick(button: View) {
when (button.id) { when (button.id) {
R.id.quick_action_share -> interactor.onSharedPressed() R.id.quick_action_share -> interactor.onQuickActionSheetSharePressed()
R.id.quick_action_downloads -> interactor.onDownloadsPressed() R.id.quick_action_downloads -> interactor.onQuickActionSheetDownloadPressed()
R.id.quick_action_bookmark -> interactor.onBookmarkPressed() R.id.quick_action_bookmark -> interactor.onQuickActionSheetBookmarkPressed()
R.id.quick_action_read -> interactor.onReadPressed() R.id.quick_action_read -> interactor.onQuickActionSheetReadPressed()
R.id.quick_action_appearance -> interactor.onAppearancePressed() R.id.quick_action_appearance -> interactor.onQuickActionSheetAppearancePressed()
R.id.quick_action_open_app_link -> interactor.onOpenAppLinkPressed() R.id.quick_action_open_app_link -> interactor.onQuickActionSheetOpenLinkPressed()
else -> return else -> return
} }
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
@ -108,27 +109,32 @@ class QuickActionView(
} }
} }
fun update(state: QuickActionSheetState) { fun update(state: BrowserState) {
view.quick_action_read.isVisible = state.readable val quickActionSheetState = state.quickActionSheetState
view.quick_action_read.isSelected = state.readerActive view.quick_action_read.isVisible = quickActionSheetState.readable
view.quick_action_read.isSelected = quickActionSheetState.readerActive
view.quick_action_read.text = view.context.getString( view.quick_action_read.text = view.context.getString(
if (state.readerActive) R.string.quick_action_read_close else R.string.quick_action_read if (quickActionSheetState.readerActive) R.string.quick_action_read_close else R.string.quick_action_read
) )
notifyReaderModeButton(state.readable) notifyReaderModeButton(quickActionSheetState.readable)
view.quick_action_appearance.isVisible = state.readerActive view.quick_action_appearance.isVisible = quickActionSheetState.readerActive
view.quick_action_bookmark.isSelected = state.bookmarked view.quick_action_bookmark.isSelected = quickActionSheetState.bookmarked
view.quick_action_bookmark.text = view.context.getString( view.quick_action_bookmark.text = view.context.getString(
if (state.bookmarked) R.string.quick_action_bookmark_edit else R.string.quick_action_bookmark if (quickActionSheetState.bookmarked) {
R.string.quick_action_bookmark_edit
} else {
R.string.quick_action_bookmark
}
) )
if (state.bounceNeeded && Settings.getInstance(view.context).shouldAutoBounceQuickActionSheet) { if (quickActionSheetState.bounceNeeded && Settings.getInstance(view.context).shouldAutoBounceQuickActionSheet) {
quickActionSheet.bounceSheet() quickActionSheet.bounceSheet()
} }
view.quick_action_open_app_link.apply { view.quick_action_open_app_link.apply {
visibility = if (state.isAppLink) View.VISIBLE else View.GONE visibility = if (quickActionSheetState.isAppLink) View.VISIBLE else View.GONE
} }
} }

View File

@ -21,24 +21,24 @@ import org.mozilla.fenix.search.SearchState
/** /**
* Interface for the Toolbar Interactor. This interface is implemented by objects that want * Interface for the Toolbar Interactor. This interface is implemented by objects that want
* to respond to user interaction on the ToolbarView * to respond to user interaction on the [BrowserToolbarView]
*/ */
interface ToolbarInteractor { interface ToolbarInteractor {
/** /**
* Called when a user hits the return key while ToolbarView has focus. * Called when a user hits the return key while [BrowserToolbarView] has focus.
* @param url the text inside the ToolbarView when committed * @param url the text inside the [BrowserToolbarView] when committed
*/ */
fun onUrlCommitted(url: String) fun onUrlCommitted(url: String)
/** /**
* Called when a removes focus from the ToolbarView * Called when a user removes focus from the [BrowserToolbarView]
*/ */
fun onEditingCanceled() fun onEditingCanceled()
/** /**
* Called whenever the text inside the ToolbarView changes * Called whenever the text inside the [BrowserToolbarView] changes
* @param text the current text displayed by ToolbarView * @param text the current text displayed by [BrowserToolbarView]
*/ */
fun onTextChanged(text: String) fun onTextChanged(text: String)
} }

View File

@ -0,0 +1,258 @@
package org.mozilla.fenix.components.toolbar
import android.content.Context
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.browser.session.Session
import org.junit.Test
import org.mozilla.fenix.browser.readermode.ReaderModeController
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.quickactionsheet.QuickActionSheetController
class BrowserInteractorTest {
@Test
fun onBrowserToolbarClicked() {
val context: Context = mockk()
val browserToolbarController: BrowserToolbarController = mockk(relaxed = true)
val interactor = BrowserInteractor(
context,
mockk(),
browserToolbarController,
mockk(),
mockk(),
mockk()
)
interactor.onBrowserToolbarClicked()
verify { browserToolbarController.handleToolbarClick() }
}
@Test
fun onBrowserToolbarMenuItemTapped() {
val context: Context = mockk()
val browserToolbarController: BrowserToolbarController = mockk(relaxed = true)
val item: ToolbarMenu.Item = mockk()
val interactor = BrowserInteractor(
context,
mockk(),
browserToolbarController,
mockk(),
mockk(),
mockk()
)
interactor.onBrowserToolbarMenuItemTapped(item)
verify { browserToolbarController.handleToolbarItemInteraction(item) }
}
@Test
fun onQuickActionSheetOpened() {
val context: Context = mockk()
val metrics: MetricController = mockk()
val interactor = BrowserInteractor(
context,
mockk(),
mockk(),
mockk(),
mockk(),
mockk()
)
every { context.metrics } returns metrics
every { metrics.track(Event.QuickActionSheetOpened) } just Runs
interactor.onQuickActionSheetOpened()
verify { metrics.track(Event.QuickActionSheetOpened) }
}
@Test
fun onQuickActionSheetClosed() {
val context: Context = mockk()
val metrics: MetricController = mockk()
val interactor = BrowserInteractor(
context,
mockk(),
mockk(),
mockk(),
mockk(),
mockk()
)
every { context.metrics } returns metrics
every { metrics.track(Event.QuickActionSheetClosed) } just Runs
interactor.onQuickActionSheetClosed()
verify { metrics.track(Event.QuickActionSheetClosed) }
}
@Test
fun onQuickActionSheetSharePressed() {
val context: Context = mockk()
val session: Session = mockk()
val quickActionSheetController: QuickActionSheetController = mockk(relaxed = true)
val interactor = BrowserInteractor(
context,
mockk(),
mockk(),
quickActionSheetController,
mockk(),
session
)
interactor.onQuickActionSheetSharePressed()
verify { quickActionSheetController.handleShare() }
}
@Test
fun onQuickActionSheetDownloadPressed() {
val context: Context = mockk()
val metrics: MetricController = mockk()
val quickActionSheetController: QuickActionSheetController = mockk(relaxed = true)
val interactor = BrowserInteractor(
context,
mockk(),
mockk(),
quickActionSheetController,
mockk(),
mockk()
)
every { context.metrics } returns metrics
every { metrics.track(Event.QuickActionSheetDownloadTapped) } just Runs
interactor.onQuickActionSheetDownloadPressed()
verify { quickActionSheetController.handleDownload() }
}
@Test
fun onQuickActionSheetBookmarkPressed() {
val context: Context = mockk()
val session: Session = mockk()
val quickActionSheetController: QuickActionSheetController = mockk(relaxed = true)
val interactor = BrowserInteractor(
context,
mockk(),
mockk(),
quickActionSheetController,
mockk(),
session
)
interactor.onQuickActionSheetBookmarkPressed()
verify { quickActionSheetController.handleBookmark() }
}
@Test
fun onQuickActionSheetReadPressed() {
val context: Context = mockk()
val metrics: MetricController = mockk()
val session: Session = mockk()
val readerModeController: ReaderModeController = mockk(relaxed = true)
val browserStore: BrowserStore = mockk(relaxed = true)
val interactor = BrowserInteractor(
context,
browserStore,
mockk(),
mockk(),
readerModeController,
session
)
every { context.metrics } returns metrics
every { context.components.core.sessionManager.selectedSession } returns session
every { session.readerMode } returns false
every { metrics.track(Event.QuickActionSheetReadTapped) } just Runs
interactor.onQuickActionSheetReadPressed()
verify { metrics.track(Event.QuickActionSheetReadTapped) }
verify { readerModeController.showReaderView() }
}
@Test
fun onQuickActionSheetReadPressedWithActiveReaderMode() {
val context: Context = mockk()
val metrics: MetricController = mockk()
val session: Session = mockk()
val readerModeController: ReaderModeController = mockk(relaxed = true)
val browserStore: BrowserStore = mockk(relaxed = true)
val interactor = BrowserInteractor(
context,
browserStore,
mockk(),
mockk(),
readerModeController,
session
)
every { context.metrics } returns metrics
every { context.components.core.sessionManager.selectedSession } returns session
every { session.readerMode } returns true
every { metrics.track(Event.QuickActionSheetReadTapped) } just Runs
interactor.onQuickActionSheetReadPressed()
verify { metrics.track(Event.QuickActionSheetReadTapped) }
verify { readerModeController.hideReaderView() }
}
@Test
fun onQuickActionSheetOpenLinkPressed() {
val context: Context = mockk()
val session: Session = mockk()
val quickActionSheetController: QuickActionSheetController = mockk(relaxed = true)
val interactor = BrowserInteractor(
context,
mockk(),
mockk(),
quickActionSheetController,
mockk(),
session
)
interactor.onQuickActionSheetOpenLinkPressed()
verify { quickActionSheetController.handleOpenLink() }
}
@Test
fun onQuickActionSheetAppearancePressed() {
val context: Context = mockk()
val readerModeController: ReaderModeController = mockk(relaxed = true)
val interactor = BrowserInteractor(
context,
mockk(),
mockk(),
mockk(),
readerModeController,
mockk()
)
interactor.onQuickActionSheetAppearancePressed()
verify { readerModeController.showControls() }
}
}

View File

@ -0,0 +1,75 @@
/* 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.components.toolbar
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Test
class BrowserStoreTest {
@Test
fun bookmarkStateChange() = runBlocking {
val initialState = defaultBrowserState()
val store = BrowserStore(initialState)
store.dispatch(QuickActionSheetAction.BookmarkedStateChange(true)).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.quickActionSheetState.bookmarked, true)
}
@Test
fun readableStateChange() = runBlocking {
val initialState = defaultBrowserState()
val store = BrowserStore(initialState)
store.dispatch(QuickActionSheetAction.ReadableStateChange(true)).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.quickActionSheetState.readable, true)
}
@Test
fun readerActiveStateChange() = runBlocking {
val initialState = defaultBrowserState()
val store = BrowserStore(initialState)
store.dispatch(QuickActionSheetAction.ReaderActiveStateChange(true)).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.quickActionSheetState.readerActive, true)
}
@Test
fun bounceNeededChange() = runBlocking {
val initialState = defaultBrowserState()
val store = BrowserStore(initialState)
store.dispatch(QuickActionSheetAction.BounceNeededChange).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.quickActionSheetState.bounceNeeded, true)
}
@Test
fun appLinkStateChange() = runBlocking {
val initialState = defaultBrowserState()
val store = BrowserStore(initialState)
store.dispatch(QuickActionSheetAction.AppLinkStateChange(true)).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.quickActionSheetState.isAppLink, true)
}
private fun defaultBrowserState(): BrowserState = BrowserState(
quickActionSheetState = defaultQuickActionSheetState()
)
private fun defaultQuickActionSheetState(): QuickActionSheetState = QuickActionSheetState(
readable = false,
bookmarked = false,
readerActive = false,
bounceNeeded = false,
isAppLink = false
)
}

View File

@ -1,274 +0,0 @@
/* 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.quickactionsheet
import android.content.Context
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
import junit.framework.Assert.assertEquals
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.feature.app.links.AppLinkRedirect
import mozilla.components.feature.app.links.AppLinksUseCases
import org.junit.Test
import org.mozilla.fenix.browser.readermode.ReaderModeController
import org.mozilla.fenix.components.Analytics
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.Core
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
class QuickActionInteractorTest {
@Test
fun onOpened() {
val context: Context = mockk()
val metrics: MetricController = mockk()
val interactor = QuickActionInteractor(
context,
mockk(),
mockk(),
mockk(),
mockk(),
mockk()
)
every { context.metrics } returns metrics
every { metrics.track(Event.QuickActionSheetOpened) } just Runs
interactor.onOpened()
verify { metrics.track(Event.QuickActionSheetOpened) }
}
@Test
fun onClosed() {
val context: Context = mockk()
val metrics: MetricController = mockk()
val interactor = QuickActionInteractor(
context,
mockk(),
mockk(),
mockk(),
mockk(),
mockk()
)
every { context.metrics } returns metrics
every { metrics.track(Event.QuickActionSheetClosed) } just Runs
interactor.onClosed()
verify { metrics.track(Event.QuickActionSheetClosed) }
}
@Test
fun onSharedPressed() {
val context: Context = mockk()
val session: Session = mockk()
var selectedSessionUrl = ""
val metrics: MetricController = mockk()
val interactor = QuickActionInteractor(
context,
mockk(),
mockk(),
{ selectedSessionUrl = it },
mockk(),
mockk()
)
val components: Components = mockk()
val core: Core = mockk()
val sessionManager: SessionManager = mockk()
val analytics: Analytics = mockk()
every { session.url } returns "mozilla.org"
every { context.components } returns components
every { components.analytics } returns analytics
every { metrics.track(Event.QuickActionSheetShareTapped) } just Runs
// Since we are mocking components, we must manually define metrics as `analytics.metrics`
every { analytics.metrics } returns metrics
every { components.core } returns core
every { core.sessionManager } returns sessionManager
every { sessionManager.selectedSession } returns session
interactor.onSharedPressed()
verify { metrics.track(Event.QuickActionSheetShareTapped) }
assertEquals("mozilla.org", selectedSessionUrl)
}
@Test
fun onDownloadsPressed() {
val context: Context = mockk()
val metrics: MetricController = mockk()
val interactor = QuickActionInteractor(
context,
mockk(),
mockk(),
mockk(),
mockk(),
mockk()
)
every { context.metrics } returns metrics
every { metrics.track(Event.QuickActionSheetDownloadTapped) } just Runs
interactor.onDownloadsPressed()
verify { metrics.track(Event.QuickActionSheetDownloadTapped) }
}
@Test
fun onBookmarkPressed() {
val context: Context = mockk()
val session: Session = mockk()
var bookmarkedSession: Session? = null
val metrics: MetricController = mockk()
val interactor = QuickActionInteractor(
context,
mockk(),
mockk(),
mockk(),
{ bookmarkedSession = it },
mockk()
)
val components: Components = mockk()
val core: Core = mockk()
val sessionManager: SessionManager = mockk()
val analytics: Analytics = mockk()
every { session.url } returns "mozilla.org"
every { context.components } returns components
every { components.analytics } returns analytics
every { metrics.track(Event.QuickActionSheetBookmarkTapped) } just Runs
// Since we are mocking components, we must manually define metrics as `analytics.metrics`
every { analytics.metrics } returns metrics
every { components.core } returns core
every { core.sessionManager } returns sessionManager
every { sessionManager.selectedSession } returns session
interactor.onBookmarkPressed()
verify { metrics.track(Event.QuickActionSheetBookmarkTapped) }
assertEquals("mozilla.org", bookmarkedSession?.url)
}
@Test
fun onReadPressed() {
val context: Context = mockk()
val metrics: MetricController = mockk()
val session: Session = mockk()
val readerModeController: ReaderModeController = mockk(relaxed = true)
val quickActionSheetStore: QuickActionSheetStore = mockk(relaxed = true)
val interactor = QuickActionInteractor(
context,
readerModeController,
quickActionSheetStore,
mockk(),
mockk(),
mockk()
)
every { context.metrics } returns metrics
every { context.components.core.sessionManager.selectedSession } returns session
every { session.readerMode } returns false
every { metrics.track(Event.QuickActionSheetReadTapped) } just Runs
interactor.onReadPressed()
verify { metrics.track(Event.QuickActionSheetReadTapped) }
verify { readerModeController.showReaderView() }
}
@Test
fun onReadPressedWithActiveReaderMode() {
val context: Context = mockk()
val metrics: MetricController = mockk()
val session: Session = mockk()
val readerModeController: ReaderModeController = mockk(relaxed = true)
val quickActionSheetStore: QuickActionSheetStore = mockk(relaxed = true)
val interactor = QuickActionInteractor(
context,
readerModeController,
quickActionSheetStore,
mockk(),
mockk(),
mockk()
)
every { context.metrics } returns metrics
every { context.components.core.sessionManager.selectedSession } returns session
every { session.readerMode } returns true
every { metrics.track(Event.QuickActionSheetReadTapped) } just Runs
interactor.onReadPressed()
verify { metrics.track(Event.QuickActionSheetReadTapped) }
verify { readerModeController.hideReaderView() }
}
@Test
fun onAppearancePressed() {
val context: Context = mockk()
val readerModeController: ReaderModeController = mockk(relaxed = true)
val interactor = QuickActionInteractor(
context,
readerModeController,
mockk(),
mockk(),
mockk(),
mockk()
)
interactor.onAppearancePressed()
verify { readerModeController.showControls() }
}
@Test
fun onOpenAppLink() {
val context: Context = mockk()
val session: Session = mockk()
val appLinksUseCases: AppLinksUseCases = mockk()
val interactor = QuickActionInteractor(
context,
mockk(),
mockk(),
mockk(),
mockk(),
appLinksUseCases
)
every { context.components.core.sessionManager.selectedSession } returns session
every { session.url } returns "mozilla.org"
val getAppLinkRedirect: AppLinksUseCases.GetAppLinkRedirect = mockk()
val appLinkRedirect: AppLinkRedirect = mockk()
val openAppLink: AppLinksUseCases.OpenAppLinkRedirect = mockk(relaxed = true)
every { appLinksUseCases.appLinkRedirect } returns getAppLinkRedirect
every { getAppLinkRedirect.invoke("mozilla.org") } returns appLinkRedirect
every { appLinksUseCases.openAppLink } returns openAppLink
every { appLinkRedirect.appIntent } returns mockk(relaxed = true)
interactor.onOpenAppLinkPressed()
verify { openAppLink.invoke(appLinkRedirect) }
}
}