1
0
Fork 0

For #3633 - Refactors the search screen to use lib-state

* For #3633 - Adds SearchStore

* For #3633 - Refactors AwesomeBarUIView

* For #3633 - Refactors ToolbarUIView to use lib-state

* For #3633 - Fixes a couple of state bugs

* For #3633 - Moves all user interaction to SearchInteractor

* For #3633 - Adds kdocs to SearchStore and SearchInteractor

* For #3633 - Adds documentation for the properties on SearchState
Also removes uneccessary property

* For #3633 - Creates `StateViewModel` to handle state restoration

* For #3633 - Adds a test for onTextChanged

* For #3633 - Adds tests for SearchInteractor

* For #3633 - Fixes bugs and adds documentation
master
Jeff Boek 2019-07-12 16:32:00 -07:00 committed by GitHub
parent 6b639b1a32
commit e4ff70c542
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 808 additions and 543 deletions

View File

@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.get
import mozilla.components.lib.state.State
/**
* Generic ViewModel to wrap a State object for state restoration
*/
@Suppress("UNCHECKED_CAST")
class StateViewModel<T : State>(initialState: T) : ViewModel() {
var state: T = initialState
private set(value) { field = value }
fun update(state: T) { this.state = state }
companion object {
fun <S : State> get(fragment: Fragment, initialState: S): StateViewModel<S> {
val factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return StateViewModel(initialState) as T
}
}
return ViewModelProviders.of(fragment, factory).get()
}
}
}

View File

@ -17,11 +17,13 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import mozilla.components.browser.search.SearchEngineManager
import mozilla.components.support.base.log.Log import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.Log.Priority.WARN import mozilla.components.support.base.log.Log.Priority.WARN
import org.mozilla.fenix.FenixApplication import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.MetricController
/** /**
* Get the BrowserApplication object from a context. * Get the BrowserApplication object from a context.
@ -35,6 +37,18 @@ val Context.application: FenixApplication
val Context.components: Components val Context.components: Components
get() = application.components get() = application.components
/**
* Helper function to get the MetricController off of context.
*/
val Context.metrics: MetricController
get() = this.components.analytics.metrics
/**
* Helper function to get the SearchEngineManager off of context.
*/
val Context.searchEngineManager: SearchEngineManager
get() = this.components.search.searchEngineManager
fun Context.asActivity() = (this as? ContextThemeWrapper)?.baseContext as? Activity fun Context.asActivity() = (this as? ContextThemeWrapper)?.baseContext as? Activity
?: this as? Activity ?: this as? Activity

View File

