Integrate new search code from Android Components into Fenix.
parent
2904ca8ac0
commit
2b759e9d6f
@ -1,37 +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.components
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.browser.search.SearchEngineManager
|
||||
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
|
||||
import org.mozilla.fenix.perf.lazyMonitored
|
||||
import org.mozilla.fenix.utils.Mockable
|
||||
|
||||
/**
|
||||
* Component group for all search engine integration related functionality.
|
||||
*/
|
||||
@Mockable
|
||||
class Search(private val context: Context) {
|
||||
val provider = FenixSearchEngineProvider(context)
|
||||
|
||||
/**
|
||||
* This component provides access to a centralized registry of search engines.
|
||||
*/
|
||||
val searchEngineManager by lazyMonitored {
|
||||
SearchEngineManager(
|
||||
coroutineContext = IO,
|
||||
providers = listOf(provider)
|
||||
).apply {
|
||||
registerForLocaleUpdates(context)
|
||||
GlobalScope.launch {
|
||||
defaultSearchEngine = provider.getDefaultEngine(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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.components.search
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import mozilla.components.browser.search.SearchEngineParser
|
||||
import mozilla.components.browser.state.search.SearchEngine
|
||||
import mozilla.components.feature.search.ext.migrate
|
||||
import mozilla.components.feature.search.middleware.SearchMiddleware
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.IOException
|
||||
|
||||
private const val PREF_FILE_SEARCH_ENGINES = "custom-search-engines"
|
||||
|
||||
private const val PREF_KEY_CUSTOM_SEARCH_ENGINES = "pref_custom_search_engines"
|
||||
private const val PREF_KEY_MIGRATED = "pref_search_migrated"
|
||||
|
||||
/**
|
||||
* Helper class to migrate the search related data in Fenix to the "Android Components" implementation.
|
||||
*/
|
||||
internal class SearchMigration(
|
||||
private val context: Context
|
||||
) : SearchMiddleware.Migration {
|
||||
|
||||
override fun getValuesToMigrate(): SearchMiddleware.Migration.MigrationValues? {
|
||||
val preferences = context.getSharedPreferences(PREF_FILE_SEARCH_ENGINES, Context.MODE_PRIVATE)
|
||||
if (preferences.getBoolean(PREF_KEY_MIGRATED, false)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val values = SearchMiddleware.Migration.MigrationValues(
|
||||
customSearchEngines = loadCustomSearchEngines(preferences),
|
||||
defaultSearchEngineName = context.components.settings.defaultSearchEngineName
|
||||
)
|
||||
|
||||
preferences.edit()
|
||||
.putBoolean(PREF_KEY_MIGRATED, true)
|
||||
.apply()
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
private fun loadCustomSearchEngines(
|
||||
preferences: SharedPreferences
|
||||
): List<SearchEngine> {
|
||||
val ids = preferences.getStringSet(PREF_KEY_CUSTOM_SEARCH_ENGINES, emptySet()) ?: emptySet()
|
||||
|
||||
val parser = SearchEngineParser()
|
||||
|
||||
return ids.mapNotNull { id ->
|
||||
val xml = preferences.getString(id, null)
|
||||
parser.loadSafely(id, xml?.byteInputStream()?.buffered())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SearchEngineParser.loadSafely(id: String, stream: BufferedInputStream?): SearchEngine? {
|
||||
return try {
|
||||
stream?.let { load(id, it).migrate() }
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
} catch (e: XmlPullParserException) {
|
||||
null
|
||||
}
|
||||
}
|
@ -1,129 +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.components.searchengine
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import mozilla.components.browser.icons.IconRequest
|
||||
import mozilla.components.browser.search.SearchEngine
|
||||
import mozilla.components.browser.search.SearchEngineParser
|
||||
import mozilla.components.browser.search.provider.SearchEngineList
|
||||
import mozilla.components.browser.search.provider.SearchEngineProvider
|
||||
import mozilla.components.support.ktx.android.content.PreferencesHolder
|
||||
import mozilla.components.support.ktx.android.content.stringSetPreference
|
||||
import org.mozilla.fenix.ext.components
|
||||
|
||||
/**
|
||||
* SearchEngineProvider implementation to load user entered custom search engines.
|
||||
*/
|
||||
class CustomSearchEngineProvider : SearchEngineProvider {
|
||||
override suspend fun loadSearchEngines(context: Context): SearchEngineList {
|
||||
return SearchEngineList(CustomSearchEngineStore.loadCustomSearchEngines(context), null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Object to handle storing custom search engines
|
||||
*/
|
||||
object CustomSearchEngineStore {
|
||||
class EngineNameAlreadyExists : Exception()
|
||||
|
||||
/**
|
||||
* Add a search engine to the store.
|
||||
* @param context [Context] used for various Android interactions.
|
||||
* @param engineName The name of the search engine
|
||||
* @param searchQuery The templated search string for the search engine
|
||||
* @throws EngineNameAlreadyExists if you try to add a search engine that already exists
|
||||
*/
|
||||
suspend fun addSearchEngine(context: Context, engineName: String, searchQuery: String) {
|
||||
val storage = engineStorage(context)
|
||||
if (storage.customSearchEngineIds.contains(engineName)) { throw EngineNameAlreadyExists() }
|
||||
|
||||
val icon = context.components.core.icons.loadIcon(IconRequest(searchQuery)).await()
|
||||
val searchEngineXml = SearchEngineWriter.buildSearchEngineXML(engineName, searchQuery, icon.bitmap)
|
||||
val engines = storage.customSearchEngineIds.toMutableSet()
|
||||
engines.add(engineName)
|
||||
storage.customSearchEngineIds = engines
|
||||
storage[engineName] = searchEngineXml
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing search engine.
|
||||
* To prevent duplicate search engines we want to remove the old engine before adding the new one
|
||||
* @param context [Context] used for various Android interactions.
|
||||
* @param oldEngineName the name of the engine you want to replace
|
||||
* @param newEngineName the name of the engine you want to save
|
||||
* @param searchQuery The templated search string for the search engine
|
||||
*/
|
||||
suspend fun updateSearchEngine(
|
||||
context: Context,
|
||||
oldEngineName: String,
|
||||
newEngineName: String,
|
||||
searchQuery: String
|
||||
) {
|
||||
removeSearchEngine(context, oldEngineName)
|
||||
addSearchEngine(context, newEngineName, searchQuery)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a search engine from the store
|
||||
* @param context [Context] used for various Android interactions.
|
||||
* @param engineId the id of the engine you want to remove
|
||||
*/
|
||||
fun removeSearchEngine(context: Context, engineId: String) {
|
||||
val storage = engineStorage(context)
|
||||
val customEngines = storage.customSearchEngineIds
|
||||
storage.customSearchEngineIds = customEngines.filterNot { it == engineId }.toSet()
|
||||
storage[engineId] = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the store to see if it contains a search engine
|
||||
* @param context [Context] used for various Android interactions.
|
||||
* @param engineId The name of the engine to check
|
||||
*/
|
||||
fun isCustomSearchEngine(context: Context, engineId: String): Boolean {
|
||||
val storage = engineStorage(context)
|
||||
return storage.customSearchEngineIds.contains(engineId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of [SearchEngine] from the store
|
||||
* @param context [Context] used for various Android interactions.
|
||||
*/
|
||||
fun loadCustomSearchEngines(context: Context): List<SearchEngine> {
|
||||
val storage = engineStorage(context)
|
||||
val parser = SearchEngineParser()
|
||||
val engines = storage.customSearchEngineIds
|
||||
|
||||
return engines.mapNotNull {
|
||||
val engineXml = storage[it] ?: return@mapNotNull null
|
||||
val engineInputStream = engineXml.byteInputStream().buffered()
|
||||
parser.load(it, engineInputStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a helper object to help interact with [SharedPreferences]
|
||||
* @param context [Context] used for various Android interactions.
|
||||
*/
|
||||
private fun engineStorage(context: Context) = object : PreferencesHolder {
|
||||
override val preferences: SharedPreferences
|
||||
get() = context.getSharedPreferences(PREF_FILE_SEARCH_ENGINES, Context.MODE_PRIVATE)
|
||||
|
||||
var customSearchEngineIds by stringSetPreference(PREF_KEY_CUSTOM_SEARCH_ENGINES, emptySet())
|
||||
|
||||
operator fun get(engineId: String): String? {
|
||||
return preferences.getString(engineId, null)
|
||||
}
|
||||
|
||||
operator fun set(engineId: String, value: String?) {
|
||||
preferences.edit().putString(engineId, value).apply()
|
||||
}
|
||||
}
|
||||
|
||||
private const val PREF_KEY_CUSTOM_SEARCH_ENGINES = "pref_custom_search_engines"
|
||||
const val PREF_FILE_SEARCH_ENGINES = "custom-search-engines"
|
||||
}
|
@ -1,306 +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.components.searchengine
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.browser.search.SearchEngine
|
||||
import mozilla.components.browser.search.provider.AssetsSearchEngineProvider
|
||||
import mozilla.components.browser.search.provider.SearchEngineList
|
||||
import mozilla.components.browser.search.provider.SearchEngineProvider
|
||||
import mozilla.components.browser.search.provider.filter.SearchEngineFilter
|
||||
import mozilla.components.browser.search.provider.localization.LocaleSearchLocalizationProvider
|
||||
import mozilla.components.browser.search.provider.localization.SearchLocalizationProvider
|
||||
import mozilla.components.service.location.LocationService
|
||||
import mozilla.components.service.location.MozillaLocationService
|
||||
import mozilla.components.service.location.search.RegionSearchLocalizationProvider
|
||||
import org.mozilla.fenix.BuildConfig
|
||||
import org.mozilla.fenix.Config
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.perf.runBlockingIncrement
|
||||
import java.util.Locale
|
||||
|
||||
@SuppressWarnings("TooManyFunctions")
|
||||
open class FenixSearchEngineProvider(
|
||||
private val context: Context
|
||||
) : SearchEngineProvider, CoroutineScope by CoroutineScope(Job() + Dispatchers.IO) {
|
||||
private val shouldMockMLS = Config.channel.isDebug || BuildConfig.MLS_TOKEN.isEmpty()
|
||||
private val locationService: LocationService = if (shouldMockMLS) {
|
||||
LocationService.dummy()
|
||||
} else {
|
||||
MozillaLocationService(
|
||||
context,
|
||||
context.components.core.client,
|
||||
BuildConfig.MLS_TOKEN
|
||||
)
|
||||
}
|
||||
|
||||
// We have two search engine types: one based on MLS reported region, one based only on Locale.
|
||||
// There are multiple steps involved in returning the default search engine for example.
|
||||
// Simplest and most effective way to make sure the MLS engines do not mix with Locale based engines
|
||||
// is to use the same type of engines for the entire duration of the app's run.
|
||||
// See fenix/issues/11875
|
||||
private val isRegionCachedByLocationService = locationService.hasRegionCached()
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
open val localizationProvider: SearchLocalizationProvider =
|
||||
RegionSearchLocalizationProvider(locationService)
|
||||
|
||||
/**
|
||||
* Unfiltered list of search engines based on locale.
|
||||
*/
|
||||
open var baseSearchEngines = async {
|
||||
AssetsSearchEngineProvider(localizationProvider)
|
||||
.loadSearchEngines(context)
|
||||
}
|
||||
|
||||
private val loadedRegion = async { localizationProvider.determineRegion() }
|
||||
|
||||
// https://github.com/mozilla-mobile/fenix/issues/9935
|
||||
// Adds a Locale search engine provider as a fallback in case the MLS lookup takes longer
|
||||
// than the time it takes for a user to try to search.
|
||||
private val fallbackLocationService: SearchLocalizationProvider = LocaleSearchLocalizationProvider()
|
||||
private val fallBackProvider =
|
||||
AssetsSearchEngineProvider(fallbackLocationService)
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
open val fallbackEngines = async { fallBackProvider.loadSearchEngines(context) }
|
||||
private val fallbackRegion = async { fallbackLocationService.determineRegion() }
|
||||
|
||||
/**
|
||||
* Default bundled search engines based on locale.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
open val bundledSearchEngines = async {
|
||||
val defaultEngineIdentifiers =
|
||||
baseSearchEngines.await().list.map { it.identifier }.toSet()
|
||||
AssetsSearchEngineProvider(
|
||||
localizationProvider,
|
||||
filters = listOf(object : SearchEngineFilter {
|
||||
override fun filter(context: Context, searchEngine: SearchEngine): Boolean {
|
||||
return BUNDLED_SEARCH_ENGINES.contains(searchEngine.identifier) &&
|
||||
!defaultEngineIdentifiers.contains(searchEngine.identifier)
|
||||
}
|
||||
}),
|
||||
additionalIdentifiers = BUNDLED_SEARCH_ENGINES
|
||||
).loadSearchEngines(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search engines that have been manually added by a user.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
open var customSearchEngines = async {
|
||||
CustomSearchEngineProvider().loadSearchEngines(context)
|
||||
}
|
||||
|
||||
private var loadedSearchEngines = refreshInstalledEngineListAsync(baseSearchEngines)
|
||||
|
||||
// https://github.com/mozilla-mobile/fenix/issues/9935
|
||||
// Create new getter that will return the fallback SearchEngineList if
|
||||
// the main one hasn't completed yet
|
||||
private val searchEngines: Deferred<SearchEngineList>
|
||||
get() =
|
||||
if (isRegionCachedByLocationService) {
|
||||
loadedSearchEngines
|
||||
} else {
|
||||
refreshInstalledEngineListAsync(fallbackEngines)
|
||||
}
|
||||
|
||||
fun getDefaultEngine(context: Context): SearchEngine {
|
||||
val engines = installedSearchEngines(context)
|
||||
val selectedName = context.settings().defaultSearchEngineName
|
||||
|
||||
return engines.list.find { it.name == selectedName }
|
||||
?: engines.default
|
||||
?: engines.list.first()
|
||||
}
|
||||
|
||||
// We should only be setting the default search engine here
|
||||
fun setDefaultEngine(context: Context, id: String) {
|
||||
val engines = installedSearchEngines(context)
|
||||
val newDefault = engines.list.find { it.name == id }
|
||||
?: engines.default
|
||||
?: engines.list.first()
|
||||
|
||||
context.settings().defaultSearchEngineName = newDefault.name
|
||||
context.components.search.searchEngineManager.defaultSearchEngine = newDefault
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a list of all SearchEngines that are currently active. These are the engines that
|
||||
* are readily available throughout the app. Includes all installed engines, both
|
||||
* default and custom
|
||||
*/
|
||||
fun installedSearchEngines(context: Context): SearchEngineList = runBlockingIncrement {
|
||||
val installedIdentifiers = installedSearchEngineIdentifiers(context)
|
||||
val defaultList = searchEngines.await()
|
||||
|
||||
defaultList.copy(
|
||||
list = defaultList.list.filter {
|
||||
installedIdentifiers.contains(it.identifier)
|
||||
}.sortedBy {
|
||||
it.name.toLowerCase(Locale.getDefault())
|
||||
},
|
||||
default = defaultList.default?.let {
|
||||
if (installedIdentifiers.contains(it.identifier)) {
|
||||
it
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun allSearchEngineIdentifiers() = runBlockingIncrement {
|
||||
loadedSearchEngines.await().list.map { it.identifier }
|
||||
}
|
||||
|
||||
fun uninstalledSearchEngines(context: Context): SearchEngineList = runBlockingIncrement {
|
||||
val installedIdentifiers = installedSearchEngineIdentifiers(context)
|
||||
val engineList = loadedSearchEngines.await()
|
||||
|
||||
return@runBlockingIncrement engineList.copy(
|
||||
list = engineList.list.filterNot { installedIdentifiers.contains(it.identifier) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun loadSearchEngines(context: Context): SearchEngineList {
|
||||
return installedSearchEngines(context)
|
||||
}
|
||||
|
||||
fun installSearchEngine(
|
||||
context: Context,
|
||||
searchEngine: SearchEngine,
|
||||
isCustom: Boolean = false
|
||||
) = runBlockingIncrement {
|
||||
if (isCustom) {
|
||||
val searchUrl = searchEngine.getSearchTemplate()
|
||||
CustomSearchEngineStore.addSearchEngine(context, searchEngine.name, searchUrl)
|
||||
reload()
|
||||
} else {
|
||||
val installedIdentifiers = installedSearchEngineIdentifiers(context).toMutableSet()
|
||||
installedIdentifiers.add(searchEngine.identifier)
|
||||
prefs(context).edit()
|
||||
.putStringSet(
|
||||
localeAwareInstalledEnginesKey(), installedIdentifiers
|
||||
).apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstallSearchEngine(
|
||||
context: Context,
|
||||
searchEngine: SearchEngine,
|
||||
isCustom: Boolean = false
|
||||
) = runBlockingIncrement {
|
||||
if (isCustom) {
|
||||
CustomSearchEngineStore.removeSearchEngine(context, searchEngine.identifier)
|
||||
reload()
|
||||
} else {
|
||||
val installedIdentifiers = installedSearchEngineIdentifiers(context).toMutableSet()
|
||||
installedIdentifiers.remove(searchEngine.identifier)
|
||||
prefs(context).edit().putStringSet(
|
||||
localeAwareInstalledEnginesKey(),
|
||||
installedIdentifiers
|
||||
).apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
launch {
|
||||
customSearchEngines = async { CustomSearchEngineProvider().loadSearchEngines(context) }
|
||||
loadedSearchEngines = refreshInstalledEngineListAsync(baseSearchEngines)
|
||||
}
|
||||
}
|
||||
|
||||
// When we change the locale we need to update the baseSearchEngines list
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
open fun updateBaseSearchEngines() {
|
||||
baseSearchEngines = async {
|
||||
AssetsSearchEngineProvider(localizationProvider)
|
||||
.loadSearchEngines(context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshInstalledEngineListAsync(
|
||||
engines: Deferred<SearchEngineList>
|
||||
): Deferred<SearchEngineList> = async {
|
||||
val engineList = engines.await()
|
||||
val bundledList = bundledSearchEngines.await().list
|
||||
val customList = customSearchEngines.await().list
|
||||
|
||||
return@async engineList.copy(list = engineList.list + bundledList + customList)
|
||||
}
|
||||
|
||||
private fun prefs(context: Context) = context.getSharedPreferences(
|
||||
PREF_FILE_SEARCH_ENGINES,
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
suspend fun installedSearchEngineIdentifiers(context: Context): Set<String> {
|
||||
val prefs = prefs(context)
|
||||
val installedEnginesKey = localeAwareInstalledEnginesKey()
|
||||
|
||||
if (!prefs.contains(installedEnginesKey)) {
|
||||
val searchEngines =
|
||||
if (isRegionCachedByLocationService) {
|
||||
baseSearchEngines
|
||||
} else {
|
||||
fallbackEngines
|
||||
}
|
||||
|
||||
val defaultSet = searchEngines.await()
|
||||
.list
|
||||
.map { it.identifier }
|
||||
.toSet()
|
||||
|
||||
prefs.edit().putStringSet(installedEnginesKey, defaultSet).apply()
|
||||
}
|
||||
|
||||
val installedIdentifiers: Set<String> =
|
||||
prefs(context).getStringSet(installedEnginesKey, setOf()) ?: setOf()
|
||||
|
||||
val customEngineIdentifiers =
|
||||
customSearchEngines.await().list.map { it.identifier }.toSet()
|
||||
|
||||
return installedIdentifiers + customEngineIdentifiers
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
suspend fun localeAwareInstalledEnginesKey(): String {
|
||||
val tag = if (isRegionCachedByLocationService) {
|
||||
val localization = loadedRegion.await()
|
||||
val region = localization.region?.let {
|
||||
if (it.isEmpty()) "" else "-$it"
|
||||
}
|
||||
|
||||
"${localization.languageTag}$region"
|
||||
} else {
|
||||
val localization = fallbackRegion.await()
|
||||
val region = localization.region?.let {
|
||||
if (it.isEmpty()) "" else "-$it"
|
||||
}
|
||||
|
||||
"${localization.languageTag}$region-fallback"
|
||||
}
|
||||
|
||||
return "$INSTALLED_ENGINES_KEY-$tag"
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
companion object {
|
||||
val BUNDLED_SEARCH_ENGINES = listOf("reddit", "youtube")
|
||||
const val PREF_FILE_SEARCH_ENGINES = "fenix-search-engine-provider"
|
||||
const val INSTALLED_ENGINES_KEY = "fenix-installed-search-engines"
|
||||
}
|
||||
}
|
@ -1,84 +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.components.searchengine
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import org.w3c.dom.Document
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.StringWriter
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.parsers.ParserConfigurationException
|
||||
import javax.xml.transform.OutputKeys
|
||||
import javax.xml.transform.TransformerConfigurationException
|
||||
import javax.xml.transform.TransformerException
|
||||
import javax.xml.transform.TransformerFactory
|
||||
import javax.xml.transform.dom.DOMSource
|
||||
import javax.xml.transform.stream.StreamResult
|
||||
|
||||
private const val BITMAP_COMPRESS_QUALITY = 100
|
||||
private fun Bitmap.toBase64(): String {
|
||||
val stream = ByteArrayOutputStream()
|
||||
compress(Bitmap.CompressFormat.PNG, BITMAP_COMPRESS_QUALITY, stream)
|
||||
val encodedImage = Base64.encodeToString(stream.toByteArray(), Base64.DEFAULT)
|
||||
return "data:image/png;base64,$encodedImage"
|
||||
}
|
||||
|
||||
class SearchEngineWriter {
|
||||
companion object {
|
||||
private const val LOG_TAG = "SearchEngineWriter"
|
||||
|
||||
fun buildSearchEngineXML(engineName: String, searchQuery: String, iconBitmap: Bitmap): String? {
|
||||
try {
|
||||
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
|
||||
val rootElement = document!!.createElement("OpenSearchDescription")
|
||||
rootElement.setAttribute("xmlns", "http://a9.com/-/spec/opensearch/1.1/")
|
||||
document.appendChild(rootElement)
|
||||
|
||||
val shortNameElement = document.createElement("ShortName")
|
||||
shortNameElement.textContent = engineName
|
||||
rootElement.appendChild(shortNameElement)
|
||||
|
||||
val imageElement = document.createElement("Image")
|
||||
imageElement.setAttribute("width", "16")
|
||||
imageElement.setAttribute("height", "16")
|
||||
imageElement.textContent = iconBitmap.toBase64()
|
||||
rootElement.appendChild(imageElement)
|
||||
|
||||
val descriptionElement = document.createElement("Description")
|
||||
descriptionElement.textContent = engineName
|
||||
rootElement.appendChild(descriptionElement)
|
||||
|
||||
val urlElement = document.createElement("Url")
|
||||
urlElement.setAttribute("type", "text/html")
|
||||
|
||||
val templateSearchString = searchQuery.replace("%s", "{searchTerms}")
|
||||
urlElement.setAttribute("template", templateSearchString)
|
||||
rootElement.appendChild(urlElement)
|
||||
|
||||
return xmlToString(document)
|
||||
} catch (e: ParserConfigurationException) {
|
||||
Log.e(LOG_TAG, "Couldn't create new Document for building search engine XML", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun xmlToString(doc: Document): String? {
|
||||
val writer = StringWriter()
|
||||
try {
|
||||
val tf = TransformerFactory.newInstance().newTransformer()
|
||||
tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8")
|
||||
tf.transform(DOMSource(doc), StreamResult(writer))
|
||||
} catch (e: TransformerConfigurationException) {
|
||||
return null
|
||||
} catch (e: TransformerException) {
|
||||
return null
|
||||
}
|
||||
|
||||
return writer.toString()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +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.ext
|
||||
|
||||
import android.content.Context
|
||||
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
|
||||
|
||||
private const val MINIMUM_SEARCH_ENGINES_NUMBER_TO_SHOW_SHORTCUTS = 2
|
||||
|
||||
/**
|
||||
* Return if the user has *at least 2* installed search engines.
|
||||
* Useful to decide whether to show / enable certain functionalities.
|
||||
*/
|
||||
fun FenixSearchEngineProvider.areShortcutsAvailable(context: Context) =
|
||||
installedSearchEngines(context).list.size >= MINIMUM_SEARCH_ENGINES_NUMBER_TO_SHOW_SHORTCUTS
|
@ -1,242 +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.settings.search
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RadioGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import kotlinx.android.synthetic.main.search_engine_radio_button.view.engine_icon
|
||||
import kotlinx.android.synthetic.main.search_engine_radio_button.view.engine_text
|
||||
import kotlinx.android.synthetic.main.search_engine_radio_button.view.overflow_menu
|
||||
import kotlinx.android.synthetic.main.search_engine_radio_button.view.radio_button
|
||||
import kotlinx.coroutines.MainScope
|
||||
import mozilla.components.browser.search.SearchEngine
|
||||
import mozilla.components.browser.search.provider.SearchEngineList
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.getRootView
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.utils.allowUndo
|
||||
import java.util.Locale
|
||||
|
||||
abstract class SearchEngineListPreference @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = android.R.attr.preferenceStyle
|
||||
) : Preference(context, attrs, defStyleAttr), CompoundButton.OnCheckedChangeListener {
|
||||
|
||||
protected lateinit var searchEngineList: SearchEngineList
|
||||
protected var searchEngineGroup: RadioGroup? = null
|
||||
|
||||
protected abstract val itemResId: Int
|
||||
|
||||
init {
|
||||
layoutResource = R.layout.preference_search_engine_chooser
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
|
||||
super.onBindViewHolder(holder)
|
||||
searchEngineGroup = holder!!.itemView.findViewById(R.id.search_engine_group)
|
||||
reload(searchEngineGroup!!.context)
|
||||
}
|
||||
|
||||
fun reload(context: Context) {
|
||||
searchEngineList = context.components.search.provider.installedSearchEngines(context)
|
||||
refreshSearchEngineViews(context)
|
||||
}
|
||||
|
||||
protected abstract fun onSearchEngineSelected(searchEngine: SearchEngine)
|
||||
protected abstract fun updateDefaultItem(defaultButton: CompoundButton)
|
||||
|
||||
private fun refreshSearchEngineViews(context: Context) {
|
||||
if (searchEngineGroup == null) {
|
||||
// We want to refresh the search engine list of this preference in onResume,
|
||||
// but the first time this preference is created onResume is called before onCreateView
|
||||
// so searchEngineGroup is not set yet.
|
||||
return
|
||||
}
|
||||
|
||||
val defaultEngineId = context.components.search.provider.getDefaultEngine(context).identifier
|
||||
|
||||
val selectedEngine = (searchEngineList.list.find {
|
||||
it.identifier == defaultEngineId
|
||||
} ?: searchEngineList.list.first()).identifier
|
||||
|
||||
// set the search engine manager default
|
||||
context.components.search.provider.setDefaultEngine(context, selectedEngine)
|
||||
|
||||
searchEngineGroup!!.removeAllViews()
|
||||
|
||||
val layoutInflater = LayoutInflater.from(context)
|
||||
val layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
|
||||
val setupSearchEngineItem: (Int, SearchEngine) -> Unit = { index, engine ->
|
||||
val engineId = engine.identifier
|
||||
val engineItem = makeButtonFromSearchEngine(
|
||||
engine = engine,
|
||||
layoutInflater = layoutInflater,
|
||||
res = context.resources,
|
||||
allowDeletion = searchEngineList.list.size > 1
|
||||
)
|
||||
|
||||
engineItem.id = index + (searchEngineList.default?.let { 1 } ?: 0)
|
||||
engineItem.tag = engineId
|
||||
if (engineId == selectedEngine) {
|
||||
updateDefaultItem(engineItem.radio_button)
|
||||
/* #11465 -> radio_button.isChecked = true does not trigger
|
||||
* onSearchEngineSelected because searchEngineGroup has null views at that point.
|
||||
* So we trigger it here.*/
|
||||
onSearchEngineSelected(engine)
|
||||
}
|
||||
searchEngineGroup!!.addView(engineItem, layoutParams)
|
||||
}
|
||||
|
||||
searchEngineList.default?.apply {
|
||||
setupSearchEngineItem(0, this)
|
||||
}
|
||||
|
||||
searchEngineList.list
|
||||
.filter { it.identifier != searchEngineList.default?.identifier }
|
||||
.sortedBy { it.name.toLowerCase(Locale.getDefault()) }
|
||||
.forEachIndexed(setupSearchEngineItem)
|
||||
}
|
||||
|
||||
private fun makeButtonFromSearchEngine(
|
||||
engine: SearchEngine,
|
||||
layoutInflater: LayoutInflater,
|
||||
res: Resources,
|
||||
allowDeletion: Boolean
|
||||
): View {
|
||||
val isCustomSearchEngine =
|
||||
CustomSearchEngineStore.isCustomSearchEngine(context, engine.identifier)
|
||||
|
||||
val wrapper = layoutInflater.inflate(itemResId, null) as LinearLayout
|
||||
wrapper.setOnClickListener { wrapper.radio_button.isChecked = true }
|
||||
wrapper.radio_button.setOnCheckedChangeListener(this)
|
||||
wrapper.engine_text.text = engine.name
|
||||
wrapper.overflow_menu.isVisible = allowDeletion || isCustomSearchEngine
|
||||
wrapper.overflow_menu.setOnClickListener {
|
||||
SearchEngineMenu(
|
||||
context = context,
|
||||
allowDeletion = allowDeletion,
|
||||
isCustomSearchEngine = isCustomSearchEngine,
|
||||
onItemTapped = {
|
||||
when (it) {
|
||||
is SearchEngineMenu.Item.Edit -> editCustomSearchEngine(engine)
|
||||
is SearchEngineMenu.Item.Delete -> deleteSearchEngine(
|
||||
context,
|
||||
engine,
|
||||
isCustomSearchEngine
|
||||
)
|
||||
}
|
||||
}
|
||||
).menuBuilder.build(context).show(wrapper.overflow_menu)
|
||||
}
|
||||
val iconSize = res.getDimension(R.dimen.preference_icon_drawable_size).toInt()
|
||||
val engineIcon = BitmapDrawable(res, engine.icon)
|
||||
engineIcon.setBounds(0, 0, iconSize, iconSize)
|
||||
wrapper.engine_icon.setImageDrawable(engineIcon)
|
||||
return wrapper
|
||||
}
|
||||
|
||||
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
||||
searchEngineList.list.forEach { engine ->
|
||||
val wrapper: LinearLayout =
|
||||
searchEngineGroup?.findViewWithTag(engine.identifier) ?: return
|
||||
|
||||
when (wrapper.radio_button == buttonView) {
|
||||
true -> onSearchEngineSelected(engine)
|
||||
false -> {
|
||||
wrapper.radio_button.setOnCheckedChangeListener(null)
|
||||
wrapper.radio_button.isChecked = false
|
||||
wrapper.radio_button.setOnCheckedChangeListener(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun editCustomSearchEngine(engine: SearchEngine) {
|
||||
val wasDefault = context.components.search.provider.getDefaultEngine(context).identifier == engine.identifier
|
||||
val directions = SearchEngineFragmentDirections
|
||||
.actionSearchEngineFragmentToEditCustomSearchEngineFragment(engine.identifier, wasDefault)
|
||||
Navigation.findNavController(searchEngineGroup!!).navigate(directions)
|
||||
}
|
||||
|
||||
private fun deleteSearchEngine(
|
||||
context: Context,
|
||||
engine: SearchEngine,
|
||||
isCustomSearchEngine: Boolean
|
||||
) {
|
||||
val isDefaultEngine = engine == context.components.search.provider.getDefaultEngine(context)
|
||||
val initialEngineList = searchEngineList.copy()
|
||||
val initialDefaultEngine = searchEngineList.default
|
||||
|
||||
context.components.search.provider.uninstallSearchEngine(
|
||||
context,
|
||||
engine,
|
||||
isCustomSearchEngine
|
||||
)
|
||||
|
||||
MainScope().allowUndo(
|
||||
view = context.getRootView()!!,
|
||||
message = context
|
||||
.getString(R.string.search_delete_search_engine_success_message, engine.name),
|
||||
undoActionTitle = context.getString(R.string.snackbar_deleted_undo),
|
||||
onCancel = {
|
||||
context.components.search.provider.installSearchEngine(
|
||||
context,
|
||||
engine,
|
||||
isCustomSearchEngine
|
||||
)
|
||||
|
||||
searchEngineList = initialEngineList.copy(
|
||||
default = initialDefaultEngine
|
||||
)
|
||||
|
||||
refreshSearchEngineViews(context)
|
||||
},
|
||||
operation = {
|
||||
if (isDefaultEngine) {
|
||||
val default = context.components.search.provider.getDefaultEngine(context)
|
||||
context.components.search.provider.setDefaultEngine(context, default.identifier)
|
||||
context.settings().defaultSearchEngineName = default.name
|
||||
}
|
||||
if (isCustomSearchEngine) {
|
||||
context.components.analytics.metrics.track(Event.CustomEngineDeleted)
|
||||
}
|
||||
refreshSearchEngineViews(context)
|
||||
}
|
||||
)
|
||||
|
||||
searchEngineList = searchEngineList.copy(
|
||||
list = searchEngineList.list.filter {
|
||||
it.identifier != engine.identifier
|
||||
},
|
||||
default = if (searchEngineList.default?.identifier == engine.identifier) {
|
||||
null
|
||||
} else {
|
||||
searchEngineList.default
|
||||
}
|
||||
)
|
||||
|
||||
refreshSearchEngineViews(context)
|
||||
}
|
||||
}
|
@ -1,207 +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.components.metrics
|
||||
|
||||
import mozilla.components.browser.search.SearchEngine
|
||||
import mozilla.components.browser.search.SearchEngineManager
|
||||
import mozilla.components.browser.search.provider.AssetsSearchEngineProvider
|
||||
import mozilla.components.browser.search.provider.localization.LocaleSearchLocalizationProvider
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.components.metrics.Event.PerformedSearch
|
||||
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.EngineSource
|
||||
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.EventSource
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class PerformedSearchTest {
|
||||
private lateinit var searchEngines: List<SearchEngine>
|
||||
|
||||
// Match against the Regex defined at
|
||||
// https://github.com/mozilla-mobile/android-components/blob/master/components/service/glean/src/main/java/mozilla/components/service/glean/private/LabeledMetricType.kt#L43
|
||||
// We're temporarily using it until better Glean testing APIs are available.
|
||||
private val countLabelRegex = Regex("^[a-z_][a-z0-9_-]{0,29}(\\.[a-z0-9_-]{0,29})*$")
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
searchEngines = SearchEngineManager(listOf(provider)).getSearchEngines(testContext)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatCountLabelIsValid() {
|
||||
val labels = searchEngines.map {
|
||||
PerformedSearch(EventSource.Action(EngineSource.Shortcut(it, false))).eventSource.countLabel
|
||||
}
|
||||
|
||||
labels.forEach {
|
||||
assertTrue("$it does not match!", it.matches(countLabelRegex))
|
||||
}
|
||||
}
|
||||
|
||||
private val provider = AssetsSearchEngineProvider(
|
||||
localizationProvider = LocaleSearchLocalizationProvider(),
|
||||
additionalIdentifiers = listOf(
|
||||
"amazon-au",
|
||||
"amazon-br",
|
||||
"amazon-ca",
|
||||
"amazon-co-uk",
|
||||
"amazon-de",
|
||||
"amazon-fr",
|
||||
"amazon-in",
|
||||
"amazon-it",
|
||||
"amazon-jp",
|
||||
"amazon-mx",
|
||||
"amazon-nl",
|
||||
"amazondotcom",
|
||||
"azerdict",
|
||||
"azet-sk",
|
||||
"baidu",
|
||||
"bing",
|
||||
"bolcom-fy-NL",
|
||||
"bolcom-nl",
|
||||
"ceneje",
|
||||
"coccoc",
|
||||
"danawa-kr",
|
||||
"daum-kr",
|
||||
"ddg",
|
||||
"diec2",
|
||||
"drae",
|
||||
"duckduckgo",
|
||||
"elebila",
|
||||
"faclair-beag",
|
||||
"google-2018",
|
||||
"google-b-1-m",
|
||||
"google-b-m",
|
||||
"google",
|
||||
"gulesider-mobile-NO",
|
||||
"heureka-cz",
|
||||
"hotline-ua",
|
||||
"leit-is",
|
||||
"leo_ende_de",
|
||||
"list-am",
|
||||
"mapy-cz",
|
||||
"mercadolibre-ar",
|
||||
"mercadolibre-cl",
|
||||
"mercadolibre-mx",
|
||||
"naver-kr",
|
||||
"odpiralni",
|
||||
"pazaruvaj",
|
||||
"pledarigrond",
|
||||
"prisjakt-sv-SE",
|
||||
"qwant",
|
||||
"rediff",
|
||||
"reta-vortaro",
|
||||
"salidzinilv",
|
||||
"seznam-cz",
|
||||
"skroutz",
|
||||
"slovnik-sk",
|
||||
"sslv",
|
||||
"sztaki-en-hu",
|
||||
"taobao",
|
||||
"tearma",
|
||||
"twitter-ja",
|
||||
"twitter",
|
||||
"vatera",
|
||||
"wikipedia-NN",
|
||||
"wikipedia-NO",
|
||||
"wikipedia-an",
|
||||
"wikipedia-ar",
|
||||
"wikipedia-as",
|
||||
"wikipedia-ast",
|
||||
"wikipedia-az",
|
||||
"wikipedia-be",
|
||||
"wikipedia-bg",
|
||||
"wikipedia-bn",
|
||||
"wikipedia-br",
|
||||
"wikipedia-bs",
|
||||
"wikipedia-ca",
|
||||
"wikipedia-cy",
|
||||
"wikipedia-cz",
|
||||
"wikipedia-da",
|
||||
"wikipedia-de",
|
||||
"wikipedia-dsb",
|
||||
"wikipedia-el",
|
||||
"wikipedia-eo",
|
||||
"wikipedia-es",
|
||||
"wikipedia-et",
|
||||
"wikipedia-eu",
|
||||
"wikipedia-fa",
|
||||
"wikipedia-fi",
|
||||
"wikipedia-fr",
|
||||
"wikipedia-fy-NL",
|
||||
"wikipedia-ga-IE",
|
||||
"wikipedia-gd",
|
||||
"wikipedia-gl",
|
||||
"wikipedia-gn",
|
||||
"wikipedia-gu",
|
||||
"wikipedia-he",
|
||||
"wikipedia-hi",
|
||||
"wikipedia-hr",
|
||||
"wikipedia-hsb",
|
||||
"wikipedia-hu",
|
||||
"wikipedia-hy-AM",
|
||||
"wikipedia-ia",
|
||||
"wikipedia-id",
|
||||
"wikipedia-is",
|
||||
"wikipedia-it",
|
||||
"wikipedia-ja",
|
||||
"wikipedia-ka",
|
||||
"wikipedia-kab",
|
||||
"wikipedia-kk",
|
||||
"wikipedia-km",
|
||||
"wikipedia-kn",
|
||||
"wikipedia-lij",
|
||||
"wikipedia-lo",
|
||||
"wikipedia-lt",
|
||||
"wikipedia-ltg",
|
||||
"wikipedia-lv",
|
||||
"wikipedia-ml",
|
||||
"wikipedia-mr",
|
||||
"wikipedia-ms",
|
||||
"wikipedia-my",
|
||||
"wikipedia-ne",
|
||||
"wikipedia-nl",
|
||||
"wikipedia-oc",
|
||||
"wikipedia-or",
|
||||
"wikipedia-pa",
|
||||
"wikipedia-pl",
|
||||
"wikipedia-pt",
|
||||
"wikipedia-rm",
|
||||
"wikipedia-ro",
|
||||
"wikipedia-ru",
|
||||
"wikipedia-sk",
|
||||
"wikipedia-sl",
|
||||
"wikipedia-sq",
|
||||
"wikipedia-sr",
|
||||
"wikipedia-sv-SE",
|
||||
"wikipedia-ta",
|
||||
"wikipedia-te",
|
||||
"wikipedia-th",
|
||||
"wikipedia-tr",
|
||||
"wikipedia-uk",
|
||||
"wikipedia-ur",
|
||||
"wikipedia-uz",
|
||||
"wikipedia-vi",
|
||||
"wikipedia-wo",
|
||||
"wikipedia-zh-CN",
|
||||
"wikipedia-zh-TW",
|
||||
"wikipedia",
|
||||
"wiktionary-kn",
|
||||
"wiktionary-oc",
|
||||
"wiktionary-or",
|
||||
"wiktionary-ta",
|
||||
"wiktionary-te",
|
||||
"yahoo-jp",
|
||||
"yandex-en",
|
||||
"yandex-ru",
|
||||
"yandex-tr",
|
||||
"yandex.by",
|
||||
"yandex"
|
||||
)
|
||||
)
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
package org.mozilla.fenix.components.searchengine
|
||||
|
||||
import android.content.Context
|
||||
import io.mockk.Runs
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import mozilla.components.browser.search.SearchEngine
|
||||
import mozilla.components.browser.search.provider.SearchEngineList
|
||||
import mozilla.components.browser.search.provider.localization.LocaleSearchLocalizationProvider
|
||||
import mozilla.components.browser.search.provider.localization.SearchLocalizationProvider
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class FenixSearchEngineProviderTest {
|
||||
|
||||
private lateinit var fenixSearchEngineProvider: FenixSearchEngineProvider
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
fenixSearchEngineProvider = FakeFenixSearchEngineProvider(testContext)
|
||||
mockkObject(CustomSearchEngineStore)
|
||||
fenixSearchEngineProvider.let {
|
||||
every { CustomSearchEngineStore.loadCustomSearchEngines(testContext) } returns listOf(
|
||||
(it as FakeFenixSearchEngineProvider)
|
||||
.mockSearchEngine("my custom site", "my custom site")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
TODO TEST:
|
||||
- public API happy path
|
||||
- list ordering
|
||||
- deduping
|
||||
- the above after adding/removing
|
||||
*/
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Test
|
||||
fun `add custom engine`() = runBlockingTest {
|
||||
val engineName = "Ecosia"
|
||||
val engineQuery = "www.ecosia.com/%s"
|
||||
val searchEngine: SearchEngine = mockk(relaxed = true)
|
||||
every { searchEngine.getSearchTemplate() } returns engineQuery
|
||||
every { searchEngine.name } returns engineName
|
||||
mockkObject(CustomSearchEngineStore)
|
||||
coEvery {
|
||||
CustomSearchEngineStore.addSearchEngine(
|
||||
testContext,
|
||||
engineName,
|
||||
engineQuery
|
||||
)
|
||||
} just Runs
|
||||
|
||||
fenixSearchEngineProvider.installSearchEngine(testContext, searchEngine, true)
|
||||
|
||||
coVerify { CustomSearchEngineStore.addSearchEngine(testContext, engineName, engineQuery) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN sharedprefs does not contain installed engines WHEN installedSearchEngineIdentifiers THEN defaultEngines + customEngines ids are returned`() = runBlockingTest {
|
||||
val expectedDefaults = fenixSearchEngineProvider.baseSearchEngines.toIdSet()
|
||||
val expectedCustom = fenixSearchEngineProvider.customSearchEngines.toIdSet()
|
||||
val expected = expectedDefaults + expectedCustom
|
||||
|
||||
val actual = fenixSearchEngineProvider.installedSearchEngineIdentifiers(testContext)
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN sharedprefs contains installed engines WHEN installedSearchEngineIdentifiers THEN defaultEngines + customEngines ids are returned`() = runBlockingTest {
|
||||
val sp = testContext.getSharedPreferences(
|
||||
FenixSearchEngineProvider.PREF_FILE_SEARCH_ENGINES,
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
sp.edit().putStringSet(
|
||||
fenixSearchEngineProvider.localeAwareInstalledEnginesKey(),
|
||||
persistedInstalledEngines
|
||||
).apply()
|
||||
|
||||
val expectedStored = persistedInstalledEngines
|
||||
val expectedCustom = fenixSearchEngineProvider.customSearchEngines.toIdSet()
|
||||
val expected = expectedStored + expectedCustom
|
||||
|
||||
val actual = fenixSearchEngineProvider.installedSearchEngineIdentifiers(testContext)
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Deferred<SearchEngineList>.toIdSet() =
|
||||
await().list.map { it.identifier }.toSet()
|
||||
|
||||
private val persistedInstalledEngines = setOf("bing", "ecosia")
|
||||
|
||||
class FakeFenixSearchEngineProvider(context: Context) : FenixSearchEngineProvider(context) {
|
||||
override val localizationProvider: SearchLocalizationProvider
|
||||
get() = LocaleSearchLocalizationProvider()
|
||||
|
||||
override var baseSearchEngines: Deferred<SearchEngineList>
|
||||
set(_) { throw NotImplementedError("Setting not currently supported on this fake") }
|
||||
get() {
|
||||
val google = mockSearchEngine(id = "google-b-1-m", n = "Google")
|
||||
|
||||
return CompletableDeferred(
|
||||
SearchEngineList(
|
||||
listOf(
|
||||
google,
|
||||
mockSearchEngine("bing", "Bing"),
|
||||
mockSearchEngine("amazondotcom", "Amazon.com")
|
||||
), default = google
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override val fallbackEngines: Deferred<SearchEngineList>
|
||||
get() {
|
||||
val google = mockSearchEngine(id = "google-b-1-m", n = "Google")
|
||||
|
||||
return CompletableDeferred(
|
||||
SearchEngineList(
|
||||
listOf(
|
||||
google,
|
||||
mockSearchEngine("bing", "Bing"),
|
||||
mockSearchEngine("amazondotcom", "Amazon.com")
|
||||
), default = google
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override val bundledSearchEngines = CompletableDeferred(
|
||||
SearchEngineList(
|
||||
listOf(
|
||||
mockSearchEngine("ecosia", "Ecosia"),
|
||||
mockSearchEngine("reddit", "Reddit"),
|
||||
mockSearchEngine("startpage", "Startpage.com")
|
||||
), default = null
|
||||
)
|
||||
)
|
||||
|
||||
override var customSearchEngines: Deferred<SearchEngineList> = CompletableDeferred(
|
||||
SearchEngineList(
|
||||
listOf(
|
||||
mockSearchEngine("my custom site", "my custom site")
|
||||
), default = null
|
||||
)
|
||||
)
|
||||
|
||||
override fun updateBaseSearchEngines() { }
|
||||
|
||||
fun mockSearchEngine(
|
||||
id: String,
|
||||
n: String = id
|
||||
): SearchEngine {
|
||||
val engine = mockk<SearchEngine>()
|
||||
every { engine.identifier } returns id
|
||||
every { engine.name } returns n
|
||||
every { engine.icon } returns mockk()
|
||||
return engine
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue