* Closes #3986: Migrate QuickActionSheet to LibState * Closes #3661: Add tests for QuickActionSheet Co-authored-by: boek <jeff@jeffboek.com> * For #3986: Fix feedbackmaster
parent
9d9685625a
commit
7588251f8b
|
@ -25,6 +25,7 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.whenStarted
|
||||
import androidx.navigation.fragment.NavHostFragment.findNavController
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
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.Main
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.appservices.places.BookmarkRoot
|
||||
import mozilla.components.browser.session.Session
|
||||
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.SitePermissionsFeature
|
||||
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.ViewBoundFeatureWrapper
|
||||
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.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.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.getAutoDisposeObservable
|
||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
||||
import org.mozilla.fenix.quickactionsheet.QuickActionAction
|
||||
import org.mozilla.fenix.quickactionsheet.QuickActionChange
|
||||
import org.mozilla.fenix.quickactionsheet.QuickActionComponent
|
||||
import org.mozilla.fenix.quickactionsheet.QuickActionInteractor
|
||||
import org.mozilla.fenix.quickactionsheet.QuickActionSheetAction
|
||||
import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior
|
||||
import org.mozilla.fenix.quickactionsheet.QuickActionState
|
||||
import org.mozilla.fenix.quickactionsheet.QuickActionViewModel
|
||||
import org.mozilla.fenix.quickactionsheet.QuickActionSheetState
|
||||
import org.mozilla.fenix.quickactionsheet.QuickActionSheetStore
|
||||
import org.mozilla.fenix.quickactionsheet.QuickActionView
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
import org.mozilla.fenix.utils.ItsNotBrokenSnack
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
|
@ -105,6 +110,7 @@ import java.net.URL
|
|||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||
class BrowserFragment : Fragment(), BackHandler {
|
||||
private lateinit var toolbarComponent: ToolbarComponent
|
||||
private lateinit var quickActionSheetStore: QuickActionSheetStore
|
||||
|
||||
private var tabCollectionObserver: Observer<List<TabCollection>>? = null
|
||||
private var sessionObserver: Session.Observer? = null
|
||||
|
@ -164,26 +170,6 @@ class BrowserFragment : Fragment(), BackHandler {
|
|||
|
||||
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
|
||||
ThemeManager.applyStatusBarTheme(activity.window, activity.themeManager, activity)
|
||||
|
||||
|
@ -372,18 +358,14 @@ class BrowserFragment : Fragment(), BackHandler {
|
|||
requireComponents.core.engine,
|
||||
requireComponents.core.sessionManager,
|
||||
view.readerViewControlsBar
|
||||
) {
|
||||
getManagedEmitter<QuickActionChange>().apply {
|
||||
if (it) {
|
||||
requireComponents.analytics.metrics.track(Event.ReaderModeAvailable)
|
||||
}
|
||||
) { available ->
|
||||
if (available) { requireComponents.analytics.metrics.track(Event.ReaderModeAvailable) }
|
||||
|
||||
onNext(QuickActionChange.ReadableStateChange(it))
|
||||
onNext(
|
||||
QuickActionChange.ReaderActiveStateChange(
|
||||
sessionManager.selectedSession?.readerMode ?: false
|
||||
)
|
||||
)
|
||||
quickActionSheetStore.apply {
|
||||
dispatch(QuickActionSheetAction.ReadableStateChange(available))
|
||||
dispatch(QuickActionSheetAction.ReaderActiveStateChange(
|
||||
sessionManager.selectedSession?.readerMode ?: false
|
||||
))
|
||||
}
|
||||
},
|
||||
owner = this,
|
||||
|
@ -411,6 +393,42 @@ class BrowserFragment : Fragment(), BackHandler {
|
|||
toolbarComponent.getView().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
|
||||
)
|
||||
)
|
||||
|
||||
quickActionSheetStore.observe(view) {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
whenStarted {
|
||||
quickActionSheetView.update(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun themeReaderViewControlsForPrivateMode(view: View) = with(view) {
|
||||
|
@ -495,115 +513,46 @@ class BrowserFragment : Fragment(), BackHandler {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAutoDisposeObservable<QuickActionAction>()
|
||||
.subscribe {
|
||||
when (it) {
|
||||
is QuickActionAction.Opened -> {
|
||||
requireComponents.analytics.metrics.track(Event.QuickActionSheetOpened)
|
||||
}
|
||||
is QuickActionAction.Closed -> {
|
||||
requireComponents.analytics.metrics.track(Event.QuickActionSheetClosed)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assignSitePermissionsRules()
|
||||
}
|
||||
|
||||
private fun bookmarkTapped() {
|
||||
getSessionById()?.let { session ->
|
||||
lifecycleScope.launch(IO) {
|
||||
val bookmarksStorage = requireComponents.core.bookmarksStorage
|
||||
val existing = bookmarksStorage.getBookmarksWithUrl(session.url)
|
||||
val found = existing.isNotEmpty() && existing[0].url == session.url
|
||||
if (found) {
|
||||
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
|
||||
private suspend fun bookmarkTapped(session: Session) = withContext(IO) {
|
||||
val bookmarksStorage = requireComponents.core.bookmarksStorage
|
||||
val existing = bookmarksStorage.getBookmarksWithUrl(session.url).firstOrNull { it.url == session.url }
|
||||
if (existing != null) {
|
||||
// Bookmark exists, go to edit fragment
|
||||
withContext(Main) {
|
||||
nav(
|
||||
R.id.browserFragment,
|
||||
BrowserFragmentDirections.actionBrowserFragmentToBookmarkEditFragment(existing.guid)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Save bookmark, then go to edit fragment
|
||||
val guid = bookmarksStorage.addItem(
|
||||
BookmarkRoot.Mobile.id,
|
||||
url = session.url,
|
||||
title = session.title,
|
||||
position = null
|
||||
)
|
||||
|
||||
withContext(Main) {
|
||||
quickActionSheetStore.dispatch(
|
||||
QuickActionSheetAction.BookmarkedStateChange(bookmarked = 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)
|
||||
)
|
||||
.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) {
|
||||
if (!loading) {
|
||||
updateBookmarkState(session)
|
||||
getManagedEmitter<QuickActionChange>().onNext(QuickActionChange.BounceNeededChange)
|
||||
quickActionSheetStore.dispatch(QuickActionSheetAction.BounceNeededChange)
|
||||
}
|
||||
|
||||
super.onLoadingStateChanged(session, loading)
|
||||
|
@ -923,12 +872,11 @@ class BrowserFragment : Fragment(), BackHandler {
|
|||
}
|
||||
|
||||
private fun updateBookmarkState(session: Session) {
|
||||
if (findBookmarkJob?.isActive == true) findBookmarkJob?.cancel()
|
||||
findBookmarkJob?.cancel()
|
||||
findBookmarkJob = lifecycleScope.launch(IO) {
|
||||
val found = findBookmarkedURL(session)
|
||||
launch(Main) {
|
||||
getManagedEmitter<QuickActionChange>()
|
||||
.onNext(QuickActionChange.BookmarkedStateChange(found))
|
||||
withContext(Main) {
|
||||
quickActionSheetStore.dispatch(QuickActionSheetAction.BookmarkedStateChange(found))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -936,8 +884,7 @@ class BrowserFragment : Fragment(), BackHandler {
|
|||
private fun updateAppLinksState(session: Session) {
|
||||
val url = session.url
|
||||
val appLinks = requireComponents.useCases.appLinksUseCases.appLinkRedirect
|
||||
getManagedEmitter<QuickActionChange>()
|
||||
.onNext(QuickActionChange.AppLinkStateChange(appLinks.invoke(url).hasExternalApp()))
|
||||
quickActionSheetStore.dispatch(QuickActionSheetAction.AppLinkStateChange(appLinks.invoke(url).hasExternalApp()))
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.widget.LinearLayout
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import mozilla.components.browser.toolbar.BrowserToolbar
|
||||
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.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
const val POSITION_SNAP_BUFFER = 1f
|
||||
|
||||
|
@ -33,21 +31,18 @@ class QuickActionSheet @JvmOverloads constructor(
|
|||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0,
|
||||
defStyleRes: Int = 0
|
||||
) : LinearLayout(context, attrs, defStyle, defStyleRes), CoroutineScope {
|
||||
) : LinearLayout(context, attrs, defStyle, defStyleRes) {
|
||||
|
||||
private lateinit var job: Job
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Main + job
|
||||
private val scope = MainScope()
|
||||
|
||||
private lateinit var quickActionSheetBehavior: QuickActionSheetBehavior
|
||||
|
||||
init {
|
||||
inflate(getContext(), R.layout.layout_quick_action_sheet, this)
|
||||
inflate(context, R.layout.layout_quick_action_sheet, this)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
job = Job()
|
||||
quickActionSheetBehavior = BottomSheetBehavior.from(quick_action_sheet.parent as View)
|
||||
as QuickActionSheetBehavior
|
||||
quickActionSheetBehavior.isHideable = false
|
||||
|
@ -56,7 +51,7 @@ class QuickActionSheet @JvmOverloads constructor(
|
|||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
job.cancel()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
private fun setupHandle() {
|
||||
|
@ -71,13 +66,13 @@ class QuickActionSheet @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
fun bounceSheet() {
|
||||
launch(Main) {
|
||||
Settings.getInstance(context).incrementAutomaticBounceQuickActionSheetCount()
|
||||
scope.launch(Dispatchers.Main) {
|
||||
delay(BOUNCE_ANIMATION_DELAY_LENGTH)
|
||||
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
delay(BOUNCE_ANIMATION_PAUSE_LENGTH)
|
||||
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
Settings.getInstance(context).incrementAutomaticBounceQuickActionSheetCount()
|
||||
}
|
||||
|
||||
class HandleAccessibilityDelegate(
|
||||
|
@ -98,17 +93,16 @@ class QuickActionSheet @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
|
||||
when (action) {
|
||||
AccessibilityNodeInfo.ACTION_CLICK -> {
|
||||
finalState = when (quickActionSheetBehavior.state) {
|
||||
finalState = when (action) {
|
||||
AccessibilityNodeInfo.ACTION_CLICK ->
|
||||
when (quickActionSheetBehavior.state) {
|
||||
BottomSheetBehavior.STATE_EXPANDED -> BottomSheetBehavior.STATE_COLLAPSED
|
||||
else -> BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
AccessibilityNodeInfo.ACTION_COLLAPSE ->
|
||||
finalState = BottomSheetBehavior.STATE_COLLAPSED
|
||||
BottomSheetBehavior.STATE_COLLAPSED
|
||||
AccessibilityNodeInfo.ACTION_EXPAND ->
|
||||
finalState = BottomSheetBehavior.STATE_EXPANDED
|
||||
BottomSheetBehavior.STATE_EXPANDED
|
||||
else -> return super.performAccessibilityAction(host, action, args)
|
||||
}
|
||||
|
||||
|
@ -133,7 +127,6 @@ class QuickActionSheet @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("unused") // Referenced from XML
|
||||
class QuickActionSheetBehavior(
|
||||
context: Context,
|
||||
attrs: AttributeSet
|
||||
|
@ -168,4 +161,9 @@ class QuickActionSheetBehavior(
|
|||
}
|
||||
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" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/quick_action_read_appearance"
|
||||
android:id="@+id/quick_action_appearance"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
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…
Reference in New Issue