@ -9,6 +9,7 @@ import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.graphics.Typeface.BOLD import android.graphics.Typeface.BOLD
import android.graphics.Typeface.ITALIC import android.graphics.Typeface.ITALIC
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle import android.os.Bundle
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.view.LayoutInflater import android.view.LayoutInflater
@ -16,59 +17,45 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.Navigation import androidx.navigation.fragment.findNavController
import kotlinx.android.synthetic.main.fragment_search.* import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.android.synthetic.main.fragment_search.view.* import kotlinx.android.synthetic.main.fragment_search.view.*
import mozilla.components.browser.search.SearchEngine import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.qr.QrFeature import mozilla.components.feature.qr.QrFeature
import mozilla.components.lib.state.Store
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.content.hasCamera import mozilla.components.support.ktx.android.content.hasCamera
import mozilla.components.support.ktx.android.content.isPermissionGranted import mozilla.components.support.ktx.android.content.isPermissionGranted
import mozilla.components.support.ktx.kotlin.isUrl import org.jetbrains.anko.backgroundDrawable
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.components.StateViewModel
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.toolbar.SearchAction
import org.mozilla.fenix.components.toolbar.SearchChange
import org.mozilla.fenix.components.toolbar.SearchState
import org.mozilla.fenix.components.toolbar.ToolbarComponent
import org.mozilla.fenix.components.toolbar.ToolbarUIView
import org.mozilla.fenix.components.toolbar.ToolbarViewModel
import org.mozilla.fenix.ext.getSpannable import org.mozilla.fenix.ext.getSpannable
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.search.awesomebar.AwesomeBarView
import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.search.toolbar.ToolbarView
import org.mozilla.fenix.mvi.getManagedEmitter import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.search.awesomebar.AwesomeBarAction
import org.mozilla.fenix.search.awesomebar.AwesomeBarChange
import org.mozilla.fenix.search.awesomebar.AwesomeBarComponent
import org.mozilla.fenix.search.awesomebar.AwesomeBarState
import org.mozilla.fenix.search.awesomebar.AwesomeBarUIView
import org.mozilla.fenix.search.awesomebar.AwesomeBarViewModel
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions", "LargeClass")
class SearchFragment : Fragment(), BackHandler { class SearchFragment : Fragment(), BackHandler {
private lateinit var toolbarComponent: ToolbarComponent private lateinit var toolbarView: ToolbarView
private lateinit var awesomeBarComponent: AwesomeBarComponent private lateinit var awesomeBarView: AwesomeBarView
private var sessionId: String? = null
private var isPrivate = false
private val qrFeature = ViewBoundFeatureWrapper<QrFeature>() private val qrFeature = ViewBoundFeatureWrapper<QrFeature>()
private var permissionDidUpdate = false private var permissionDidUpdate = false
private lateinit var searchStore: SearchStore
private lateinit var searchInteractor: SearchInteractor
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Disabled while awaiting a better solution to #3209
// postponeEnterTransition()
// sharedElementEnterTransition =
// TransitionInflater.from(context).inflateTransition(android.R.transition.move).setDuration(
// SHARED_TRANSITION_MS
// )
requireComponents.analytics.metrics.track(Event.InteractWithSearchURLArea) requireComponents.analytics.metrics.track(Event.InteractWithSearchURLArea)
} }
@ -77,43 +64,44 @@ class SearchFragment : Fragment(), BackHandler {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
sessionId = SearchFragmentArgs.fromBundle(arguments!!).sessionId val session = arguments
isPrivate = (activity as HomeActivity).browsingModeManager.isPrivate ?.let(SearchFragmentArgs.Companion::fromBundle)
?.let { it.sessionId }
?.let(requireComponents.core.sessionManager::findSessionById)
val session = sessionId?.let { requireComponents.core.sessionManager.findSessionById(it) }
val view = inflater.inflate(R.layout.fragment_search, container, false) val view = inflater.inflate(R.layout.fragment_search, container, false)
val url = session?.url ?: "" val url = session?.url ?: ""
toolbarComponent = ToolbarComponent( val viewModel = StateViewModel.get(
view.toolbar_component_wrapper, this,
ActionBusFactory.get(this), SearchState(
sessionId, query = url,
isPrivate, showShortcutEnginePicker = false,
true, searchEngineSource = SearchEngineSource.Default(
view.search_engine_icon, requireComponents.search.searchEngineManager.getDefaultSearchEngine(requireContext())
FenixViewModelProvider.create( ),
this, showSuggestions = Settings.getInstance(requireContext()).showSearchSuggestions,
ToolbarViewModel::class.java showVisitedSitesBookmarks = Settings.getInstance(requireContext()).shouldShowVisitedSitesBookmarks,
) { session = session
ToolbarViewModel(SearchState(url, session?.searchTerms ?: "", isEditing = true)) )
}
).also {
// Remove background from toolbar view since it conflicts with the search UI.
it.uiView.view.background = null
it.uiView.view.layoutParams.height = CoordinatorLayout.LayoutParams.MATCH_PARENT
}
awesomeBarComponent = AwesomeBarComponent(
view.search_layout,
ActionBusFactory.get(this),
FenixViewModelProvider.create(
this,
AwesomeBarViewModel::class.java
) {
AwesomeBarViewModel(AwesomeBarState("", false))
}
) )
ActionBusFactory.get(this).logMergedObservables()
searchStore = Store(
viewModel.state,
::searchStateReducer
)
searchStore.observe(this) { viewModel.update(it) }
searchInteractor = SearchInteractor(
activity as HomeActivity,
findNavController(),
searchStore
)
toolbarView = ToolbarView(view.toolbar_component_wrapper, searchInteractor, historyStorageProvider())
awesomeBarView = AwesomeBarView(view.search_layout, searchInteractor)
return view return view
} }
@ -151,7 +139,7 @@ class SearchFragment : Fragment(), BackHandler {
(activity as HomeActivity) (activity as HomeActivity)
.openToBrowserAndLoad( .openToBrowserAndLoad(
searchTermOrURL = result, searchTermOrURL = result,
newTab = sessionId == null, newTab = searchStore.state.session == null,
from = BrowserDirection.FromSearch from = BrowserDirection.FromSearch
) )
dialog.dismiss() dialog.dismiss()
@ -166,19 +154,16 @@ class SearchFragment : Fragment(), BackHandler {
) )
view.search_scan_button.setOnClickListener { view.search_scan_button.setOnClickListener {
getManagedEmitter<SearchChange>().onNext(SearchChange.ToolbarClearedFocus) toolbarView.view.clearFocus()
requireComponents.analytics.metrics.track(Event.QRScannerOpened) requireComponents.analytics.metrics.track(Event.QRScannerOpened)
qrFeature.get()?.scan(R.id.container) qrFeature.get()?.scan(R.id.container)
} }
lifecycle.addObserver((toolbarComponent.uiView as ToolbarUIView).toolbarIntegration)
view.toolbar_wrapper.clipToOutline = false view.toolbar_wrapper.clipToOutline = false
search_shortcuts_button.setOnClickListener { search_shortcuts_button.setOnClickListener {
val isOpen = (awesomeBarComponent.uiView as AwesomeBarUIView).state?.showShortcutEnginePicker ?: false val isOpen = searchStore.state.showShortcutEnginePicker
searchStore.dispatch(SearchAction.SearchShortcutEnginePicker(!isOpen))
getManagedEmitter<AwesomeBarChange>().onNext(AwesomeBarChange.SearchShortcutEnginePicker(!isOpen))
if (isOpen) { if (isOpen) {
requireComponents.analytics.metrics.track(Event.SearchShortcutMenuClosed) requireComponents.analytics.metrics.track(Event.SearchShortcutMenuClosed)
@ -187,124 +172,72 @@ class SearchFragment : Fragment(), BackHandler {
} }
} }
searchStore.observe(view) {
MainScope().launch {
awesomeBarView.update(it)
toolbarView.update(it)
updateSearchEngineIcon(it)
updateSearchShortuctsIcon(it)
updateSearchWithLabel(it)
}
}
startPostponedEnterTransition() startPostponedEnterTransition()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
subscribeToSearchActions()
subscribeToAwesomeBarActions()
if (!permissionDidUpdate) { if (!permissionDidUpdate) {
getManagedEmitter<SearchChange>().onNext(SearchChange.ToolbarRequestedFocus) toolbarView.view.requestFocus()
} }
permissionDidUpdate = false permissionDidUpdate = false
(activity as AppCompatActivity).supportActionBar?.hide() (activity as AppCompatActivity).supportActionBar?.hide()
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
getManagedEmitter<SearchChange>().onNext(SearchChange.ToolbarClearedFocus) toolbarView.view.clearFocus()
} }
override fun onBackPressed(): Boolean { override fun onBackPressed(): Boolean {
return when { return when {
qrFeature.onBackPressed() -> { qrFeature.onBackPressed() -> {
view?.search_scan_button?.isChecked = false view?.search_scan_button?.isChecked = false
getManagedEmitter<SearchChange>().onNext(SearchChange.ToolbarRequestedFocus) toolbarView.view.requestFocus()
true true
} }
else -> false else -> false
} }
} }
private fun subscribeToSearchActions() { private fun updateSearchEngineIcon(searchState: SearchState) {
getAutoDisposeObservable<SearchAction>() val searchIcon = searchState.searchEngineSource.searchEngine.icon
.subscribe { val draw = BitmapDrawable(resources, searchIcon)
when (it) { val iconSize = resources.getDimension(R.dimen.preference_icon_drawable_size).toInt()
is SearchAction.UrlCommitted -> { draw.setBounds(0, 0, iconSize, iconSize)
if (it.url.isNotBlank()) { search_engine_icon?.backgroundDrawable = draw
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = it.url,
newTab = sessionId == null,
from = BrowserDirection.FromSearch,
engine = it.engine
)
val event = if (it.url.isUrl()) {
Event.EnteredUrl(false)
} else {
val engine = it.engine ?: requireComponents
.search.searchEngineManager.getDefaultSearchEngine(requireContext())
createSearchEvent(engine, false)
}
requireComponents.analytics.metrics.track(event)
}
}
is SearchAction.TextChanged -> {
getManagedEmitter<SearchChange>().onNext(SearchChange.QueryTextChanged(it.query))
getManagedEmitter<AwesomeBarChange>().onNext(AwesomeBarChange.UpdateQuery(it.query))
}
is SearchAction.EditingCanceled -> {
Navigation.findNavController(toolbar_wrapper).navigateUp()
}
}
}
} }
private fun subscribeToAwesomeBarActions() { private fun updateSearchWithLabel(searchState: SearchState) {
getAutoDisposeObservable<AwesomeBarAction>() search_with_shortcuts.visibility = if (searchState.showShortcutEnginePicker) View.VISIBLE else View.GONE
.subscribe {
when (it) {
is AwesomeBarAction.URLTapped -> {
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = it.url,
newTab = sessionId == null,
from = BrowserDirection.FromSearch
)
requireComponents.analytics.metrics.track(Event.EnteredUrl(false))
}
is AwesomeBarAction.SearchTermsTapped -> {
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = it.searchTerms,
newTab = sessionId == null,
from = BrowserDirection.FromSearch,
engine = it.engine,
forceSearch = true
)
val engine = it.engine ?: requireComponents
.search.searchEngineManager.getDefaultSearchEngine(requireContext())
val event = createSearchEvent(engine, true)
requireComponents.analytics.metrics.track(event)
}
is AwesomeBarAction.SearchShortcutEngineSelected -> {
getManagedEmitter<AwesomeBarChange>()
.onNext(AwesomeBarChange.SearchShortcutEngineSelected(it.engine))
getManagedEmitter<SearchChange>()
.onNext(SearchChange.SearchShortcutEngineSelected(it.engine))
requireComponents.analytics.metrics.track(Event.SearchShortcutSelected(it.engine.name))
}
}
}
} }
private fun createSearchEvent(engine: SearchEngine, isSuggestion: Boolean): Event.PerformedSearch { private fun updateSearchShortuctsIcon(searchState: SearchState) {
val isShortcut = engine != requireComponents.search.searchEngineManager.defaultSearchEngine with(requireContext()) {
val showShortcuts = searchState.showShortcutEnginePicker
search_shortcuts_button?.isChecked = showShortcuts
val engineSource = val color = if (showShortcuts) R.attr.foundation else R.attr.primaryText
if (isShortcut) Event.PerformedSearch.EngineSource.Shortcut(engine)
else Event.PerformedSearch.EngineSource.Default(engine)
val source = search_shortcuts_button.compoundDrawables[0]?.setTint(
if (isSuggestion) Event.PerformedSearch.EventSource.Suggestion(engineSource) ContextCompat.getColor(
else Event.PerformedSearch.EventSource.Action(engineSource) this,
ThemeManager.resolveAttribute(color, this)
return Event.PerformedSearch(source) )
)
}
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
@ -324,8 +257,13 @@ class SearchFragment : Fragment(), BackHandler {
} }
} }
private fun historyStorageProvider(): HistoryStorage? {
return if (Settings.getInstance(requireContext()).shouldShowVisitedSitesBookmarks) {
requireComponents.core.historyStorage
} else null
}
companion object { companion object {
private const val SHARED_TRANSITION_MS = 150L
private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1 private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1
} }
} }

