1
0
Fork 0

Wires up controller, store and interactor. (#13324)

* For #13320 - Wires up the search store, controller and interactor for the new search experience

* For #13323 - Navigates to new search experience from the browser when enabled
master
Jeff Boek 2020-08-05 16:22:07 -07:00 committed by GitHub
parent 6492773fc7
commit ccb5b0b641
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 315 additions and 78 deletions

View File

@ -17,6 +17,7 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromGlobal(0),
FromHome(R.id.homeFragment),
FromSearch(R.id.searchFragment),
FromSearchDialog(R.id.searchDialogFragment),
FromSettings(R.id.settingsFragment),
FromSyncedTabs(R.id.syncedTabsFragment),
FromBookmarks(R.id.bookmarkFragment),

View File

@ -86,6 +86,7 @@ import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.search.SearchFragmentDirections
import org.mozilla.fenix.searchdialog.SearchDialogFragmentDirections
import org.mozilla.fenix.session.NotificationSessionObserver
import org.mozilla.fenix.settings.SettingsFragmentDirections
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
@ -556,6 +557,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
HomeFragmentDirections.actionHomeFragmentToBrowserFragment(customTabSessionId, true)
BrowserDirection.FromSearch ->
SearchFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSearchDialog ->
SearchDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSettings ->
SettingsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSyncedTabs ->

View File

@ -81,6 +81,9 @@ class DefaultBrowserToolbarController(
private val onCloseTab: (Session) -> Unit
) : BrowserToolbarController {
private val useNewSearchExperience
get() = activity.settings().useNewSearchExperience
private val currentSession
get() = customTabSession ?: activity.components.core.sessionManager.selectedSession
@ -91,10 +94,17 @@ class DefaultBrowserToolbarController(
internal var ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
override fun handleToolbarPaste(text: String) {
val directions = BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
sessionId = currentSession?.id,
pastedText = text
)
val directions = if (useNewSearchExperience) {
BrowserFragmentDirections.actionGlobalSearchDialog(
sessionId = currentSession?.id,
pastedText = text
)
} else {
BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
sessionId = currentSession?.id,
pastedText = text
)
}
navController.nav(R.id.browserFragment, directions, getToolbarNavOptions(activity))
}
@ -117,9 +127,15 @@ class DefaultBrowserToolbarController(
Event.SearchBarTapped(Event.SearchBarTapped.Source.BROWSER)
)
val directions = BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
currentSession?.id
)
val directions = if (useNewSearchExperience) {
BrowserFragmentDirections.actionGlobalSearchDialog(
currentSession?.id
)
} else {
BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
currentSession?.id
)
}
navController.nav(R.id.browserFragment, directions, getToolbarNavOptions(activity))
}

View File

