[fenix] Closes https://github.com/mozilla-mobile/fenix/issues/3986 & Closes https://github.com/mozilla-mobile/fenix/issues/3661: Migrate QuickActionSheet to LibState & add tests (https://github.com/mozilla-mobile/fenix/pull/4058)
* 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 feedbackpull/600/head
parent
85b152e817
commit
8cb4414365
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
@ -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