View File

@ -0,0 +1,101 @@
/* 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.search
import android.content.Context
import androidx.navigation.NavController
import mozilla.components.browser.search.SearchEngine
import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.searchEngineManager
import org.mozilla.fenix.search.awesomebar.AwesomeBarInteractor
import org.mozilla.fenix.search.toolbar.ToolbarInteractor
/**
* Interactor for the search screen
* Provides implementations for the AwesomeBarView and ToolbarView
*/
class SearchInteractor(
private val context: Context,
private val navController: NavController,
private val store: SearchStore
) : AwesomeBarInteractor, ToolbarInteractor {
override fun onUrlCommitted(url: String) {
if (url.isNotBlank()) {
(context as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = url,
newTab = store.state.session == null,
from = BrowserDirection.FromSearch,
engine = store.state.searchEngineSource.searchEngine
)
val event = if (url.isUrl()) {
Event.EnteredUrl(false)
} else {
createSearchEvent(store.state.searchEngineSource.searchEngine, false)
}
context.metrics.track(event)
}
}
override fun onEditingCanceled() {
navController.navigateUp()
}
override fun onTextChanged(text: String) {
store.dispatch(SearchAction.UpdateQuery(text))
}
override fun onUrlTapped(url: String) {
(context as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = url,
newTab = store.state.session == null,
from = BrowserDirection.FromSearch
)
context.metrics.track(Event.EnteredUrl(false))
}
override fun onSearchTermsTapped(searchTerms: String) {
(context as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = searchTerms,
newTab = store.state.session == null,
from = BrowserDirection.FromSearch,
engine = store.state.searchEngineSource.searchEngine,
forceSearch = true
)
val event = createSearchEvent(store.state.searchEngineSource.searchEngine, true)
context.metrics.track(event)
}
override fun onSearchShortcutEngineSelected(searchEngine: SearchEngine) {
store.dispatch(SearchAction.SearchShortcutEngineSelected(searchEngine))
context.metrics.track(Event.SearchShortcutSelected(searchEngine.name))
}
override fun onClickSearchEngineSettings() {
val directions = SearchFragmentDirections.actionSearchFragmentToSearchEngineFragment()
navController.navigate(directions)
}
private fun createSearchEvent(engine: SearchEngine, isSuggestion: Boolean): Event.PerformedSearch {
val isShortcut = engine != context.searchEngineManager.defaultSearchEngine
val engineSource =
if (isShortcut) Event.PerformedSearch.EngineSource.Shortcut(engine)
else Event.PerformedSearch.EngineSource.Default(engine)
val source =
if (isSuggestion) Event.PerformedSearch.EventSource.Suggestion(engineSource)
else Event.PerformedSearch.EventSource.Action(engineSource)
return Event.PerformedSearch(source)
}
}

