From 36af5107c63fa7eb1b91a15fc5e8d9f345bd9462 Mon Sep 17 00:00:00 2001 From: Sawyer Blatz Date: Fri, 29 Mar 2019 13:49:50 -0700 Subject: [PATCH] Closes #875: Adds search shortcuts (#882) * Closes #875: Adds shortcuts * Refactor and clean up * Remove TODO * Removes local * Fix nits * Refactors to add ShortcutEngineManager --- .../java/org/mozilla/fenix/HomeActivity.kt | 16 +- .../mozilla/fenix/browser/BrowserFragment.kt | 4 +- .../components/toolbar/ToolbarComponent.kt | 25 +++- .../fenix/components/toolbar/ToolbarUIView.kt | 63 +++++++- .../mozilla/fenix/search/SearchFragment.kt | 33 +++-- .../org/mozilla/fenix/search/SearchLayouts.kt | 7 +- .../search/awesomebar/AwesomeBarComponent.kt | 17 ++- .../search/awesomebar/AwesomeBarUIView.kt | 139 ++++++++++++++---- .../awesomebar/ShortcutEngineManager.kt | 100 +++++++++++++ .../awesomebar/ShortcutsSuggestionProvider.kt | 57 +++++++ .../res/drawable/search_pill_background.xml | 4 +- .../main/res/layout/component_awesomebar.xml | 1 - app/src/main/res/layout/fragment_search.xml | 25 +++- app/src/main/res/navigation/nav_graph.xml | 2 + app/src/main/res/values/attrs.xml | 9 +- app/src/main/res/values/colors.xml | 6 +- app/src/main/res/values/strings.xml | 4 + app/src/main/res/values/styles.xml | 4 +- 18 files changed, 441 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/search/awesomebar/ShortcutEngineManager.kt create mode 100644 app/src/main/java/org/mozilla/fenix/search/awesomebar/ShortcutsSuggestionProvider.kt diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 1506a3aa3..b8dea6382 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -16,6 +16,7 @@ import androidx.appcompat.widget.Toolbar import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.NavigationUI +import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.session.Session import mozilla.components.concept.engine.EngineView import mozilla.components.feature.intent.IntentProcessor @@ -148,9 +149,14 @@ open class HomeActivity : AppCompatActivity() { openToBrowser(SafeIntent(intent).getStringExtra(IntentProcessor.ACTIVE_SESSION_ID), BrowserDirection.FromGlobal) } - fun openToBrowserAndLoad(text: String, sessionId: String? = null, from: BrowserDirection) { + fun openToBrowserAndLoad( + text: String, + sessionId: String? = null, + engine: SearchEngine? = null, + from: BrowserDirection + ) { openToBrowser(sessionId, from) - load(text, sessionId) + load(text, sessionId, engine) } fun openToBrowser(sessionId: String?, from: BrowserDirection) { @@ -165,7 +171,7 @@ open class HomeActivity : AppCompatActivity() { navHost.navController.navigate(directions) } - private fun load(text: String, sessionId: String?) { + private fun load(text: String, sessionId: String?, engine: SearchEngine?) { val isPrivate = this.browsingModeManager.isPrivate val loadUrlUseCase = if (sessionId == null) { @@ -179,8 +185,8 @@ open class HomeActivity : AppCompatActivity() { val searchUseCase: (String) -> Unit = { searchTerms -> if (sessionId == null) { components.useCases.searchUseCases.newTabSearch - .invoke(searchTerms, Session.Source.USER_ENTERED, true, isPrivate) - } else components.useCases.searchUseCases.defaultSearch.invoke(searchTerms) + .invoke(searchTerms, Session.Source.USER_ENTERED, true, isPrivate, searchEngine = engine) + } else components.useCases.searchUseCases.defaultSearch.invoke(searchTerms, engine) } if (text.isUrl()) { diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index 73b99a693..80ec0e937 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -21,6 +21,7 @@ import androidx.navigation.Navigation import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.component_search.* import kotlinx.android.synthetic.main.fragment_browser.view.* +import kotlinx.android.synthetic.main.fragment_search.* import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior import mozilla.components.feature.contextmenu.ContextMenuCandidate import mozilla.components.feature.contextmenu.ContextMenuFeature @@ -90,7 +91,8 @@ class BrowserFragment : Fragment(), BackHandler { view.browserLayout, ActionBusFactory.get(this), sessionId, (activity as HomeActivity).browsingModeManager.isPrivate, - SearchState("", isEditing = false) + SearchState("", isEditing = false), + search_engine_icon ) toolbarComponent.uiView.view.apply { diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarComponent.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarComponent.kt index a2fe8e7ae..93e96a853 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarComponent.kt @@ -5,8 +5,10 @@ package org.mozilla.fenix.components.toolbar import android.view.ViewGroup +import android.widget.ImageView import androidx.core.content.ContextCompat import kotlinx.android.synthetic.main.component_search.* +import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.toolbar.BrowserToolbar import org.mozilla.fenix.DefaultThemeManager import org.mozilla.fenix.R @@ -22,7 +24,8 @@ class ToolbarComponent( bus: ActionBusFactory, private val sessionId: String?, private val isPrivate: Boolean, - override var initialState: SearchState = SearchState("", false) + override var initialState: SearchState = SearchState("", false), + private val engineIconView: ImageView? = null ) : UIComponent( bus.getManagedEmitter(SearchAction::class.java), @@ -34,10 +37,19 @@ class ToolbarComponent( override val reducer: Reducer = { state, change -> when (change) { is SearchChange.QueryChanged -> state.copy(query = change.query) + is SearchChange.SearchShortcutEngineSelected -> + state.copy(engine = change.engine) } } - override fun initView() = ToolbarUIView(sessionId, isPrivate, container, actionEmitter, changesObservable) + override fun initView() = ToolbarUIView( + sessionId, + isPrivate, + container, + actionEmitter, + changesObservable, + engineIconView + ) init { render(reducer) @@ -60,10 +72,14 @@ class ToolbarComponent( } } -data class SearchState(val query: String, val isEditing: Boolean) : ViewState +data class SearchState( + val query: String, + val isEditing: Boolean, + val engine: SearchEngine? = null +) : ViewState sealed class SearchAction : Action { - data class UrlCommitted(val url: String, val session: String?) : SearchAction() + data class UrlCommitted(val url: String, val session: String?, val engine: SearchEngine? = null) : SearchAction() data class TextChanged(val query: String) : SearchAction() object ToolbarTapped : SearchAction() data class ToolbarMenuItemTapped(val item: ToolbarMenu.Item) : SearchAction() @@ -72,4 +88,5 @@ sealed class SearchAction : Action { sealed class SearchChange : Change { data class QueryChanged(val query: String) : SearchChange() + data class SearchShortcutEngineSelected(val engine: SearchEngine) : SearchChange() } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarUIView.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarUIView.kt index a0f94f519..4fa493b55 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarUIView.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarUIView.kt @@ -4,8 +4,10 @@ package org.mozilla.fenix.components.toolbar +import android.graphics.drawable.BitmapDrawable import android.view.LayoutInflater import android.view.ViewGroup +import android.widget.ImageView import androidx.core.content.ContextCompat import io.reactivex.Observable import io.reactivex.Observer @@ -13,6 +15,7 @@ import io.reactivex.functions.Consumer import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.support.ktx.android.content.res.pxToDp +import org.jetbrains.anko.backgroundDrawable import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.mvi.UIView @@ -22,11 +25,14 @@ class ToolbarUIView( isPrivate: Boolean, container: ViewGroup, actionEmitter: Observer, - changesObservable: Observable + changesObservable: Observable, + private val engineIconView: ImageView? = null ) : UIView(container, actionEmitter, changesObservable) { val toolbarIntegration: ToolbarIntegration + var state: SearchState? = null + private set override val view: BrowserToolbar = LayoutInflater.from(container.context) .inflate(R.layout.component_search, container, true) @@ -38,7 +44,7 @@ class ToolbarUIView( init { view.apply { setOnUrlCommitListener { - actionEmitter.onNext(SearchAction.UrlCommitted(it, sessionId)) + actionEmitter.onNext(SearchAction.UrlCommitted(it, sessionId, state?.engine)) false } onUrlClicked = { @@ -87,14 +93,63 @@ class ToolbarUIView( } override fun updateView() = Consumer { - if (it.isEditing) { - view.url = it.query + if (shouldUpdateEngineIcon(it)) { + updateEngineIcon(it) + } + + if (shouldClearSearchURL(it)) { + clearSearchURL() + } + + if (shouldUpdateEditingState(it)) { + updateEditingState(it) + } + + state = it + } + + private fun shouldUpdateEngineIcon(newState: SearchState): Boolean { + return newState.isEditing && (engineDidChange(newState) || state == null) + } + + private fun updateEngineIcon(newState: SearchState) { + with(view.context) { + val defaultEngineIcon = components.search.searchEngineManager.defaultSearchEngine?.icon + val searchIcon = newState.engine?.icon ?: defaultEngineIcon + val draw = BitmapDrawable(searchIcon) + val iconSize = + containerView?.context!!.resources.getDimension(R.dimen.preference_icon_drawable_size).toInt() + draw.setBounds(0, 0, iconSize, iconSize) + engineIconView?.backgroundDrawable = draw + } + } + + private fun shouldClearSearchURL(newState: SearchState): Boolean { + return newState.engine != state?.engine && view.url == newState.query + } + + private fun clearSearchURL() { + view.url = "" + view.editMode() + } + + private fun shouldUpdateEditingState(newState: SearchState): Boolean { + return !engineDidChange(newState) + } + + private fun updateEditingState(newState: SearchState) { + if (newState.isEditing) { + view.url = newState.query view.editMode() } else { view.displayMode() } } + private fun engineDidChange(newState: SearchState): Boolean { + return newState.engine != state?.engine + } + companion object { const val browserActionMarginDp = 8 } diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt index 0f0bdfae2..a8104790c 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt @@ -5,7 +5,6 @@ package org.mozilla.fenix.search import android.content.Context -import android.graphics.drawable.BitmapDrawable import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -17,13 +16,13 @@ import kotlinx.android.synthetic.main.fragment_search.view.* import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.session.SessionUseCases import mozilla.components.support.ktx.kotlin.isUrl -import org.jetbrains.anko.backgroundDrawable import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.utils.ItsNotBrokenSnack import org.mozilla.fenix.R 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 @@ -35,6 +34,7 @@ import org.mozilla.fenix.mvi.getManagedEmitter 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.AwesomeBarUIView class SearchFragment : Fragment() { private lateinit var toolbarComponent: ToolbarComponent @@ -65,7 +65,8 @@ class SearchFragment : Fragment() { ActionBusFactory.get(this), sessionId, isPrivate, - SearchState(url, isEditing = true) + SearchState(url, isEditing = true), + view.search_engine_icon ) awesomeBarComponent = AwesomeBarComponent(view.search_layout, ActionBusFactory.get(this)) @@ -82,15 +83,11 @@ class SearchFragment : Fragment() { view.toolbar_wrapper.clipToOutline = false - val searchIcon = requireComponents.search.searchEngineManager.getDefaultSearchEngine( - requireContext() - ).let { - BitmapDrawable(resources, it.icon) + search_shortcuts_button.setOnClickListener { + getManagedEmitter().onNext(AwesomeBarChange + .SearchShortcutEnginePicker(!( + (awesomeBarComponent.uiView as AwesomeBarUIView).state?.showShortcutEnginePicker ?: true))) } - - val iconSize = resources.getDimension(R.dimen.preference_icon_drawable_size).toInt() - searchIcon.setBounds(0, 0, iconSize, iconSize) - search_engine_icon.backgroundDrawable = searchIcon } override fun onResume() { @@ -100,13 +97,17 @@ class SearchFragment : Fragment() { override fun onStart() { super.onStart() + subscribeToSearchActions() + subscribeToAwesomeBarActions() + } + private fun subscribeToSearchActions() { getAutoDisposeObservable() .subscribe { when (it) { is SearchAction.UrlCommitted -> { if (it.url.isNotBlank()) { - (activity as HomeActivity).openToBrowserAndLoad(it.url, it.session, + (activity as HomeActivity).openToBrowserAndLoad(it.url, it.session, it.engine, BrowserDirection.FromSearch) val event = if (it.url.isUrl()) { @@ -126,7 +127,9 @@ class SearchFragment : Fragment() { } } } + } + private fun subscribeToAwesomeBarActions() { getAutoDisposeObservable() .subscribe { when (it) { @@ -141,6 +144,12 @@ class SearchFragment : Fragment() { (activity as HomeActivity).openToBrowser(sessionId, BrowserDirection.FromSearch) requireComponents.analytics.metrics.track(Event.PerformedSearch(true)) } + is AwesomeBarAction.SearchShortcutEngineSelected -> { + getManagedEmitter() + .onNext(AwesomeBarChange.SearchShortcutEngineSelected(it.engine)) + getManagedEmitter() + .onNext(SearchChange.SearchShortcutEngineSelected(it.engine)) + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchLayouts.kt b/app/src/main/java/org/mozilla/fenix/search/SearchLayouts.kt index eaeee2947..9df05785d 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchLayouts.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchLayouts.kt @@ -63,10 +63,15 @@ internal fun SearchFragment.setOutOfExperimentConstraints(layout: ConstraintLayo BOTTOM to TOP of UNSET ) } + search_with_shortcuts { + connect( + TOP to BOTTOM of toolbar_wrapper + ) + } awesomeBar { connect( TOP to TOP of UNSET, - TOP to BOTTOM of toolbar_wrapper, + TOP to BOTTOM of search_with_shortcuts, BOTTOM to TOP of pill_wrapper ) } diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarComponent.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarComponent.kt index 815720e5b..a24cf5da0 100644 --- a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarComponent.kt @@ -12,27 +12,38 @@ import org.mozilla.fenix.mvi.Reducer import org.mozilla.fenix.mvi.UIComponent import org.mozilla.fenix.mvi.ViewState -data class AwesomeBarState(val query: String) : ViewState +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?) : 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, - override var initialState: AwesomeBarState = AwesomeBarState("") + override var initialState: AwesomeBarState = AwesomeBarState("", false) ) : UIComponent( bus.getManagedEmitter(AwesomeBarAction::class.java), bus.getSafeManagedObservable(AwesomeBarChange::class.java) ) { override val reducer: Reducer = { 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) } } diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarUIView.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarUIView.kt index b3c8f0039..aaf56d0a0 100644 --- a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarUIView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarUIView.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.search.awesomebar import android.view.LayoutInflater import android.view.ViewGroup +import androidx.core.content.ContextCompat import io.reactivex.Observable import io.reactivex.Observer import io.reactivex.functions.Consumer @@ -24,7 +25,7 @@ import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.utils.Settings class AwesomeBarUIView( - container: ViewGroup, + private val container: ViewGroup, actionEmitter: Observer, changesObservable: Observable ) : @@ -37,56 +38,134 @@ class AwesomeBarUIView( .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 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) { + 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 { - val loadUrlUseCase = object : SessionUseCases.LoadUrlUseCase { - override fun invoke(url: String) { - actionEmitter.onNext(AwesomeBarAction.URLTapped(url)) - } - } - - val searchUseCase = object : SearchUseCases.SearchUseCase { - override fun invoke(searchTerms: String, searchEngine: SearchEngine?) { - actionEmitter.onNext(AwesomeBarAction.SearchTermsTapped(searchTerms, searchEngine)) - } - } - with(container.context) { - view.addProviders(ClipboardSuggestionProvider( + clipboardSuggestionProvider = ClipboardSuggestionProvider( this, loadUrlUseCase, getDrawable(R.drawable.ic_link)!!.toBitmap(), getString(R.string.awesomebar_clipboard_title) ) - ) - if (Settings.getInstance(container.context).showSearchSuggestions()) { - view.addProviders( - SearchSuggestionProvider( - searchEngine = components.search.searchEngineManager.getDefaultSearchEngine(this), - searchUseCase = searchUseCase, - fetchClient = components.core.client, - mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS, - limit = 3 - ) - ) - } - - view.addProviders( + sessionProvider = SessionSuggestionProvider( components.core.sessionManager, components.useCases.tabsUseCases.selectTab, components.utils.icons - ), + ) + + historyStorageProvider = HistoryStorageSuggestionProvider( components.core.historyStorage, loadUrlUseCase, components.utils.icons ) - ) + + if (Settings.getInstance(container.context).showSearchSuggestions()) { + val draw = getDrawable(R.drawable.ic_search) + draw?.setTint(ContextCompat.getColor(this, R.color.search_text)) + + defaultSearchSuggestionProvider = + SearchSuggestionProvider( + searchEngine = components.search.searchEngineManager.getDefaultSearchEngine(this), + searchUseCase = searchUseCase, + fetchClient = components.core.client, + mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS, + limit = 3, + icon = draw?.toBitmap() + ) + } + + shortcutsEnginePickerProvider = + ShortcutsSuggestionProvider( + components.search.searchEngineManager, + this, + shortcutEngineManager::selectShortcutEngine, + shortcutEngineManager::selectShortcutEngineSettings) + + shortcutEngineManager.shortcutsEnginePickerProvider = shortcutsEnginePickerProvider + } + } + + private fun showSuggestionProviders() { + if (Settings.getInstance(container.context).showSearchSuggestions()) { + view.addProviders(searchSuggestionProvider!!) + } + + view.addProviders( + clipboardSuggestionProvider!!, + historyStorageProvider!!, + sessionProvider!! + ) + } + + private fun showSearchSuggestionProvider() { + view.addProviders(searchSuggestionProvider!!) + } + + private fun setShortcutEngine(engine: SearchEngine) { + with(container.context) { + val draw = getDrawable(R.drawable.ic_search) + draw?.setTint(androidx.core.content.ContextCompat.getColor(this, R.color.search_text)) + + 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 { + shortcutEngineManager.updateSelectedEngineIfNecessary(it) + shortcutEngineManager.updateEnginePickerVisibilityIfNecessary(it) + view.onInputChanged(it.query) + state = it } } diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/ShortcutEngineManager.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/ShortcutEngineManager.kt new file mode 100644 index 000000000..fb3553e65 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/ShortcutEngineManager.kt @@ -0,0 +1,100 @@ +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.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.jetbrains.anko.textColor +import org.mozilla.fenix.DefaultThemeManager +import org.mozilla.fenix.R +import org.mozilla.fenix.search.SearchFragmentDirections + +class ShortcutEngineManager( + private val awesomeBarUIView: AwesomeBarUIView, + private val actionEmitter: Observer, + 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.background = getDrawable(R.drawable.search_pill_background) + + awesomeBarUIView.search_shortcuts_button.compoundDrawables[0].setTint(ContextCompat.getColor(this, + DefaultThemeManager.resolveAttribute(R.attr.pillWrapperBackground, this))) + + awesomeBarUIView.search_shortcuts_button.textColor = ContextCompat.getColor(this, + DefaultThemeManager.resolveAttribute(R.attr.pillWrapperBackground, this)) + + awesomeBarUIView.view.removeAllProviders() + awesomeBarUIView.view.addProviders(shortcutsEnginePickerProvider!!) + } + } + + private fun hideShortcutEnginePicker() { + with(context) { + awesomeBarUIView.search_shortcuts_button.setBackgroundColor(ContextCompat.getColor(this, + DefaultThemeManager.resolveAttribute(R.attr.pillWrapperBackground, this))) + + awesomeBarUIView.search_shortcuts_button.compoundDrawables[0].setTint(ContextCompat.getColor(this, + DefaultThemeManager.resolveAttribute(R.attr.searchShortcutsTextColor, this))) + + awesomeBarUIView.search_shortcuts_button.textColor = ContextCompat.getColor(this, + DefaultThemeManager.resolveAttribute(R.attr.searchShortcutsTextColor, this)) + + awesomeBarUIView.view.removeProviders(shortcutsEnginePickerProvider!!) + } + } + + private fun updateSearchWithVisibility(visible: Boolean) { + awesomeBarUIView.search_with_shortcuts.visibility = if (visible) View.VISIBLE else View.GONE + } +} diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/ShortcutsSuggestionProvider.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/ShortcutsSuggestionProvider.kt new file mode 100644 index 000000000..2b9b9c5ce --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/ShortcutsSuggestionProvider.kt @@ -0,0 +1,57 @@ +package org.mozilla.fenix.search.awesomebar + +import android.content.Context +import mozilla.components.browser.search.SearchEngine +import mozilla.components.browser.search.SearchEngineManager +import mozilla.components.concept.awesomebar.AwesomeBar +import mozilla.components.support.ktx.android.graphics.drawable.toBitmap +import org.mozilla.fenix.R +import java.util.UUID + +/** + * A [AwesomeBar.SuggestionProvider] implementation that provides search engine suggestions. + */ +class ShortcutsSuggestionProvider( + private val searchEngineManager: SearchEngineManager, + private val context: Context, + private val selectShortcutEngine: (engine: SearchEngine) -> Unit, + private val selectShortcutEngineSettings: () -> Unit +) : AwesomeBar.SuggestionProvider { + override val id: String = UUID.randomUUID().toString() + + override val shouldClearSuggestions: Boolean + get() = false + + override suspend fun onInputChanged(text: String): List { + val suggestions = mutableListOf() + + searchEngineManager.getSearchEngines(context).forEach { + suggestions.add( + AwesomeBar.Suggestion( + provider = this, + id = id, + icon = { _, _ -> + it.icon + }, + title = it.name, + onSuggestionClicked = { + selectShortcutEngine(it) + }) + ) + } + + suggestions.add( + AwesomeBar.Suggestion( + provider = this, + id = id, + icon = { _, _ -> + context.getDrawable(R.drawable.ic_settings)?.toBitmap() + }, + title = context.getString(R.string.search_shortcuts_engine_settings), + onSuggestionClicked = { + selectShortcutEngineSettings() + }) + ) + return suggestions + } +} diff --git a/app/src/main/res/drawable/search_pill_background.xml b/app/src/main/res/drawable/search_pill_background.xml index 6a67b6163..eb8f2660a 100644 --- a/app/src/main/res/drawable/search_pill_background.xml +++ b/app/src/main/res/drawable/search_pill_background.xml @@ -3,10 +3,10 @@ - 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/. --> - + + android:color="?attr/pillWrapperSelectedBackground"/> \ No newline at end of file diff --git a/app/src/main/res/layout/component_awesomebar.xml b/app/src/main/res/layout/component_awesomebar.xml index 00cc92f3a..31d2be151 100644 --- a/app/src/main/res/layout/component_awesomebar.xml +++ b/app/src/main/res/layout/component_awesomebar.xml @@ -10,7 +10,6 @@ android:layout_width="0dp" android:layout_height="0dp" android:padding="4dp" - android:layout_marginTop="16dp" app:layout_constraintTop_toBottomOf="@id/toolbar_wrapper" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 2f78d895d..b1382f1fd 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -40,8 +40,25 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@id/search_engine_icon" app:layout_constraintEnd_toEndOf="parent"/> + + + + android:background="?attr/pillWrapperBackground"/>