* Closes https://github.com/mozilla-mobile/fenix/issues/3986: Migrate QuickActionSheet to LibState

* Closes https://github.com/mozilla-mobile/fenix/issues/3661: Add tests for QuickActionSheet

Co-authored-by: boek <jeff@jeffboek.com>

* For https://github.com/mozilla-mobile/fenix/issues/3986: Fix feedback
pull/600/head
Sawyer Blatz 5 years ago committed by GitHub
parent 85b152e817
commit 8cb4414365

@ -25,6 +25,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenStarted
import androidx.navigation.fragment.NavHostFragment.findNavController import androidx.navigation.fragment.NavHostFragment.findNavController
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -34,8 +35,10 @@ import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
@ -53,6 +56,7 @@ import mozilla.components.feature.session.ThumbnailsFeature
import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.sitepermissions.SitePermissionsFeature import mozilla.components.feature.sitepermissions.SitePermissionsFeature
import mozilla.components.feature.sitepermissions.SitePermissionsRules import mozilla.components.feature.sitepermissions.SitePermissionsRules
import mozilla.components.lib.state.ext.observe
import mozilla.components.support.base.feature.BackHandler import mozilla.components.support.base.feature.BackHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded
@ -63,11 +67,13 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
import org.mozilla.fenix.collections.CreateCollectionViewModel import org.mozilla.fenix.collections.CreateCollectionViewModel
import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.collections.getStepForCollectionsSize import org.mozilla.fenix.collections.getStepForCollectionsSize
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.FindInPageIntegration import org.mozilla.fenix.components.FindInPageIntegration
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.Event.BrowserMenuItemTapped.Item import org.mozilla.fenix.components.metrics.Event.BrowserMenuItemTapped.Item
@ -90,14 +96,13 @@ import org.mozilla.fenix.lib.Do
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.mvi.getManagedEmitter import org.mozilla.fenix.mvi.getManagedEmitter
import org.mozilla.fenix.quickactionsheet.QuickActionAction import org.mozilla.fenix.quickactionsheet.QuickActionInteractor
import org.mozilla.fenix.quickactionsheet.QuickActionChange import org.mozilla.fenix.quickactionsheet.QuickActionSheetAction
import org.mozilla.fenix.quickactionsheet.QuickActionComponent
import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior
import org.mozilla.fenix.quickactionsheet.QuickActionState import org.mozilla.fenix.quickactionsheet.QuickActionSheetState
import org.mozilla.fenix.quickactionsheet.QuickActionViewModel import org.mozilla.fenix.quickactionsheet.QuickActionSheetStore
import org.mozilla.fenix.quickactionsheet.QuickActionView
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.ItsNotBrokenSnack
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
@ -105,6 +110,7 @@ import java.net.URL
@SuppressWarnings("TooManyFunctions", "LargeClass") @SuppressWarnings("TooManyFunctions", "LargeClass")
class BrowserFragment : Fragment(), BackHandler { class BrowserFragment : Fragment(), BackHandler {
private lateinit var toolbarComponent: ToolbarComponent private lateinit var toolbarComponent: ToolbarComponent
private lateinit var quickActionSheetStore: QuickActionSheetStore
private var tabCollectionObserver: Observer<List<TabCollection>>? = null private var tabCollectionObserver: Observer<List<TabCollection>>? = null
private var sessionObserver: Session.Observer? = null private var sessionObserver: Session.Observer? = null
@ -164,26 +170,6 @@ class BrowserFragment : Fragment(), BackHandler {
startPostponedEnterTransition() startPostponedEnterTransition()
QuickActionComponent(
view.nestedScrollQuickAction,
ActionBusFactory.get(this),
FenixViewModelProvider.create(
this,
QuickActionViewModel::class.java
) {
val appLink = requireComponents.useCases.appLinksUseCases.appLinkRedirect
QuickActionViewModel(
QuickActionState(
readable = getSessionById()?.readerable ?: false,
bookmarked = findBookmarkedURL(getSessionById()),
readerActive = getSessionById()?.readerMode ?: false,
bounceNeeded = false,
isAppLink = getSessionById()?.let { appLink.invoke(it.url).hasExternalApp() } ?: false
)
)
}
)
val activity = activity as HomeActivity val activity = activity as HomeActivity
ThemeManager.applyStatusBarTheme(activity.window, activity.themeManager, activity) ThemeManager.applyStatusBarTheme(activity.window, activity.themeManager, activity)
@ -372,18 +358,14 @@ class BrowserFragment : Fragment(), BackHandler {
requireComponents.core.engine, requireComponents.core.engine,
requireComponents.core.sessionManager, requireComponents.core.sessionManager,
view.readerViewControlsBar view.readerViewControlsBar
) { ) { available ->
getManagedEmitter<QuickActionChange>().apply { if (available) { requireComponents.analytics.metrics.track(Event.ReaderModeAvailable) }
if (it) {
requireComponents.analytics.metrics.track(Event.ReaderModeAvailable) quickActionSheetStore.apply {
} dispatch(QuickActionSheetAction.ReadableStateChange(available))
dispatch(QuickActionSheetAction.ReaderActiveStateChange(
onNext(QuickActionChange.ReadableStateChange(it)) sessionManager.selectedSession?.readerMode ?: false
onNext( ))
QuickActionChange.ReaderActiveStateChange(
sessionManager.selectedSession?.readerMode ?: false
)
)
} }
}, },
owner = this, owner = this,
@ -411,6 +393,42 @@ class BrowserFragment : Fragment(), BackHandler {
toolbarComponent.getView().setOnSiteSecurityClickedListener { toolbarComponent.getView().setOnSiteSecurityClickedListener {
showQuickSettingsDialog() 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
)
)
quickActionSheetStore.observe(view) {
viewLifecycleOwner.lifecycleScope.launch {
whenStarted {
quickActionSheetView.update(it)
}
}
}
} }
private fun themeReaderViewControlsForPrivateMode(view: View) = with(view) { private fun themeReaderViewControlsForPrivateMode(view: View) = with(view) {
@ -495,115 +513,46 @@ class BrowserFragment : Fragment(), BackHandler {
} }
} }
} }
assignSitePermissionsRules()
}
getAutoDisposeObservable<QuickActionAction>() private suspend fun bookmarkTapped(session: Session) = withContext(IO) {
.subscribe { val bookmarksStorage = requireComponents.core.bookmarksStorage
when (it) { val existing = bookmarksStorage.getBookmarksWithUrl(session.url).firstOrNull { it.url == session.url }
is QuickActionAction.Opened -> { if (existing != null) {
requireComponents.analytics.metrics.track(Event.QuickActionSheetOpened) // Bookmark exists, go to edit fragment
} withContext(Main) {
is QuickActionAction.Closed -> { nav(
requireComponents.analytics.metrics.track(Event.QuickActionSheetClosed) R.id.browserFragment,
} BrowserFragmentDirections.actionBrowserFragmentToBookmarkEditFragment(existing.guid)
is QuickActionAction.SharePressed -> { )
requireComponents.analytics.metrics.track(Event.QuickActionSheetShareTapped)
getSessionById()?.let { session ->
shareUrl(session.url)
}
}
is QuickActionAction.DownloadsPressed -> {
requireComponents.analytics.metrics.track(Event.QuickActionSheetDownloadTapped)
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "348")
}
is QuickActionAction.BookmarkPressed -> {
requireComponents.analytics.metrics.track(Event.QuickActionSheetBookmarkTapped)
bookmarkTapped()
}
is QuickActionAction.ReadPressed -> {
readerViewFeature.withFeature { feature ->
requireComponents.analytics.metrics.track(Event.QuickActionSheetReadTapped)
val actionEmitter = getManagedEmitter<QuickActionChange>()
val enabled = requireComponents.core.sessionManager.selectedSession?.readerMode ?: false
if (enabled) {
feature.hideReaderView()
actionEmitter.onNext(QuickActionChange.ReaderActiveStateChange(false))
} else {
feature.showReaderView()
actionEmitter.onNext(QuickActionChange.ReaderActiveStateChange(true))
requireComponents.analytics.metrics.track(Event.ReaderModeOpened)
}
}
}
is QuickActionAction.ReadAppearancePressed -> {
requireComponents.analytics.metrics.track(Event.ReaderModeAppearanceOpened)
readerViewFeature.withFeature { feature ->
feature.showControls()
}
}
is QuickActionAction.OpenAppLinkPressed -> {
appLinksFeature.withFeature { feature ->
val getRedirect = requireComponents.useCases.appLinksUseCases.appLinkRedirect
val redirect = getSessionById()?.let { session ->
getRedirect.invoke(session.url)
} ?: return@withFeature
redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
val openAppLink = requireComponents.useCases.appLinksUseCases.openAppLink
openAppLink.invoke(redirect)
}
}
}
} }
} else {
// Save bookmark, then go to edit fragment
val guid = bookmarksStorage.addItem(
BookmarkRoot.Mobile.id,
url = session.url,
title = session.title,
position = null
)
assignSitePermissionsRules() withContext(Main) {
} quickActionSheetStore.dispatch(
QuickActionSheetAction.BookmarkedStateChange(bookmarked = true)
)
requireComponents.analytics.metrics.track(Event.AddBookmark)
private fun bookmarkTapped() { view?.let {
getSessionById()?.let { session -> FenixSnackbar.make(it.rootView, Snackbar.LENGTH_LONG)
lifecycleScope.launch(IO) { .setAnchorView(toolbarComponent.uiView.view)
val bookmarksStorage = requireComponents.core.bookmarksStorage .setAction(getString(R.string.edit_bookmark_snackbar_action)) {
val existing = bookmarksStorage.getBookmarksWithUrl(session.url) nav(
val found = existing.isNotEmpty() && existing[0].url == session.url R.id.browserFragment,
if (found) { BrowserFragmentDirections.actionBrowserFragmentToBookmarkEditFragment(guid)
launch(Main) {
nav(
R.id.browserFragment,
BrowserFragmentDirections
.actionBrowserFragmentToBookmarkEditFragment(existing[0].guid)
)
}
} else {
val guid = bookmarksStorage.addItem(
BookmarkRoot.Mobile.id,
session.url,
session.title,
null
)
launch(Main) {
getManagedEmitter<QuickActionChange>()
.onNext(QuickActionChange.BookmarkedStateChange(true))
requireComponents.analytics.metrics.track(Event.AddBookmark)
view?.let {
FenixSnackbar.make(
it.rootView,
Snackbar.LENGTH_LONG
) )
.setAnchorView(toolbarComponent.uiView.view)
.setAction(getString(R.string.edit_bookmark_snackbar_action)) {
nav(
R.id.browserFragment,
BrowserFragmentDirections
.actionBrowserFragmentToBookmarkEditFragment(
guid
)
)
}
.setText(getString(R.string.bookmark_saved_snackbar))
.show()
} }
} .setText(getString(R.string.bookmark_saved_snackbar))
.show()
} }
} }
} }
@ -881,7 +830,7 @@ class BrowserFragment : Fragment(), BackHandler {
override fun onLoadingStateChanged(session: Session, loading: Boolean) { override fun onLoadingStateChanged(session: Session, loading: Boolean) {
if (!loading) { if (!loading) {
updateBookmarkState(session) updateBookmarkState(session)
getManagedEmitter<QuickActionChange>().onNext(QuickActionChange.BounceNeededChange) quickActionSheetStore.dispatch(QuickActionSheetAction.BounceNeededChange)
} }
super.onLoadingStateChanged(session, loading) super.onLoadingStateChanged(session, loading)
@ -923,12 +872,11 @@ class BrowserFragment : Fragment(), BackHandler {
} }
private fun updateBookmarkState(session: Session) { private fun updateBookmarkState(session: Session) {
if (findBookmarkJob?.isActive == true) findBookmarkJob?.cancel() findBookmarkJob?.cancel()
findBookmarkJob = lifecycleScope.launch(IO) { findBookmarkJob = lifecycleScope.launch(IO) {
val found = findBookmarkedURL(session) val found = findBookmarkedURL(session)
launch(Main) { withContext(Main) {
getManagedEmitter<QuickActionChange>() quickActionSheetStore.dispatch(QuickActionSheetAction.BookmarkedStateChange(found))
.onNext(QuickActionChange.BookmarkedStateChange(found))
} }
} }
} }
@ -936,8 +884,7 @@ class BrowserFragment : Fragment(), BackHandler {
private fun updateAppLinksState(session: Session) { private fun updateAppLinksState(session: Session) {
val url = session.url val url = session.url
val appLinks = requireComponents.useCases.appLinksUseCases.appLinkRedirect val appLinks = requireComponents.useCases.appLinksUseCases.appLinkRedirect
getManagedEmitter<QuickActionChange>() quickActionSheetStore.dispatch(QuickActionSheetAction.AppLinkStateChange(appLinks.invoke(url).hasExternalApp()))
.onNext(QuickActionChange.AppLinkStateChange(appLinks.invoke(url).hasExternalApp()))
} }
private val collectionStorageObserver = object : TabCollectionStorage.Observer { private val collectionStorageObserver = object : TabCollectionStorage.Observer {

@ -0,0 +1,33 @@
/* 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.browser.readermode
import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
/**
* An interface that exposes the hide and show reader view functions of a ReaderViewFeature
*/
interface ReaderModeController {
fun hideReaderView()
fun showReaderView()
fun showControls()
}
class DefaultReaderModeController(
private val readerViewFeature: ViewBoundFeatureWrapper<ReaderViewFeature>
) : ReaderModeController {
override fun hideReaderView() {
readerViewFeature.withFeature { it.hideReaderView() }
}
override fun showReaderView() {
readerViewFeature.withFeature { it.showReaderView() }
}
override fun showControls() {
readerViewFeature.withFeature { it.showControls() }
}
}

@ -1,86 +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.view.ViewGroup
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.UIComponentViewModelProvider
import org.mozilla.fenix.mvi.ViewState
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.UIView
class QuickActionComponent(
private val container: ViewGroup,
bus: ActionBusFactory,
viewModelProvider: UIComponentViewModelProvider<QuickActionState, QuickActionChange>
) : UIComponent<QuickActionState, QuickActionAction, QuickActionChange>(
bus.getManagedEmitter(QuickActionAction::class.java),
bus.getSafeManagedObservable(QuickActionChange::class.java),
viewModelProvider
) {
override fun initView(): UIView<QuickActionState, QuickActionAction, QuickActionChange> =
QuickActionUIView(container, actionEmitter, changesObservable)
init {
bind()
}
}
data class QuickActionState(
val readable: Boolean,
val bookmarked: Boolean,
val readerActive: Boolean,
val bounceNeeded: Boolean,
val isAppLink: Boolean
) : ViewState
sealed class QuickActionAction : Action {
object Opened : QuickActionAction()
object Closed : QuickActionAction()
object SharePressed : QuickActionAction()
object DownloadsPressed : QuickActionAction()
object BookmarkPressed : QuickActionAction()
object ReadPressed : QuickActionAction()
object ReadAppearancePressed : QuickActionAction()
object OpenAppLinkPressed : QuickActionAction()
}
sealed class QuickActionChange : Change {
data class BookmarkedStateChange(val bookmarked: Boolean) : QuickActionChange()
data class ReadableStateChange(val readable: Boolean) : QuickActionChange()
data class ReaderActiveStateChange(val active: Boolean) : QuickActionChange()
data class AppLinkStateChange(val isAppLink: Boolean) : QuickActionChange()
object BounceNeededChange : QuickActionChange()
}
class QuickActionViewModel(
initialState: QuickActionState
) : UIComponentViewModelBase<QuickActionState, QuickActionChange>(initialState, reducer) {
companion object {
val reducer: Reducer<QuickActionState, QuickActionChange> = { state, change ->
when (change) {
is QuickActionChange.BounceNeededChange -> {
state.copy(bounceNeeded = true)
}
is QuickActionChange.BookmarkedStateChange -> {
state.copy(bookmarked = change.bookmarked)
}
is QuickActionChange.ReadableStateChange -> {
state.copy(readable = change.readable)
}
is QuickActionChange.ReaderActiveStateChange -> {
state.copy(readerActive = change.active)
}
is QuickActionChange.AppLinkStateChange -> {
state.copy(isAppLink = change.isAppLink)
}
}
}
}
}

@ -0,0 +1,89 @@
/* 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()
}
}

@ -5,26 +5,24 @@
package org.mozilla.fenix.quickactionsheet package org.mozilla.fenix.quickactionsheet
import android.content.Context import android.content.Context
import android.os.Bundle
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import org.mozilla.fenix.R import org.mozilla.fenix.R
import android.os.Bundle
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.android.synthetic.main.layout_quick_action_sheet.view.* import kotlinx.android.synthetic.main.layout_quick_action_sheet.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch
import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import kotlin.coroutines.CoroutineContext
const val POSITION_SNAP_BUFFER = 1f const val POSITION_SNAP_BUFFER = 1f
@ -33,21 +31,18 @@ class QuickActionSheet @JvmOverloads constructor(
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyle: Int = 0, defStyle: Int = 0,
defStyleRes: Int = 0 defStyleRes: Int = 0
) : LinearLayout(context, attrs, defStyle, defStyleRes), CoroutineScope { ) : LinearLayout(context, attrs, defStyle, defStyleRes) {
private lateinit var job: Job private val scope = MainScope()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
private lateinit var quickActionSheetBehavior: QuickActionSheetBehavior private lateinit var quickActionSheetBehavior: QuickActionSheetBehavior
init { init {
inflate(getContext(), R.layout.layout_quick_action_sheet, this) inflate(context, R.layout.layout_quick_action_sheet, this)
} }
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
job = Job()
quickActionSheetBehavior = BottomSheetBehavior.from(quick_action_sheet.parent as View) quickActionSheetBehavior = BottomSheetBehavior.from(quick_action_sheet.parent as View)
as QuickActionSheetBehavior as QuickActionSheetBehavior
quickActionSheetBehavior.isHideable = false quickActionSheetBehavior.isHideable = false
@ -56,7 +51,7 @@ class QuickActionSheet @JvmOverloads constructor(
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
super.onDetachedFromWindow() super.onDetachedFromWindow()
job.cancel() scope.cancel()
} }
private fun setupHandle() { private fun setupHandle() {
@ -71,13 +66,13 @@ class QuickActionSheet @JvmOverloads constructor(
} }
fun bounceSheet() { fun bounceSheet() {
launch(Main) { Settings.getInstance(context).incrementAutomaticBounceQuickActionSheetCount()
scope.launch(Dispatchers.Main) {
delay(BOUNCE_ANIMATION_DELAY_LENGTH) delay(BOUNCE_ANIMATION_DELAY_LENGTH)
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED quickActionSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
delay(BOUNCE_ANIMATION_PAUSE_LENGTH) delay(BOUNCE_ANIMATION_PAUSE_LENGTH)
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
} }
Settings.getInstance(context).incrementAutomaticBounceQuickActionSheetCount()
} }
class HandleAccessibilityDelegate( class HandleAccessibilityDelegate(
@ -98,17 +93,16 @@ class QuickActionSheet @JvmOverloads constructor(
} }
override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean { override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
when (action) { finalState = when (action) {
AccessibilityNodeInfo.ACTION_CLICK -> { AccessibilityNodeInfo.ACTION_CLICK ->
finalState = when (quickActionSheetBehavior.state) { when (quickActionSheetBehavior.state) {
BottomSheetBehavior.STATE_EXPANDED -> BottomSheetBehavior.STATE_COLLAPSED BottomSheetBehavior.STATE_EXPANDED -> BottomSheetBehavior.STATE_COLLAPSED
else -> BottomSheetBehavior.STATE_EXPANDED else -> BottomSheetBehavior.STATE_EXPANDED
} }
}
AccessibilityNodeInfo.ACTION_COLLAPSE -> AccessibilityNodeInfo.ACTION_COLLAPSE ->
finalState = BottomSheetBehavior.STATE_COLLAPSED BottomSheetBehavior.STATE_COLLAPSED
AccessibilityNodeInfo.ACTION_EXPAND -> AccessibilityNodeInfo.ACTION_EXPAND ->
finalState = BottomSheetBehavior.STATE_EXPANDED BottomSheetBehavior.STATE_EXPANDED
else -> return super.performAccessibilityAction(host, action, args) else -> return super.performAccessibilityAction(host, action, args)
} }
@ -133,7 +127,6 @@ class QuickActionSheet @JvmOverloads constructor(
} }
} }
@Suppress("unused") // Referenced from XML
class QuickActionSheetBehavior( class QuickActionSheetBehavior(
context: Context, context: Context,
attrs: AttributeSet attrs: AttributeSet
@ -168,4 +161,9 @@ class QuickActionSheetBehavior(
} }
quickActionSheetContainer.translationY = toolbar.translationY + toolbar.height * -1.0f quickActionSheetContainer.translationY = toolbar.translationY + toolbar.height * -1.0f
} }
companion object {
fun from(view: NestedScrollView) =
BottomSheetBehavior.from(view) as QuickActionSheetBehavior
}
} }

@ -0,0 +1,63 @@
/* 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)
}
}
}

@ -1,161 +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.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.annotation.DrawableRes
import androidx.core.content.edit
import androidx.core.widget.NestedScrollView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer
import kotlinx.android.synthetic.main.fragment_browser.*
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.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.utils.Settings
class QuickActionUIView(
container: ViewGroup,
actionEmitter: Observer<QuickActionAction>,
changesObservable: Observable<QuickActionChange>
) : UIView<QuickActionState, QuickActionAction, QuickActionChange>(container, actionEmitter, changesObservable) {
override val view: NestedScrollView = LayoutInflater.from(container.context)
.inflate(R.layout.component_quick_action_sheet, container, true)
.findViewById(R.id.nestedScrollQuickAction) as NestedScrollView
val quickActionSheet = view.quick_action_sheet as QuickActionSheet
init {
val quickActionSheetBehavior =
BottomSheetBehavior.from(nestedScrollQuickAction as View) as QuickActionSheetBehavior
quickActionSheetBehavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(v: View, state: Int) {
updateImportantForAccessibility(state)
if (state == BottomSheetBehavior.STATE_EXPANDED) {
actionEmitter.onNext(QuickActionAction.Opened)
} else if (state == BottomSheetBehavior.STATE_COLLAPSED) {
actionEmitter.onNext(QuickActionAction.Closed)
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
animateOverlay(slideOffset)
}
})
updateImportantForAccessibility(quickActionSheetBehavior.state)
view.quick_action_share.setOnClickListener {
actionEmitter.onNext(QuickActionAction.SharePressed)
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
view.quick_action_downloads.setOnClickListener {
actionEmitter.onNext(QuickActionAction.DownloadsPressed)
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
view.quick_action_bookmark.setOnClickListener {
actionEmitter.onNext(QuickActionAction.BookmarkPressed)
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
view.quick_action_read.setOnClickListener {
actionEmitter.onNext(QuickActionAction.ReadPressed)
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
view.quick_action_read_appearance.setOnClickListener {
actionEmitter.onNext(QuickActionAction.ReadAppearancePressed)
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
view.quick_action_open_app_link.setOnClickListener {
actionEmitter.onNext(QuickActionAction.OpenAppLinkPressed)
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
/**
* Changes alpha of overlay based on new offset of this sheet within [-1,1] range.
*/
private fun animateOverlay(offset: Float) {
overlay.alpha = (1 - offset)
}
private fun updateImportantForAccessibility(state: Int) {
view.findViewById<LinearLayout>(R.id.quick_action_buttons_layout).importantForAccessibility =
if (state == BottomSheetBehavior.STATE_COLLAPSED || state == BottomSheetBehavior.STATE_HIDDEN)
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
else
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
}
private fun sendTelemetryEvent(state: Int) {
when (state) {
BottomSheetBehavior.STATE_EXPANDED ->
view.context.components.analytics.metrics.track(Event.QuickActionSheetOpened)
BottomSheetBehavior.STATE_COLLAPSED ->
view.context.components.analytics.metrics.track(Event.QuickActionSheetClosed)
}
}
@Suppress("ComplexMethod")
override fun updateView() = Consumer<QuickActionState> {
view.quick_action_read.apply {
visibility = if (it.readable) View.VISIBLE else View.GONE
val shouldNotify = Settings.getInstance(context).preferences
.getBoolean(context.getString(R.string.pref_key_reader_mode_notification), true)
updateReaderModeButton(it.readable && shouldNotify)
isSelected = it.readerActive
text = if (it.readerActive) {
context.getString(R.string.quick_action_read_close)
} else {
context.getString(R.string.quick_action_read)
}
}
view.quick_action_read_appearance.visibility = if (it.readerActive) View.VISIBLE else View.GONE
view.quick_action_bookmark.isSelected = it.bookmarked
view.quick_action_bookmark.text = if (it.bookmarked) {
view.context.getString(R.string.quick_action_bookmark_edit)
} else {
view.context.getString(R.string.quick_action_bookmark)
}
if (it.bounceNeeded && Settings.getInstance(view.context).shouldAutoBounceQuickActionSheet) {
quickActionSheet.bounceSheet()
}
view.quick_action_open_app_link.apply {
visibility = if (it.isAppLink) View.VISIBLE else View.GONE
}
}
private fun updateReaderModeButton(withNotification: Boolean) {
@DrawableRes
val readerTwoStateDrawableId = if (withNotification) {
quickActionSheet.bounceSheet()
Settings.getInstance(view.context).preferences.edit {
putBoolean(view.context.getString(R.string.pref_key_reader_mode_notification), false)
}
R.drawable.reader_two_state_with_notification
} else {
R.drawable.reader_two_state
}
view.quick_action_read.putCompoundDrawablesRelativeWithIntrinsicBounds(
top = view.context.getDrawable(readerTwoStateDrawableId)
)
}
}

@ -0,0 +1,152 @@
/* 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.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.core.content.edit
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.fragment_browser.*
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.utils.Settings
interface QuickActionSheetInteractor {
fun onOpened()
fun onClosed()
fun onSharedPressed()
fun onDownloadsPressed()
fun onBookmarkPressed()
fun onReadPressed()
fun onAppearancePressed()
fun onOpenAppLinkPressed()
}
/**
* View for the quick action sheet that slides out from the toolbar.
*/
class QuickActionView(
override val containerView: ViewGroup,
private val interactor: QuickActionSheetInteractor
) : LayoutContainer, View.OnClickListener {
val view: NestedScrollView = LayoutInflater.from(containerView.context)
.inflate(R.layout.component_quick_action_sheet, containerView, true)
.findViewById(R.id.nestedScrollQuickAction)
private val quickActionSheet = view.quick_action_sheet as QuickActionSheet
private val quickActionSheetBehavior = QuickActionSheetBehavior.from(nestedScrollQuickAction)
init {
quickActionSheetBehavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(v: View, state: Int) {
updateImportantForAccessibility(state)
if (state == BottomSheetBehavior.STATE_EXPANDED) {
interactor.onOpened()
} else if (state == BottomSheetBehavior.STATE_COLLAPSED) {
interactor.onClosed()
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
animateOverlay(slideOffset)
}
})
updateImportantForAccessibility(quickActionSheetBehavior.state)
view.quick_action_share.setOnClickListener(this)
view.quick_action_downloads.setOnClickListener(this)
view.quick_action_bookmark.setOnClickListener(this)
view.quick_action_read.setOnClickListener(this)
view.quick_action_appearance.setOnClickListener(this)
view.quick_action_open_app_link.setOnClickListener(this)
}
/**
* Handles clicks from quick action buttons
*/
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()
else -> return
}
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
/**
* Changes alpha of overlay based on new offset of this sheet within [-1,1] range.
*/
private fun animateOverlay(offset: Float) {
overlay.alpha = (1 - offset)
}
/**
* Updates the important for accessibility flag on the buttons container,
* depending on if the sheet is opened or closed.
*/
private fun updateImportantForAccessibility(state: Int) {
view.quick_action_buttons_layout.importantForAccessibility = when (state) {
BottomSheetBehavior.STATE_COLLAPSED, BottomSheetBehavior.STATE_HIDDEN ->
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
else ->
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
}
}
fun update(state: QuickActionSheetState) {
view.quick_action_read.isVisible = state.readable
view.quick_action_read.isSelected = state.readerActive
view.quick_action_read.text = view.context.getString(
if (state.readerActive) R.string.quick_action_read_close else R.string.quick_action_read
)
notifyReaderModeButton(state.readable)
view.quick_action_appearance.isVisible = state.readerActive
view.quick_action_bookmark.isSelected = state.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 (state.bounceNeeded && Settings.getInstance(view.context).shouldAutoBounceQuickActionSheet) {
quickActionSheet.bounceSheet()
}
view.quick_action_open_app_link.apply {
visibility = if (state.isAppLink) View.VISIBLE else View.GONE
}
}
private fun notifyReaderModeButton(readable: Boolean) {
val settings = Settings.getInstance(view.context).preferences
val shouldNotifyKey = view.context.getString(R.string.pref_key_reader_mode_notification)
@DrawableRes
val readerTwoStateDrawableRes = if (readable && settings.getBoolean(shouldNotifyKey, true)) {
quickActionSheet.bounceSheet()
settings.edit { putBoolean(shouldNotifyKey, false) }
R.drawable.reader_two_state_with_notification
} else {
R.drawable.reader_two_state
}
view.quick_action_read.putCompoundDrawablesRelativeWithIntrinsicBounds(
top = view.context.getDrawable(readerTwoStateDrawableRes)
)
}
}

@ -78,7 +78,7 @@
android:textSize="12sp" /> android:textSize="12sp" />
<Button <Button
android:id="@+id/quick_action_read_appearance" android:id="@+id/quick_action_appearance"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"

@ -0,0 +1,274 @@
/* 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) }
}
}
Loading…
Cancel
Save