View File

@ -0,0 +1,70 @@
/* 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.search
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
/**
* An alias to make it easier to work with `Store<SearchState, SearchAction>`
*/
typealias SearchStore = Store<SearchState, SearchAction>
/**
* Wraps a `SearchEngine` to give consumers the context that it was selected as a shortcut
*/
sealed class SearchEngineSource {
abstract val searchEngine: SearchEngine
data class Default(override val searchEngine: SearchEngine) : SearchEngineSource()
data class Shortcut(override val searchEngine: SearchEngine) : SearchEngineSource()
}
/**
* The state for the Search Screen
* @property query The current search query string
* @property showShortcutEnginePicker Whether or not to show the available search engine view
* @property searchEngineSource The current selected search engine with the context of how it was selected
* @property showSuggestions Whether or not to show search suggestions for the selected search engine in the AwesomeBar
* @property showVisitedSitesBookmarks Whether or not to show history and bookmark suggestions in the AwesomeBar
* @property session The current session if available
*/
data class SearchState(
val query: String,
val showShortcutEnginePicker: Boolean,
val searchEngineSource: SearchEngineSource,
val showSuggestions: Boolean,
val showVisitedSitesBookmarks: Boolean,
val session: Session?
) : State
/**
* Actions to dispatch through the `SearchStore` to modify `SearchState` through the reducer.
*/
sealed class SearchAction : Action {
data class SearchShortcutEngineSelected(val engine: SearchEngine) : SearchAction()
data class SearchShortcutEnginePicker(val show: Boolean) : SearchAction()
data class UpdateQuery(val query: String) : SearchAction()
}
/**
* The SearchState Reducer.
*/
fun searchStateReducer(state: SearchState, action: SearchAction): SearchState {
return when (action) {
is SearchAction.SearchShortcutEngineSelected ->
state.copy(
searchEngineSource = SearchEngineSource.Shortcut(action.engine),
showShortcutEnginePicker = false
)
is SearchAction.SearchShortcutEnginePicker ->
state.copy(showShortcutEnginePicker = action.show)
is SearchAction.UpdateQuery ->
state.copy(query = action.query)
}
}

View File

@ -1,66 +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.search.awesomebar
import android.view.ViewGroup
import mozilla.components.browser.search.SearchEngine
import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModelBase
import org.mozilla.fenix.mvi.UIComponentViewModelProvider
data class AwesomeBarState(
val query: String,
val showShortcutEnginePicker: Boolean,
val suggestionEngine: SearchEngine? = null
) : ViewState
sealed class AwesomeBarAction : Action {
data class URLTapped(val url: String) : AwesomeBarAction()
data class SearchTermsTapped(val searchTerms: String, val engine: SearchEngine? = null) : AwesomeBarAction()
data class SearchShortcutEngineSelected(val engine: SearchEngine) : AwesomeBarAction()
}
sealed class AwesomeBarChange : Change {
data class SearchShortcutEngineSelected(val engine: SearchEngine) : AwesomeBarChange()
data class SearchShortcutEnginePicker(val show: Boolean) : AwesomeBarChange()
data class UpdateQuery(val query: String) : AwesomeBarChange()
}
class AwesomeBarComponent(
private val container: ViewGroup,
bus: ActionBusFactory,
viewModelProvider: UIComponentViewModelProvider<AwesomeBarState, AwesomeBarChange>
) : UIComponent<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(
bus.getManagedEmitter(AwesomeBarAction::class.java),
bus.getSafeManagedObservable(AwesomeBarChange::class.java),
viewModelProvider
) {
override fun initView() = AwesomeBarUIView(container, actionEmitter, changesObservable)
init {
bind()
}
}
class AwesomeBarViewModel(
initialState: AwesomeBarState
) : UIComponentViewModelBase<AwesomeBarState, AwesomeBarChange>(initialState, reducer) {
companion object {
val reducer: Reducer<AwesomeBarState, AwesomeBarChange> = { state, change ->
when (change) {
is AwesomeBarChange.SearchShortcutEngineSelected ->
state.copy(suggestionEngine = change.engine, showShortcutEnginePicker = false)
is AwesomeBarChange.SearchShortcutEnginePicker ->
state.copy(showShortcutEnginePicker = change.show)
is AwesomeBarChange.UpdateQuery -> state.copy(query = change.query)
}
}
}
}

