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 documentationmaster
parent
6b639b1a32
commit
e4ff70c542
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
mock-maker-inline
|
||||||
|
// This allows mocking final classes (classes are final by default in Kotlin)
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue