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.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.lifecycleScope
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 kotlinx.android.synthetic.main.component_search.*
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.ViewBoundFeatureWrapper
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.FenixViewModelProvider
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
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.FindInPageIntegration
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.toolbar.SearchAction
import org.mozilla.fenix.components.toolbar.SearchState
import org.mozilla.fenix.components.toolbar.ToolbarComponent
import org.mozilla.fenix.components.toolbar.BrowserInteractor
import org.mozilla.fenix.components.toolbar.BrowserState
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.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.ext.components
import org.mozilla.fenix.ext.enterToImmersiveMode
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.toTab
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.quickactionsheet.QuickActionInteractor
import org.mozilla.fenix.quickactionsheet.QuickActionSheetAction
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.quickactionsheet.DefaultQuickActionSheetController
import org.mozilla.fenix.quickactionsheet.QuickActionSheetView
import org.mozilla.fenix.utils.Settings
import java.net.MalformedURLException
import java.net.URL
@SuppressWarnings("TooManyFunctions", "LargeClass")
class BrowserFragment : Fragment(), BackHandler {
private lateinit var toolbarComponent: ToolbarComponent
private lateinit var quickActionSheetStore: QuickActionSheetStore
private lateinit var browserStore: BrowserStore
private lateinit var browserInteractor: BrowserInteractor
private lateinit var browserToolbarView: BrowserToolbarView
private lateinit var quickActionSheetView: QuickActionSheetView
private var tabCollectionObserver: Observer<List<TabCollection>>? = 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)
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()
val activity = activity as HomeActivity
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
}
@ -176,13 +167,62 @@ class BrowserFragment : Fragment(), BackHandler {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
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(
feature = (toolbarComponent.uiView as ToolbarUIView).toolbarIntegration,
feature = browserToolbarView.toolbarIntegration,
owner = this,
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(
feature = ContextMenuFeature(
@ -252,14 +292,6 @@ class BrowserFragment : Fragment(), BackHandler {
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())
sitePermissionsFeature.set(
@ -357,7 +389,7 @@ class BrowserFragment : Fragment(), BackHandler {
) { available ->
if (available) { requireComponents.analytics.metrics.track(Event.ReaderModeAvailable) }
quickActionSheetStore.apply {
browserStore.apply {
dispatch(QuickActionSheetAction.ReadableStateChange(available))
dispatch(QuickActionSheetAction.ReaderActiveStateChange(
sessionManager.selectedSession?.readerMode ?: false
@ -368,8 +400,6 @@ class BrowserFragment : Fragment(), BackHandler {
view = view
)
val actionEmitter = ActionBusFactory.get(this).getManagedEmitter(SearchAction::class.java)
customTabSessionId?.let {
customTabsIntegration.set(
feature = CustomTabsIntegration(
@ -380,46 +410,19 @@ class BrowserFragment : Fragment(), BackHandler {
activity,
view.nestedScrollQuickAction,
view.swipeRefresh,
onItemTapped = { actionEmitter.onNext(SearchAction.ToolbarMenuItemTapped(it)) }
onItemTapped = { browserInteractor.onBrowserToolbarMenuItemTapped(it) }
),
owner = this,
view = view)
}
toolbarComponent.setOnSiteSecurityClickedListener {
browserToolbarView.view.setOnSiteSecurityClickedListener {
showQuickSettingsDialog()
}
val appLink = requireComponents.useCases.appLinksUseCases.appLinkRedirect
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) {
consumeFrom(browserStore) {
quickActionSheetView.update(it)
browserToolbarView.update(it)
}
}
@ -484,28 +487,6 @@ class BrowserFragment : Fragment(), BackHandler {
getSessionById()?.let { (activity as HomeActivity).updateThemeForSession(it) }
(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()
}
@ -530,14 +511,14 @@ class BrowserFragment : Fragment(), BackHandler {
)
withContext(Main) {
quickActionSheetStore.dispatch(
browserStore.dispatch(
QuickActionSheetAction.BookmarkedStateChange(bookmarked = true)
)
requireComponents.analytics.metrics.track(Event.AddBookmark)
view?.let {
FenixSnackbar.make(it.rootView, Snackbar.LENGTH_LONG)
.setAnchorView(toolbarComponent.uiView.view)
.setAnchorView(browserToolbarView.view)
.setAction(getString(R.string.edit_bookmark_snackbar_action)) {
nav(
R.id.browserFragment,
@ -624,110 +605,6 @@ class BrowserFragment : Fragment(), BackHandler {
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() {
val settings = Settings.getInstance(requireContext())
@ -787,7 +664,7 @@ class BrowserFragment : Fragment(), BackHandler {
override fun onLoadingStateChanged(session: Session, loading: Boolean) {
if (!loading) {
updateBookmarkState(session)
quickActionSheetStore.dispatch(QuickActionSheetAction.BounceNeededChange)
browserStore.dispatch(QuickActionSheetAction.BounceNeededChange)
}
}
@ -827,7 +704,7 @@ class BrowserFragment : Fragment(), BackHandler {
findBookmarkJob = lifecycleScope.launch(IO) {
val found = findBookmarkedURL(session)
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) {
val url = session.url
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 {
@ -852,7 +729,7 @@ class BrowserFragment : Fragment(), BackHandler {
view?.let { view ->
FenixSnackbar.make(view, Snackbar.LENGTH_SHORT)
.setText(view.context.getString(R.string.create_collection_tab_saved))
.setAnchorView(toolbarComponent.uiView.view)
.setAnchorView(browserToolbarView.view)
.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 mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds
import org.mozilla.fenix.R
import org.mozilla.fenix.components.toolbar.BrowserState
import org.mozilla.fenix.utils.Settings
interface QuickActionSheetInteractor {
fun onOpened()
fun onClosed()
fun onSharedPressed()
fun onDownloadsPressed()
fun onBookmarkPressed()
fun onReadPressed()
fun onAppearancePressed()
fun onOpenAppLinkPressed()
interface QuickActionSheetViewInteractor {
fun onQuickActionSheetOpened()
fun onQuickActionSheetClosed()
fun onQuickActionSheetSharePressed()
fun onQuickActionSheetDownloadPressed()
fun onQuickActionSheetBookmarkPressed()
fun onQuickActionSheetReadPressed()
fun onQuickActionSheetAppearancePressed()
fun onQuickActionSheetOpenLinkPressed()
}
/**
* View for the quick action sheet that slides out from the toolbar.
*/
class QuickActionView(
class QuickActionSheetView(
override val containerView: ViewGroup,
private val interactor: QuickActionSheetInteractor
private val interactor: QuickActionSheetViewInteractor
) : LayoutContainer, View.OnClickListener {
val view: NestedScrollView = LayoutInflater.from(containerView.context)
@ -51,9 +52,9 @@ class QuickActionView(
updateImportantForAccessibility(state)
if (state == BottomSheetBehavior.STATE_EXPANDED) {
interactor.onOpened()
interactor.onQuickActionSheetOpened()
} else if (state == BottomSheetBehavior.STATE_COLLAPSED) {
interactor.onClosed()
interactor.onQuickActionSheetClosed()
}
}
@ -77,12 +78,12 @@ class QuickActionView(
*/
override fun onClick(button: View) {
when (button.id) {
R.id.quick_action_share -> interactor.onSharedPressed()
R.id.quick_action_downloads -> interactor.onDownloadsPressed()
R.id.quick_action_bookmark -> interactor.onBookmarkPressed()
R.id.quick_action_read -> interactor.onReadPressed()
R.id.quick_action_appearance -> interactor.onAppearancePressed()
R.id.quick_action_open_app_link -> interactor.onOpenAppLinkPressed()
R.id.quick_action_share -> interactor.onQuickActionSheetSharePressed()
R.id.quick_action_downloads -> interactor.onQuickActionSheetDownloadPressed()
R.id.quick_action_bookmark -> interactor.onQuickActionSheetBookmarkPressed()
R.id.quick_action_read -> interactor.onQuickActionSheetReadPressed()
R.id.quick_action_appearance -> interactor.onQuickActionSheetAppearancePressed()
R.id.quick_action_open_app_link -> interactor.onQuickActionSheetOpenLinkPressed()
else -> return
}
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
@ -108,27 +109,32 @@ class QuickActionView(
}
}
fun update(state: QuickActionSheetState) {
view.quick_action_read.isVisible = state.readable
view.quick_action_read.isSelected = state.readerActive
fun update(state: BrowserState) {
val quickActionSheetState = state.quickActionSheetState
view.quick_action_read.isVisible = quickActionSheetState.readable
view.quick_action_read.isSelected = quickActionSheetState.readerActive
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(
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()
}
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
* to respond to user interaction on the ToolbarView
* to respond to user interaction on the [BrowserToolbarView]
*/
interface ToolbarInteractor {
/**
* Called when a user hits the return key while ToolbarView has focus.
* @param url the text inside the ToolbarView when committed
* Called when a user hits the return key while [BrowserToolbarView] has focus.
* @param url the text inside the [BrowserToolbarView] when committed
*/
fun onUrlCommitted(url: String)
/**
* Called when a removes focus from the ToolbarView
* Called when a user removes focus from the [BrowserToolbarView]
*/
fun onEditingCanceled()
/**
* Called whenever the text inside the ToolbarView changes
* @param text the current text displayed by ToolbarView
* Called whenever the text inside the [BrowserToolbarView] changes
* @param text the current text displayed by [BrowserToolbarView]
*/
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) }
}
}