View File

@ -1,219 +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.search.awesomebar
import android.graphics.PorterDuff.Mode.SRC_IN
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer
import mozilla.components.browser.awesomebar.BrowserAwesomeBar
import mozilla.components.browser.search.SearchEngine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.feature.awesomebar.provider.BookmarksStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider
import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider
import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
import mozilla.components.feature.search.SearchUseCases
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.utils.Settings
class AwesomeBarUIView(
private val container: ViewGroup,
actionEmitter: Observer<AwesomeBarAction>,
changesObservable: Observable<AwesomeBarChange>
) :
UIView<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(
container,
actionEmitter,
changesObservable
) {
override val view: BrowserAwesomeBar = LayoutInflater.from(container.context)
.inflate(R.layout.component_awesomebar, container, true)
.findViewById(R.id.awesomeBar)
var state: AwesomeBarState? = null
private set
private var clipboardSuggestionProvider: ClipboardSuggestionProvider? = null
private var sessionProvider: SessionSuggestionProvider? = null
private var historyStorageProvider: HistoryStorageSuggestionProvider? = null
private var shortcutsEnginePickerProvider: ShortcutsSuggestionProvider? = null
private var bookmarksStorageSuggestionProvider: BookmarksStorageSuggestionProvider? = null
private val searchSuggestionProvider: SearchSuggestionProvider?
get() = searchSuggestionFromShortcutProvider ?: defaultSearchSuggestionProvider!!
private var defaultSearchSuggestionProvider: SearchSuggestionProvider? = null
private var searchSuggestionFromShortcutProvider: SearchSuggestionProvider? = null
private val shortcutEngineManager by lazy {
ShortcutEngineManager(
this,
actionEmitter,
::setShortcutEngine,
::showSuggestionProviders,
::showSearchSuggestionProvider
)
}
private val loadUrlUseCase = object : SessionUseCases.LoadUrlUseCase {
override fun invoke(url: String, flags: EngineSession.LoadUrlFlags) {
actionEmitter.onNext(AwesomeBarAction.URLTapped(url))
}
}
private val searchUseCase = object : SearchUseCases.SearchUseCase {
override fun invoke(searchTerms: String, searchEngine: SearchEngine?) {
actionEmitter.onNext(AwesomeBarAction.SearchTermsTapped(searchTerms, searchEngine))
}
}
private val shortcutSearchUseCase = object : SearchUseCases.SearchUseCase {
override fun invoke(searchTerms: String, searchEngine: SearchEngine?) {
actionEmitter.onNext(
AwesomeBarAction.SearchTermsTapped(
searchTerms,
state?.suggestionEngine
)
)
}
}
init {
with(container.context) {
val primaryTextColor = ContextCompat.getColor(
this,
ThemeManager.resolveAttribute(R.attr.primaryText, this)
)
val draw = getDrawable(R.drawable.ic_link)
draw?.setColorFilter(primaryTextColor, SRC_IN)
clipboardSuggestionProvider = ClipboardSuggestionProvider(
this,
loadUrlUseCase,
draw!!.toBitmap(),
getString(R.string.awesomebar_clipboard_title)
)
sessionProvider =
SessionSuggestionProvider(
components.core.sessionManager,
components.useCases.tabsUseCases.selectTab,
components.core.icons
)
historyStorageProvider =
HistoryStorageSuggestionProvider(
components.core.historyStorage,
loadUrlUseCase,
components.core.icons
)
bookmarksStorageSuggestionProvider =
BookmarksStorageSuggestionProvider(
components.core.bookmarksStorage,
loadUrlUseCase,
components.core.icons
)
if (Settings.getInstance(container.context).showSearchSuggestions) {
val searchDrawable = getDrawable(R.drawable.ic_search)
searchDrawable?.setColorFilter(primaryTextColor, SRC_IN)
defaultSearchSuggestionProvider =
SearchSuggestionProvider(
searchEngine = components.search.searchEngineManager.getDefaultSearchEngine(
this
),
searchUseCase = searchUseCase,
fetchClient = components.core.client,
mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
limit = 3,
icon = searchDrawable?.toBitmap()
)
}
shortcutsEnginePickerProvider =
ShortcutsSuggestionProvider(
components.search.searchEngineManager,
this,
shortcutEngineManager::selectShortcutEngine,
shortcutEngineManager::selectShortcutEngineSettings
)
shortcutEngineManager.shortcutsEnginePickerProvider = shortcutsEnginePickerProvider
val listener = object : RecyclerView.OnFlingListener() {
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
view.hideKeyboard()
return false
}
}
view.onFlingListener = listener
}
}
private fun showSuggestionProviders() {
if (Settings.getInstance(container.context).showSearchSuggestions) {
view.addProviders(searchSuggestionProvider!!)
}
if (Settings.getInstance(container.context).shouldShowVisitedSitesBookmarks) {
view.addProviders(bookmarksStorageSuggestionProvider!!)
view.addProviders(historyStorageProvider!!)
}
view.addProviders(
clipboardSuggestionProvider!!,
sessionProvider!!
)
}
private fun showSearchSuggestionProvider() {
if (Settings.getInstance(container.context).showSearchSuggestions) {
view.addProviders(searchSuggestionProvider!!)
}
}
private fun setShortcutEngine(engine: SearchEngine) {
with(container.context) {
val draw = getDrawable(R.drawable.ic_search)
draw?.setColorFilter(
ContextCompat.getColor(
this,
ThemeManager.resolveAttribute(R.attr.primaryText, this)
), SRC_IN
)
searchSuggestionFromShortcutProvider =
SearchSuggestionProvider(
components.search.searchEngineManager.getDefaultSearchEngine(this, engine.name),
shortcutSearchUseCase,
components.core.client,
mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
icon = draw?.toBitmap()
)
}
}
override fun updateView() = Consumer<AwesomeBarState> {
shortcutEngineManager.updateSelectedEngineIfNecessary(it)
shortcutEngineManager.updateEnginePickerVisibilityIfNecessary(it)
view.onInputChanged(it.query)
state = it
}
}