@ -674,7 +674,9 @@ class HomeFragment : Fragment() {
private fun navigateToSearch() {
val directions = if (requireContext().settings().useNewSearchExperience) {
HomeFragmentDirections.actionGlobalSearchDialog()
HomeFragmentDirections.actionGlobalSearchDialog(
sessionId = null
)
} else {
HomeFragmentDirections.actionGlobalSearch(
sessionId = null

View File

@ -0,0 +1,180 @@
/* 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.searchdialog
import android.content.Intent
import androidx.navigation.NavController
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.crashes.CrashListActivity
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.search.SearchController
import org.mozilla.fenix.search.SearchFragmentAction
import org.mozilla.fenix.search.SearchFragmentStore
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings
@Suppress("TooManyFunctions", "LongParameterList")
class SearchDialogController(
private val activity: HomeActivity,
private val sessionManager: SessionManager,
private val store: SearchFragmentStore,
private val navController: NavController,
private val settings: Settings,
private val metrics: MetricController,
private val clearToolbarFocus: () -> Unit
) : SearchController {
override fun handleUrlCommitted(url: String) {
when (url) {
"about:crashes" -> {
// The list of past crashes can be accessed via "settings > about", but desktop and
// fennec users may be used to navigating to "about:crashes". So we intercept this here
// and open the crash list activity instead.
activity.startActivity(Intent(activity, CrashListActivity::class.java))
}
"moz://a" -> openSearchOrUrl(SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.MANIFESTO))
else -> if (url.isNotBlank()) {
openSearchOrUrl(url)
}
}
}
private fun openSearchOrUrl(url: String) {
activity.openToBrowserAndLoad(
searchTermOrURL = url,
newTab = store.state.tabId == null,
from = BrowserDirection.FromSearchDialog,
engine = store.state.searchEngineSource.searchEngine
)
val event = if (url.isUrl()) {
Event.EnteredUrl(false)
} else {
settings.incrementActiveSearchCount()
val searchAccessPoint = when (store.state.searchAccessPoint) {
Event.PerformedSearch.SearchAccessPoint.NONE -> Event.PerformedSearch.SearchAccessPoint.ACTION
else -> store.state.searchAccessPoint
}
searchAccessPoint?.let { sap ->
MetricsUtils.createSearchEvent(
store.state.searchEngineSource.searchEngine,
activity,
sap
)
}
}
event?.let { metrics.track(it) }
}
override fun handleEditingCancelled() {
clearToolbarFocus()
}
override fun handleTextChanged(text: String) {
// Display the search shortcuts on each entry of the search fragment (see #5308)
val textMatchesCurrentUrl = store.state.url == text
val textMatchesCurrentSearch = store.state.searchTerms == text
store.dispatch(SearchFragmentAction.UpdateQuery(text))
store.dispatch(
SearchFragmentAction.ShowSearchShortcutEnginePicker(
(textMatchesCurrentUrl || textMatchesCurrentSearch || text.isEmpty()) &&
settings.shouldShowSearchShortcuts
)
)
store.dispatch(
SearchFragmentAction.AllowSearchSuggestionsInPrivateModePrompt(
text.isNotEmpty() &&
activity.browsingModeManager.mode.isPrivate &&
!settings.shouldShowSearchSuggestionsInPrivate &&
!settings.showSearchSuggestionsInPrivateOnboardingFinished
)
)
}
override fun handleUrlTapped(url: String) {
clearToolbarFocus()
activity.openToBrowserAndLoad(
searchTermOrURL = url,
newTab = store.state.tabId == null,
from = BrowserDirection.FromSearchDialog
)
metrics.track(Event.EnteredUrl(false))
}
override fun handleSearchTermsTapped(searchTerms: String) {
settings.incrementActiveSearchCount()
clearToolbarFocus()
activity.openToBrowserAndLoad(
searchTermOrURL = searchTerms,
newTab = store.state.tabId == null,
from = BrowserDirection.FromSearchDialog,
engine = store.state.searchEngineSource.searchEngine,
forceSearch = true
)
val searchAccessPoint = when (store.state.searchAccessPoint) {
Event.PerformedSearch.SearchAccessPoint.NONE -> Event.PerformedSearch.SearchAccessPoint.SUGGESTION
else -> store.state.searchAccessPoint
}
val event = searchAccessPoint?.let { sap ->
MetricsUtils.createSearchEvent(
store.state.searchEngineSource.searchEngine,
activity,
sap
)
}
event?.let { metrics.track(it) }
}
override fun handleSearchShortcutEngineSelected(searchEngine: SearchEngine) {
store.dispatch(SearchFragmentAction.SearchShortcutEngineSelected(searchEngine))
val isCustom =
CustomSearchEngineStore.isCustomSearchEngine(activity, searchEngine.identifier)
metrics.track(Event.SearchShortcutSelected(searchEngine, isCustom))
}
override fun handleSearchShortcutsButtonClicked() {
val isOpen = store.state.showSearchShortcuts
store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(!isOpen))
}
override fun handleClickSearchEngineSettings() {
val directions = SearchDialogFragmentDirections.actionGlobalSearchEngineFragment()
navController.navigateSafe(R.id.searchDialogFragment, directions)
}
override fun handleExistingSessionSelected(session: Session) {
clearToolbarFocus()
sessionManager.select(session)
activity.openToBrowser(
from = BrowserDirection.FromSearchDialog
)
}
override fun handleExistingSessionSelected(tabId: String) {
val session = sessionManager.findSessionById(tabId)
if (session != null) {
handleExistingSessionSelected(session)
}
}
}

View File

@ -9,88 +9,43 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.fragment_search.view.*
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
import kotlinx.android.synthetic.main.fragment_search_dialog.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.selector.findTab
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.logDebug
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.search.SearchEngineSource
import org.mozilla.fenix.search.SearchFragmentState
import org.mozilla.fenix.search.awesomebar.AwesomeBarInteractor
import org.mozilla.fenix.search.SearchFragmentStore
import org.mozilla.fenix.search.SearchInteractor
import org.mozilla.fenix.search.awesomebar.AwesomeBarView
import org.mozilla.fenix.search.toolbar.ToolbarInteractor
import org.mozilla.fenix.search.toolbar.ToolbarView
import org.mozilla.fenix.utils.Settings
class TempSearchInteractor(val onTextChangedCallback: (String) -> Unit) : ToolbarInteractor, AwesomeBarInteractor {
override fun onUrlCommitted(url: String) {
logDebug("boek", "onUrlCommitted $url")
}
override fun onEditingCanceled() {
logDebug("boek", "onEditingCanceled")
}
override fun onTextChanged(text: String) {
onTextChangedCallback.invoke(text)
}
override fun onUrlTapped(url: String) {
logDebug("boek", "onEditingCanceled")
}
override fun onSearchTermsTapped(searchTerms: String) {
logDebug("boek", "onEditingCanceled")
}
override fun onSearchShortcutEngineSelected(searchEngine: SearchEngine) {
logDebug("boek", "onEditingCanceled")
}
override fun onClickSearchEngineSettings() {
logDebug("boek", "onEditingCanceled")
}
override fun onExistingSessionSelected(session: Session) {
logDebug("boek", "onEditingCanceled")
}
override fun onExistingSessionSelected(tabId: String) {
logDebug("boek", "onEditingCanceled")
}
override fun onSearchShortcutsButtonClicked() {
logDebug("boek", "onEditingCanceled")
typealias SearchDialogFragmentStore = SearchFragmentStore
typealias SearchDialogInteractor = SearchInteractor
fun Settings.shouldShowSearchSuggestions(isPrivate: Boolean): Boolean {
return if (isPrivate) {
shouldShowSearchSuggestions && shouldShowSearchSuggestionsInPrivate
} else {
shouldShowSearchSuggestions
}
}
class SearchDialogFragment : AppCompatDialogFragment() {
private lateinit var interactor: SearchDialogInteractor
private lateinit var store: SearchDialogFragmentStore
private lateinit var toolbarView: ToolbarView
private lateinit var awesomeBarView: AwesomeBarView
private val tempInteractor = TempSearchInteractor {
view?.awesomeBar?.visibility = if (it.isEmpty()) View.INVISIBLE else View.VISIBLE
awesomeBarView.update(
SearchFragmentState(
query = it,
url = "",
searchTerms = "",
searchEngineSource = SearchEngineSource.Default(requireComponents.search.provider.getDefaultEngine(requireContext())),
defaultEngineSource = SearchEngineSource.Default(requireComponents.search.provider.getDefaultEngine(requireContext())),
showSearchSuggestions = true,
showSearchSuggestionsHint = false,
showSearchShortcuts = false,
areShortcutsAvailable = false,
showClipboardSuggestions = true,
showHistorySuggestions = true,
showBookmarkSuggestions = true,
tabId = null,
pastedText = null,
searchAccessPoint = null
)
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -103,10 +58,26 @@ class SearchDialogFragment : AppCompatDialogFragment() {
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_search_dialog, container, false)
store = SearchDialogFragmentStore(setUpState())
interactor = SearchDialogInteractor(
SearchDialogController(
activity = requireActivity() as HomeActivity,
sessionManager = requireComponents.core.sessionManager,
store = store,
navController = findNavController(),
settings = requireContext().settings(),
metrics = requireComponents.analytics.metrics,
clearToolbarFocus = {
toolbarView.view.hideKeyboard()
toolbarView.view.clearFocus()
}
)
)
toolbarView = ToolbarView(
requireContext(),
tempInteractor,
interactor,
null,
false,
view.toolbar,
@ -115,10 +86,60 @@ class SearchDialogFragment : AppCompatDialogFragment() {
awesomeBarView = AwesomeBarView(
requireContext(),
tempInteractor,
interactor,
view.awesomeBar
)
return view
}
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
consumeFrom(store) {
awesomeBar?.visibility = if (it.query.isEmpty()) View.INVISIBLE else View.VISIBLE
toolbarView.update(it)
awesomeBarView.update(it)
}
}
private fun setUpState(): SearchFragmentState {
val activity = activity as HomeActivity
val settings = activity.settings()
val args by navArgs<SearchDialogFragmentArgs>()
val tabId = args.sessionId
val tab = tabId?.let { requireComponents.core.store.state.findTab(it) }
val url = tab?.content?.url.orEmpty()
val currentSearchEngine = SearchEngineSource.Default(
requireComponents.search.provider.getDefaultEngine(requireContext())
)
val isPrivate = activity.browsingModeManager.mode.isPrivate
val areShortcutsAvailable =
requireContext().components.search.provider.installedSearchEngines(requireContext())
.list.size >= MINIMUM_SEARCH_ENGINES_NUMBER_TO_SHOW_SHORTCUTS
return SearchFragmentState(
query = url,
url = url,
searchTerms = tab?.content?.searchTerms.orEmpty(),
searchEngineSource = currentSearchEngine,
defaultEngineSource = currentSearchEngine,
showSearchSuggestions = settings.shouldShowSearchSuggestions(isPrivate),
showSearchSuggestionsHint = false,
showSearchShortcuts = settings.shouldShowSearchShortcuts &&
url.isEmpty() &&
areShortcutsAvailable,
areShortcutsAvailable = areShortcutsAvailable,
showClipboardSuggestions = settings.shouldShowClipboardSuggestions,
showHistorySuggestions = settings.shouldShowHistorySuggestions,
showBookmarkSuggestions = settings.shouldShowBookmarkSuggestions,
tabId = tabId,
pastedText = args.pastedText,
searchAccessPoint = args.searchAccessPoint
)
}
companion object {
private const val MINIMUM_SEARCH_ENGINES_NUMBER_TO_SHOW_SHORTCUTS = 2
}
}

View File

@ -144,7 +144,21 @@
<dialog
android:id="@+id/searchDialogFragment"
android:name="org.mozilla.fenix.searchdialog.SearchDialogFragment"
tools:layout="@layout/fragment_search_dialog" />
tools:layout="@layout/fragment_search_dialog">
<argument
android:name="session_id"
app:argType="string"
app:nullable="true" />
<argument
android:name="pastedText"
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
<argument
android:name="search_access_point"
android:defaultValue="NONE"
app:argType="org.mozilla.fenix.components.metrics.Event$PerformedSearch$SearchAccessPoint" />
</dialog>
<fragment
android:id="@+id/searchFragment"