/* 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 androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.core.graphics.BlendModeColorFilterCompat.createBlendModeColorFilterCompat import androidx.core.graphics.BlendModeCompat.SRC_IN import androidx.core.graphics.drawable.toBitmap import mozilla.components.browser.awesomebar.BrowserAwesomeBar import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.session.Session import mozilla.components.concept.awesomebar.AwesomeBar import mozilla.components.concept.engine.EngineSession import mozilla.components.feature.awesomebar.provider.BookmarksStorageSuggestionProvider import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider import mozilla.components.feature.awesomebar.provider.SearchActionProvider 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.feature.tabs.TabsUseCases import mozilla.components.support.ktx.android.content.getColorFromAttr import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.ext.asActivity import org.mozilla.fenix.ext.components import org.mozilla.fenix.search.SearchEngineSource import org.mozilla.fenix.search.SearchFragmentState /** * View that contains and configures the BrowserAwesomeBar */ class AwesomeBarView( private val container: ViewGroup, val interactor: AwesomeBarInteractor, val view: BrowserAwesomeBar ) { private val sessionProvider: SessionSuggestionProvider private val historyStorageProvider: HistoryStorageSuggestionProvider private val shortcutsEnginePickerProvider: ShortcutsSuggestionProvider private val bookmarksStorageSuggestionProvider: BookmarksStorageSuggestionProvider private val defaultSearchSuggestionProvider: SearchSuggestionProvider private val defaultSearchActionProvider: SearchActionProvider private val searchSuggestionProviderMap: MutableMap> private var providersInUse = mutableSetOf() private val loadUrlUseCase = object : SessionUseCases.LoadUrlUseCase { override fun invoke( url: String, flags: EngineSession.LoadUrlFlags, additionalHeaders: Map? ) { interactor.onUrlTapped(url) } } private val searchUseCase = object : SearchUseCases.SearchUseCase { override fun invoke( searchTerms: String, searchEngine: SearchEngine?, parentSession: Session? ) { interactor.onSearchTermsTapped(searchTerms) } } private val shortcutSearchUseCase = object : SearchUseCases.SearchUseCase { override fun invoke( searchTerms: String, searchEngine: SearchEngine?, parentSession: Session? ) { interactor.onSearchTermsTapped(searchTerms) } } private val selectTabUseCase = object : TabsUseCases.SelectTabUseCase { override fun invoke(session: Session) { interactor.onExistingSessionSelected(session) } override fun invoke(tabId: String) { interactor.onExistingSessionSelected(tabId) } } init { view.itemAnimator = null val context = container.context val components = context.components val primaryTextColor = context.getColorFromAttr(R.attr.primaryText) val draw = getDrawable(context, R.drawable.ic_link)!! draw.colorFilter = createBlendModeColorFilterCompat(primaryTextColor, SRC_IN) val engineForSpeculativeConnects = if (!isBrowsingModePrivate()) components.core.engine else null sessionProvider = SessionSuggestionProvider( context.resources, components.core.store, selectTabUseCase, components.core.icons, getDrawable(context, R.drawable.ic_search_results_tab), excludeSelectedSession = true ) historyStorageProvider = HistoryStorageSuggestionProvider( components.core.historyStorage, loadUrlUseCase, components.core.icons, engineForSpeculativeConnects ) bookmarksStorageSuggestionProvider = BookmarksStorageSuggestionProvider( bookmarksStorage = components.core.bookmarksStorage, loadUrlUseCase = loadUrlUseCase, icons = components.core.icons, indicatorIcon = getDrawable(context, R.drawable.ic_search_results_bookmarks), engine = engineForSpeculativeConnects ) val searchBitmap = getDrawable(context, R.drawable.ic_search)!!.apply { colorFilter = createBlendModeColorFilterCompat(primaryTextColor, SRC_IN) }.toBitmap() defaultSearchSuggestionProvider = SearchSuggestionProvider( context = context, searchEngineManager = components.search.searchEngineManager, searchUseCase = searchUseCase, fetchClient = components.core.client, mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS, limit = 3, icon = searchBitmap, showDescription = false, engine = engineForSpeculativeConnects, filterExactMatch = true ) defaultSearchActionProvider = SearchActionProvider( searchEngineGetter = suspend { components.search.searchEngineManager.getDefaultSearchEngineAsync(context) }, searchUseCase = searchUseCase, icon = searchBitmap, showDescription = false ) shortcutsEnginePickerProvider = ShortcutsSuggestionProvider( searchEngineProvider = components.search.provider, context = context, selectShortcutEngine = interactor::onSearchShortcutEngineSelected, selectShortcutEngineSettings = interactor::onClickSearchEngineSettings ) searchSuggestionProviderMap = HashMap() } fun update(state: SearchFragmentState) { updateSuggestionProvidersVisibility(state) // Do not make suggestions based on user's current URL unless it's a search shortcut if (state.query.isNotEmpty() && state.query == state.url && !state.showSearchShortcuts) { return } view.onInputChanged(state.query) } private fun updateSuggestionProvidersVisibility(state: SearchFragmentState) { if (state.showSearchShortcuts) { handleDisplayShortcutsProviders() return } val providersToAdd = getProvidersToAdd(state) val providersToRemove = getProvidersToRemove(state) performProviderListChanges(providersToAdd, providersToRemove) } private fun performProviderListChanges( providersToAdd: MutableSet, providersToRemove: MutableSet ) { for (provider in providersToAdd) { if (providersInUse.find { it.id == provider.id } == null) { providersInUse.add(provider) view.addProviders(provider) } } for (provider in providersToRemove) { if (providersInUse.find { it.id == provider.id } != null) { providersInUse.remove(provider) view.removeProviders(provider) } } } private fun getProvidersToAdd(state: SearchFragmentState): MutableSet { val providersToAdd = mutableSetOf() if (state.showHistorySuggestions) { providersToAdd.add(historyStorageProvider) } if (state.showBookmarkSuggestions) { providersToAdd.add(bookmarksStorageSuggestionProvider) } if (state.showSearchSuggestions) { providersToAdd.addAll(getSelectedSearchSuggestionProvider(state)) } if (!isBrowsingModePrivate()) { providersToAdd.add(sessionProvider) } return providersToAdd } private fun getProvidersToRemove(state: SearchFragmentState): MutableSet { val providersToRemove = mutableSetOf() providersToRemove.add(shortcutsEnginePickerProvider) if (!state.showHistorySuggestions) { providersToRemove.add(historyStorageProvider) } if (!state.showBookmarkSuggestions) { providersToRemove.add(bookmarksStorageSuggestionProvider) } if (!state.showSearchSuggestions) { providersToRemove.addAll(getSelectedSearchSuggestionProvider(state)) } if (isBrowsingModePrivate()) { providersToRemove.add(sessionProvider) } return providersToRemove } private fun isBrowsingModePrivate(): Boolean { return (container.context.asActivity() as? HomeActivity)?.browsingModeManager?.mode?.isPrivate ?: false } private fun getSelectedSearchSuggestionProvider(state: SearchFragmentState): List { return when (state.searchEngineSource) { is SearchEngineSource.Default -> listOf( defaultSearchActionProvider, defaultSearchSuggestionProvider ) is SearchEngineSource.Shortcut -> getSuggestionProviderForEngine( state.searchEngineSource.searchEngine ) } } private fun handleDisplayShortcutsProviders() { view.removeAllProviders() providersInUse.clear() providersInUse.add(shortcutsEnginePickerProvider) view.addProviders(shortcutsEnginePickerProvider) } private fun getSuggestionProviderForEngine(engine: SearchEngine): List { return searchSuggestionProviderMap.getOrPut(engine) { val context = container.context val components = context.components val primaryTextColor = context.getColorFromAttr(R.attr.primaryText) val searchBitmap = getDrawable(context, R.drawable.ic_search)?.apply { colorFilter = createBlendModeColorFilterCompat(primaryTextColor, SRC_IN) }?.toBitmap() val engineForSpeculativeConnects = if (!isBrowsingModePrivate()) components.core.engine else null val searchEngine = components.search.provider.installedSearchEngines(context).list.find { it.name == engine.name } ?: components.search.provider.getDefaultEngine(context) listOf( SearchActionProvider( searchEngineGetter = suspend { searchEngine }, searchUseCase = shortcutSearchUseCase, icon = searchBitmap ), SearchSuggestionProvider( searchEngine, shortcutSearchUseCase, components.core.client, limit = 3, mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS, icon = searchBitmap, engine = engineForSpeculativeConnects, filterExactMatch = true ) ) } } }