View File

@ -0,0 +1,207 @@
package org.mozilla.fenix.search.awesomebar
/* 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/. */
import android.graphics.PorterDuff.Mode.SRC_IN
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import kotlinx.android.extensions.LayoutContainer
import mozilla.components.browser.awesomebar.BrowserAwesomeBar
import mozilla.components.browser.search.SearchEngine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider
import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider
import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.BookmarksStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
import mozilla.components.feature.search.SearchUseCases
import mozilla.components.feature.session.SessionUseCases
import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.search.SearchEngineSource
import org.mozilla.fenix.search.SearchState
/**
* Interface for the AwesomeBarView Interactor. This interface is implemented by objects that want
* to respond to user interaction on the AwesomebarView
*/
interface AwesomeBarInteractor {
/**
* Called whenever a suggestion containing a URL is tapped
* @param url the url the suggestion was providing
*/
fun onUrlTapped(url: String)
/**
* Called whenever a search engine suggestion is tapped
* @param searchTerms the query contained by the search suggestion
*/
fun onSearchTermsTapped(searchTerms: String)
/**
* Called whenever a search engine shortcut is tapped
* @param searchEngine the searchEngine that was selected
*/
fun onSearchShortcutEngineSelected(searchEngine: SearchEngine)
/**
* Called whenever the "Search Engine Settings" item is tapped
*/
fun onClickSearchEngineSettings()
}
/**
* View that contains and configures the BrowserAwesomeBar
*/
class AwesomeBarView(
private val container: ViewGroup,
val interactor: AwesomeBarInteractor
) : LayoutContainer {
val view: BrowserAwesomeBar = LayoutInflater.from(container.context)
.inflate(R.layout.component_awesomebar, container, true)
.findViewById(R.id.awesomeBar)
override val containerView: View?
get() = container
private val clipboardSuggestionProvider: ClipboardSuggestionProvider
private val sessionProvider: SessionSuggestionProvider
private val historyStorageProvider: HistoryStorageSuggestionProvider
private val shortcutsEnginePickerProvider: ShortcutsSuggestionProvider
private val bookmarksStorageSuggestionProvider: BookmarksStorageSuggestionProvider
private val defaultSearchSuggestionProvider: SearchSuggestionProvider
private val loadUrlUseCase = object : SessionUseCases.LoadUrlUseCase {
override fun invoke(url: String, flags: EngineSession.LoadUrlFlags) {
interactor.onUrlTapped(url)
}
}
private val searchUseCase = object : SearchUseCases.SearchUseCase {
override fun invoke(searchTerms: String, searchEngine: SearchEngine?) {
interactor.onSearchTermsTapped(searchTerms)
}
}
private val shortcutSearchUseCase = object : SearchUseCases.SearchUseCase {
override fun invoke(searchTerms: String, searchEngine: SearchEngine?) {
interactor.onSearchTermsTapped(searchTerms)
}
}
init {
with(container.context) {
val primaryTextColor = ContextCompat.getColor(
this,
ThemeManager.resolveAttribute(R.attr.primaryText, this)
)
val draw = getDrawable(R.drawable.ic_link)!!
draw.setColorFilter(primaryTextColor, SRC_IN)
clipboardSuggestionProvider = ClipboardSuggestionProvider(
this,
loadUrlUseCase,
draw.toBitmap(),
getString(R.string.awesomebar_clipboard_title)
)
sessionProvider =
SessionSuggestionProvider(
components.core.sessionManager,
components.useCases.tabsUseCases.selectTab,
components.core.icons
)
historyStorageProvider =
HistoryStorageSuggestionProvider(
components.core.historyStorage,
loadUrlUseCase,
components.core.icons
)
bookmarksStorageSuggestionProvider =
BookmarksStorageSuggestionProvider(
components.core.bookmarksStorage,
loadUrlUseCase,
components.core.icons
)
val searchDrawable = getDrawable(R.drawable.ic_search)!!
searchDrawable.setColorFilter(primaryTextColor, SRC_IN)
defaultSearchSuggestionProvider =
SearchSuggestionProvider(
searchEngine = components.search.searchEngineManager.getDefaultSearchEngine(
this
),
searchUseCase = searchUseCase,
fetchClient = components.core.client,
mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
limit = 3,
icon = searchDrawable.toBitmap()
)
shortcutsEnginePickerProvider =
ShortcutsSuggestionProvider(
components.search.searchEngineManager,
this,
interactor::onSearchShortcutEngineSelected,
interactor::onClickSearchEngineSettings
)
}
}
fun update(state: SearchState) {
view.removeAllProviders()
if (state.showShortcutEnginePicker) {
view.addProviders(shortcutsEnginePickerProvider)
} else {
if (state.showSuggestions) {
view.addProviders(when (state.searchEngineSource) {
is SearchEngineSource.Default -> defaultSearchSuggestionProvider
is SearchEngineSource.Shortcut -> createSuggestionProviderForEngine(
state.searchEngineSource.searchEngine
)
})
}
if (state.showVisitedSitesBookmarks) {
view.addProviders(bookmarksStorageSuggestionProvider, historyStorageProvider)
}
view.addProviders(clipboardSuggestionProvider, sessionProvider)
}
view.onInputChanged(state.query)
}
private fun createSuggestionProviderForEngine(engine: SearchEngine): SearchSuggestionProvider {
return with(container.context) {
val draw = getDrawable(R.drawable.ic_search)
draw?.setColorFilter(
ContextCompat.getColor(
this,
ThemeManager.resolveAttribute(R.attr.primaryText, this)
), SRC_IN
)
SearchSuggestionProvider(
components.search.searchEngineManager.getDefaultSearchEngine(this, engine.name),
shortcutSearchUseCase,
components.core.client,
limit = 3,
mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
icon = draw?.toBitmap()
)
}
}
}

