From 6fa022c2f8fa9223bb096decb6d41a833d5a1528 Mon Sep 17 00:00:00 2001 From: Sawyer Blatz Date: Mon, 29 Jul 2019 12:39:36 -0700 Subject: [PATCH] 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 --- .../mozilla/fenix/browser/BrowserFragment.kt | 309 ++++++------------ .../components/toolbar/BrowserInteractor.kt | 71 ++++ .../fenix/components/toolbar/BrowserStore.kt | 86 +++++ .../toolbar/BrowserToolbarController.kt | 187 +++++++++++ .../components/toolbar/BrowserToolbarView.kt | 124 +++++++ .../components/toolbar/ToolbarComponent.kt | 63 ---- .../components/toolbar/ToolbarMetrics.kt | 36 -- .../fenix/components/toolbar/ToolbarUIView.kt | 107 ------ .../quickactionsheet/QuickActionInteractor.kt | 89 ----- .../QuickActionSheetController.kt | 64 ++++ .../quickactionsheet/QuickActionSheetStore.kt | 63 ---- ...kActionView.kt => QuickActionSheetView.kt} | 64 ++-- .../fenix/search/toolbar/ToolbarView.kt | 12 +- .../toolbar/BrowserInteractorTest.kt | 258 +++++++++++++++ .../components/toolbar/BrowserStoreTest.kt | 75 +++++ .../QuickActionInteractorTest.kt | 274 ---------------- 16 files changed, 999 insertions(+), 883 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserInteractor.kt create mode 100644 app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserStore.kt create mode 100644 app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt create mode 100644 app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarComponent.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMetrics.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarUIView.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionInteractor.kt create mode 100644 app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetController.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetStore.kt rename app/src/main/java/org/mozilla/fenix/quickactionsheet/{QuickActionView.kt => QuickActionSheetView.kt} (67%) create mode 100644 app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserInteractorTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserStoreTest.kt delete mode 100644 app/src/test/java/org/mozilla/fenix/quickactionsheet/QuickActionInteractorTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index 92442bab5..885a984a3 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -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>? = 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() - .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() } } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserInteractor.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserInteractor.kt new file mode 100644 index 000000000..3795704d3 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserInteractor.kt @@ -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() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserStore.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserStore.kt new file mode 100644 index 000000000..bd8b9c4f9 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserStore.kt @@ -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(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)) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt new file mode 100644 index 000000000..3616bb031 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt @@ -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)) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt new file mode 100644 index 000000000..375fb6b5d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt @@ -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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarComponent.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarComponent.kt deleted file mode 100644 index e3298072c..000000000 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarComponent.kt +++ /dev/null @@ -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 -) : - UIComponent( - 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(initialState, reducer) { - - companion object { - val reducer: Reducer = { state, _ -> state } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMetrics.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMetrics.kt deleted file mode 100644 index f952f1a42..000000000 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMetrics.kt +++ /dev/null @@ -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)) -} diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarUIView.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarUIView.kt deleted file mode 100644 index eecc7be44..000000000 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarUIView.kt +++ /dev/null @@ -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, - changesObservable: Observable -) : - UIView(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 {} - - companion object { - private const val TOOLBAR_ELEVATION = 16 - private const val PROGRESS_BOTTOM = 0 - private const val PROGRESS_TOP = 1 - const val browserActionMarginDp = 8 - } -} diff --git a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionInteractor.kt b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionInteractor.kt deleted file mode 100644 index 66aa0d0fc..000000000 --- a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionInteractor.kt +++ /dev/null @@ -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() - } -} diff --git a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetController.kt b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetController.kt new file mode 100644 index 000000000..9f6b1deb5 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetController.kt @@ -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) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetStore.kt b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetStore.kt deleted file mode 100644 index c662c4f57..000000000 --- a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetStore.kt +++ /dev/null @@ -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(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) - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionView.kt b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetView.kt similarity index 67% rename from app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionView.kt rename to app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetView.kt index 504eb43b5..2dd4f0f86 100644 --- a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionView.kt +++ b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetView.kt @@ -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 } } diff --git a/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt b/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt index 19aaabd7b..1ee6ae223 100644 --- a/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt @@ -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) } diff --git a/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserInteractorTest.kt new file mode 100644 index 000000000..0f582e1ce --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserInteractorTest.kt @@ -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() } + } +} \ No newline at end of file diff --git a/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserStoreTest.kt b/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserStoreTest.kt new file mode 100644 index 000000000..2ef12d46e --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserStoreTest.kt @@ -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 + ) +} diff --git a/app/src/test/java/org/mozilla/fenix/quickactionsheet/QuickActionInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/quickactionsheet/QuickActionInteractorTest.kt deleted file mode 100644 index 4824f02f3..000000000 --- a/app/src/test/java/org/mozilla/fenix/quickactionsheet/QuickActionInteractorTest.kt +++ /dev/null @@ -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) } - } -}