diff --git a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt index f25f3ef9f..f23c51c31 100644 --- a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt +++ b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt @@ -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), diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index a03672372..c095b28b8 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -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 -> diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt index 7851cb05f..11d5faca1 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt @@ -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)) } diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 822888852..643e6da94 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogController.kt b/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogController.kt new file mode 100644 index 000000000..3585efbbf --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogController.kt @@ -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) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt index 91b8bcf22..dad829902 100644 --- a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt @@ -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() + 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 + } } diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 6d0b2265b..43cea13ed 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -144,7 +144,21 @@ + tools:layout="@layout/fragment_search_dialog"> + + + +