View File

@ -1,99 +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.search.awesomebar
import android.view.View
import androidx.core.content.ContextCompat
import androidx.navigation.Navigation
import io.reactivex.Observer
import kotlinx.android.synthetic.main.fragment_search.*
import mozilla.components.browser.search.SearchEngine
import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.search.SearchFragmentDirections
class ShortcutEngineManager(
private val awesomeBarUIView: AwesomeBarUIView,
private val actionEmitter: Observer<AwesomeBarAction>,
private val setShortcutEngine: (newEngine: SearchEngine) -> Unit,
private val showSuggestionProviders: () -> Unit,
private val showSearchSuggestionProvider: () -> Unit
) {
var shortcutsEnginePickerProvider: ShortcutsSuggestionProvider? = null
val context = awesomeBarUIView.containerView?.context!!
fun updateSelectedEngineIfNecessary(newState: AwesomeBarState) {
if (engineDidChange(newState)) {
newState.suggestionEngine?.let { newEngine ->
setShortcutEngine(newEngine)
}
}
}
fun updateEnginePickerVisibilityIfNecessary(newState: AwesomeBarState) {
if (shouldUpdateShortcutEnginePickerVisibility(newState)) {
if (newState.showShortcutEnginePicker) {
showShortcutEnginePicker()
updateSearchWithVisibility(true)
} else {
hideShortcutEnginePicker()
updateSearchWithVisibility(false)
newState.suggestionEngine?.also { showSearchSuggestionProvider() } ?: showSuggestionProviders()
}
}
}
fun selectShortcutEngine(engine: SearchEngine) {
actionEmitter.onNext(AwesomeBarAction.SearchShortcutEngineSelected(engine))
}
fun selectShortcutEngineSettings() {
val directions = SearchFragmentDirections.actionSearchFragmentToSearchEngineFragment()
Navigation.findNavController(awesomeBarUIView.view).navigate(directions)
}
private fun engineDidChange(newState: AwesomeBarState): Boolean {
return awesomeBarUIView.state?.suggestionEngine != newState.suggestionEngine
}
private fun shouldUpdateShortcutEnginePickerVisibility(newState: AwesomeBarState): Boolean {
return awesomeBarUIView.state?.showShortcutEnginePicker != newState.showShortcutEnginePicker
}
private fun showShortcutEnginePicker() {
with(context) {
awesomeBarUIView.search_shortcuts_button?.isChecked = true
awesomeBarUIView.search_shortcuts_button.compoundDrawables[0]?.setTint(
ContextCompat.getColor(
this,
ThemeManager.resolveAttribute(R.attr.foundation, this)
)
)
awesomeBarUIView.view.removeAllProviders()
awesomeBarUIView.view.addProviders(shortcutsEnginePickerProvider!!)
}
}
private fun hideShortcutEnginePicker() {
with(context) {
awesomeBarUIView.search_shortcuts_button?.isChecked = false
awesomeBarUIView.search_shortcuts_button.compoundDrawables[0]?.setTint(
ContextCompat.getColor(
this,
ThemeManager.resolveAttribute(R.attr.primaryText, this)
)
)
awesomeBarUIView.view.removeProviders(shortcutsEnginePickerProvider!!)
}
}
private fun updateSearchWithVisibility(visible: Boolean) {
awesomeBarUIView.search_with_shortcuts.visibility = if (visible) View.VISIBLE else View.GONE
}
}

View File

@ -0,0 +1,126 @@
/* 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.search.toolbar
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import kotlinx.android.extensions.LayoutContainer
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature
import mozilla.components.support.ktx.android.content.res.pxToDp
import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.search.SearchState
/**
* Interface for the Toolbar Interactor. This interface is implemented by objects that want
* to respond to user interaction on the ToolbarView
*/
interface ToolbarInteractor {
/**
* Called when a user hits the return key while ToolbarView has focus.
* @param url the text inside the ToolbarView when committed
*/
fun onUrlCommitted(url: String)
/**
* Called when a removes focus from the ToolbarView
*/
fun onEditingCanceled()
/**
* Called whenever the text inside the ToolbarView changes
* @param text the current text displayed by ToolbarView
*/
fun onTextChanged(text: String)
}
/**
* View that contains and configures the BrowserToolbar to only be used in its editing mode.
*/
class ToolbarView(
private val container: ViewGroup,
private val interactor: ToolbarInteractor,
private val historyStorage: HistoryStorage?
) : LayoutContainer {
override val containerView: View?
get() = container
val view: BrowserToolbar = LayoutInflater.from(container.context)
.inflate(R.layout.component_search, container, true)
.findViewById(R.id.toolbar)
private var isInitialized = false
init {
view.apply {
editMode()
elevation = resources.pxToDp(TOOLBAR_ELEVATION_IN_DP).toFloat()
setOnUrlCommitListener {
interactor.onUrlCommitted(it)
false
}
background = null
layoutParams.height = CoordinatorLayout.LayoutParams.MATCH_PARENT
hint = context.getString(R.string.search_hint)
textColor = ContextCompat.getColor(
container.context,
ThemeManager.resolveAttribute(R.attr.primaryText, container.context)
)
hintColor = ContextCompat.getColor(
container.context,
ThemeManager.resolveAttribute(R.attr.secondaryText, container.context)
)
suggestionBackgroundColor = ContextCompat.getColor(
container.context,
R.color.suggestion_highlight_color
)
setOnEditListener(object : mozilla.components.concept.toolbar.Toolbar.OnEditListener {
override fun onCancelEditing(): Boolean {
interactor.onEditingCanceled()
return false
}
override fun onTextChanged(text: String) {
url = text
this@ToolbarView.interactor.onTextChanged(text)
}
})
}
ToolbarAutocompleteFeature(view).apply {
addDomainProvider(ShippedDomainsProvider().also { it.initialize(view.context) })
historyStorage?.also(::addHistoryStorageProvider)
}
}
fun update(searchState: SearchState) {
if (!isInitialized) {
view.url = searchState.query
view.setSearchTerms(searchState.session?.searchTerms ?: "")
view.editMode()
isInitialized = true
}
}
companion object {
private const val TOOLBAR_ELEVATION_IN_DP = 16
}
}

View File

@ -0,0 +1,156 @@
/* 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.search
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.search.SearchEngineManager
import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.searchEngineManager
class SearchInteractorTest {
@Test
fun onUrlCommitted() {
val context: HomeActivity = mockk()
val store: SearchStore = mockk()
val state: SearchState = mockk()
val searchEngineManager: SearchEngineManager = mockk(relaxed = true)
val searchEngine = SearchEngineSource.Default(mockk())
every { context.metrics } returns mockk(relaxed = true)
every { context.searchEngineManager } returns searchEngineManager
every { context.openToBrowserAndLoad(any(), any(), any(), any(), any(), any()) } just Runs
every { store.state } returns state
every { state.session } returns null
every { state.searchEngineSource } returns searchEngine
val interactor = SearchInteractor(context, mockk(), store)
interactor.onUrlCommitted("test")
verify {
context.openToBrowserAndLoad(
searchTermOrURL = "test",
newTab = true,
from = BrowserDirection.FromSearch,
engine = searchEngine.searchEngine
)
}
}
@Test
fun onEditingCanceled() {
val navController: NavController = mockk(relaxed = true)
val interactor = SearchInteractor(mockk(), navController, mockk())
interactor.onEditingCanceled()
verify {
navController.navigateUp()
}
}
@Test
fun onTextChanged() {
val store: SearchStore = mockk(relaxed = true)
val interactor = SearchInteractor(mockk(), mockk(), store)
interactor.onTextChanged("test")
verify { store.dispatch(SearchAction.UpdateQuery("test")) }
}
@Test
fun onUrlTapped() {
val context: HomeActivity = mockk()
val store: SearchStore = mockk()
val state: SearchState = mockk()
every { context.metrics } returns mockk(relaxed = true)
every { context.openToBrowserAndLoad(any(), any(), any()) } just Runs
every { store.state } returns state
every { state.session } returns null
val interactor = SearchInteractor(context, mockk(), store)
interactor.onUrlTapped("test")
verify {
context.openToBrowserAndLoad(
"test",
true,
BrowserDirection.FromSearch
)
}
}
@Test
fun onSearchTermsTapped() {
val context: HomeActivity = mockk()
val store: SearchStore = mockk()
val state: SearchState = mockk()
val searchEngineManager: SearchEngineManager = mockk(relaxed = true)
val searchEngine = SearchEngineSource.Default(mockk())
every { context.metrics } returns mockk(relaxed = true)
every { context.searchEngineManager } returns searchEngineManager
every { context.openToBrowserAndLoad(any(), any(), any(), any(), any(), any()) } just Runs
every { store.state } returns state
every { state.session } returns null
every { state.searchEngineSource } returns searchEngine
val interactor = SearchInteractor(context, mockk(), store)
interactor.onSearchTermsTapped("test")
verify { context.openToBrowserAndLoad(
searchTermOrURL = "test",
newTab = true,
from = BrowserDirection.FromSearch,
engine = searchEngine.searchEngine,
forceSearch = true
) }
}
@Test
fun onSearchShortcutEngineSelected() {
val context: HomeActivity = mockk(relaxed = true)
every { context.metrics } returns mockk(relaxed = true)
val store: SearchStore = mockk(relaxed = true)
val interactor = SearchInteractor(context, mockk(), store)
val searchEngine: SearchEngine = mockk(relaxed = true)
interactor.onSearchShortcutEngineSelected(searchEngine)
verify { store.dispatch(SearchAction.SearchShortcutEngineSelected(searchEngine)) }
}
@Test
fun onClickSearchEngineSettings() {
val navController: NavController = mockk()
val interactor = SearchInteractor(mockk(), navController, mockk())
every { navController.navigate(any() as NavDirections) } just Runs
interactor.onClickSearchEngineSettings()
verify {
navController.navigate(SearchFragmentDirections.actionSearchFragmentToSearchEngineFragment())
}
}
}

View File

@ -0,0 +1,2 @@
mock-maker-inline
// This allows mocking final classes (classes are final by default in Kotlin)

View File

@ -46,7 +46,7 @@ private object Versions {
const val installreferrer = "1.0" const val installreferrer = "1.0"
const val junit = "4.12" const val junit = "4.12"
const val mockito = "2.23.0" const val mockito = "2.24.5"
const val mockk = "1.9.kotlin12" const val mockk = "1.9.kotlin12"
const val glide = "4.9.0" const val glide = "4.9.0"
const val flipper = "0.21.0" const val flipper = "0.21.0"