Adds custom search engines (#6551)
* For #5577 - Adds button to add a new search engine * For #5577 - Adds custom engine store * For #5577 - Creates a custom SearchEngineProvider * For #5577 - Gives the ability to delete search engines * For #5577 - Adds the UI to add a custom search engine * For #5577 - Adds form to create a custom search engine * For #5577 - Adds the ability to add a custom search engine * For #5577 - Adds the ability to delete custom search engines * For #5577 - Selects the first element on the add custom search engine screen * For #5577 - Prevents adding a search engine that already exists * For #5577 - Styles the add search engine preference * For #5577 - Makes the name check case-insensitive * For #5577 - Fix bug where home screen doesnt see new search engines * For #5577 - Moves Search URL validation to its own type * For #5577 - Fixes linting errors * For #5577 - Adds the ability to edit a custom search engine * For #5577 - Allows the user to edit a serach engine even when it is the last item in the list * For #5577 - Adds an undo snackbar when deleting a search engine * For #5577 - Moves all of the strings to be translated * For #5577 - Fixes bug when deleting your default search engine * For #5577 - Puts adding search engines behind a feature flag * For #5577 - Navigate to custom search engine SUMO article when tapping learn more * For #5577 - Fixes nits * For #5577 - Uses concept-fetch to validate search string * For #5577 - Adds string resources for the cannot reach error statemaster
parent
8abf580579
commit
607c3d4c87
|
@ -70,14 +70,14 @@ android {
|
||||||
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
|
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
|
||||||
applicationIdSuffix ".firefox"
|
applicationIdSuffix ".firefox"
|
||||||
manifestPlaceholders = [
|
manifestPlaceholders = [
|
||||||
// This release type is meant to replace Firefox (Release channel) and therefore needs to inherit
|
// This release type is meant to replace Firefox (Release channel) and therefore needs to inherit
|
||||||
// its sharedUserId for all eternity. See:
|
// its sharedUserId for all eternity. See:
|
||||||
// https://searchfox.org/mozilla-central/search?q=moz_android_shared_id&case=false®exp=false&path=
|
// https://searchfox.org/mozilla-central/search?q=moz_android_shared_id&case=false®exp=false&path=
|
||||||
// Shipping an app update without sharedUserId can have
|
// Shipping an app update without sharedUserId can have
|
||||||
// fatal consequences. For example see:
|
// fatal consequences. For example see:
|
||||||
// - https://issuetracker.google.com/issues/36924841
|
// - https://issuetracker.google.com/issues/36924841
|
||||||
// - https://issuetracker.google.com/issues/36905922
|
// - https://issuetracker.google.com/issues/36905922
|
||||||
"sharedUserId": "org.mozilla.firefox.sharedID"
|
"sharedUserId": "org.mozilla.firefox.sharedID"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,7 @@ android {
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
androidTest {
|
androidTest {
|
||||||
resources.srcDirs += ['src/androidTest/resources']
|
resources.srcDirs += ['src/androidTest/resources']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -544,22 +544,22 @@ if (project.hasProperty("coverage")) {
|
||||||
task printVariants {
|
task printVariants {
|
||||||
doLast {
|
doLast {
|
||||||
def variants = android.applicationVariants.collect {[
|
def variants = android.applicationVariants.collect {[
|
||||||
apks: it.variantData.outputScope.apkDatas.collect {[
|
apks: it.variantData.outputScope.apkDatas.collect {[
|
||||||
abi: it.filters.find { it.filterType == 'ABI' }.identifier,
|
abi: it.filters.find { it.filterType == 'ABI' }.identifier,
|
||||||
fileName: it.outputFileName,
|
fileName: it.outputFileName,
|
||||||
]},
|
]},
|
||||||
build_type: it.buildType.name,
|
build_type: it.buildType.name,
|
||||||
engine: it.productFlavors.find { it.dimension == 'engine' }.name,
|
engine: it.productFlavors.find { it.dimension == 'engine' }.name,
|
||||||
name: it.name,
|
name: it.name,
|
||||||
]}
|
]}
|
||||||
println 'variants: ' + groovy.json.JsonOutput.toJson(variants)
|
println 'variants: ' + groovy.json.JsonOutput.toJson(variants)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def glean_android_components_tag = (
|
def glean_android_components_tag = (
|
||||||
Versions.mozilla_android_components.endsWith('-SNAPSHOT') ?
|
Versions.mozilla_android_components.endsWith('-SNAPSHOT') ?
|
||||||
'master' :
|
'master' :
|
||||||
'v' + Versions.mozilla_android_components
|
'v' + Versions.mozilla_android_components
|
||||||
)
|
)
|
||||||
|
|
||||||
// Generate markdown docs for the collected metrics.
|
// Generate markdown docs for the collected metrics.
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
|
||||||
|
<ShortName>Startpage.com</ShortName>
|
||||||
|
<Description>Startpage.com Search</Description>
|
||||||
|
<InputEncoding>UTF-8</InputEncoding>
|
||||||
|
<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAACNAAAAjQHGZvekAAAAhklEQVQ4jWNMLf6fwMDAMIGBgYGfgTTwkYGBoYCJTM0MUD0TmMjUDDeEiQLNYDBqAAMD4807///LSjMwcHKQacD/////owveuvOM4fOXH3D+mfN3sWq+dfsZA2NEfAnYgCdPXzA8efqSdBcoaLpiuIAUMDjSAShXkQs+ggwoINOQjwwMDAUAhucl85AOIvIAAAAASUVORK5CYII=</Image>
|
||||||
|
<Url type="text/html" template="https://www.startpage.com/do/dsearch?query={searchTerms}&cat=web&pl=opensearch&language=english"></Url>
|
||||||
|
<Url type="application/x-suggestions+json" template="https://www.startpage.com/cgi-bin/csuggest?query={searchTerms}&limit=10&lang=english&format=json"/>
|
||||||
|
</SearchPlugin>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -9,8 +9,7 @@ import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mozilla.components.browser.search.SearchEngineManager
|
import mozilla.components.browser.search.SearchEngineManager
|
||||||
import mozilla.components.browser.search.provider.AssetsSearchEngineProvider
|
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
|
||||||
import mozilla.components.browser.search.provider.localization.LocaleSearchLocalizationProvider
|
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
import org.mozilla.fenix.test.Mockable
|
import org.mozilla.fenix.test.Mockable
|
||||||
|
|
||||||
|
@ -19,15 +18,15 @@ import org.mozilla.fenix.test.Mockable
|
||||||
*/
|
*/
|
||||||
@Mockable
|
@Mockable
|
||||||
class Search(private val context: Context) {
|
class Search(private val context: Context) {
|
||||||
|
val provider = FenixSearchEngineProvider(context)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component provides access to a centralized registry of search engines.
|
* This component provides access to a centralized registry of search engines.
|
||||||
*/
|
*/
|
||||||
val searchEngineManager by lazy {
|
val searchEngineManager by lazy {
|
||||||
SearchEngineManager(
|
SearchEngineManager(
|
||||||
coroutineContext = IO, providers = listOf(
|
coroutineContext = IO,
|
||||||
AssetsSearchEngineProvider(LocaleSearchLocalizationProvider())
|
providers = listOf(provider)
|
||||||
)
|
|
||||||
).apply {
|
).apply {
|
||||||
registerForLocaleUpdates(context)
|
registerForLocaleUpdates(context)
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
/* 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
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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"
|
||||||
|
private const val PREF_FILE_SEARCH_ENGINES = "custom-search-engines"
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
/* 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 kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
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 org.mozilla.fenix.ext.settings
|
||||||
|
|
||||||
|
@SuppressWarnings("TooManyFunctions")
|
||||||
|
class FenixSearchEngineProvider(
|
||||||
|
private val context: Context
|
||||||
|
) : SearchEngineProvider, CoroutineScope by CoroutineScope(Job() + Dispatchers.IO) {
|
||||||
|
private val defaultEngines = async {
|
||||||
|
AssetsSearchEngineProvider(LocaleSearchLocalizationProvider()).loadSearchEngines(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val bundledEngines = async {
|
||||||
|
AssetsSearchEngineProvider(
|
||||||
|
LocaleSearchLocalizationProvider(),
|
||||||
|
filters = listOf(object : SearchEngineFilter {
|
||||||
|
override fun filter(context: Context, searchEngine: SearchEngine): Boolean {
|
||||||
|
return BUNDLED_SEARCH_ENGINES.contains(searchEngine.identifier)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
additionalIdentifiers = BUNDLED_SEARCH_ENGINES
|
||||||
|
).loadSearchEngines(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var customEngines = async {
|
||||||
|
CustomSearchEngineProvider().loadSearchEngines(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var loadedSearchEngines = refreshAsync()
|
||||||
|
|
||||||
|
fun getDefaultEngine(context: Context): SearchEngine {
|
||||||
|
val engines = installedSearchEngines(context)
|
||||||
|
val selectedName = context.settings().defaultSearchEngineName
|
||||||
|
|
||||||
|
return engines.list.find { it.name == selectedName } ?: engines.list.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installedSearchEngines(context: Context): SearchEngineList = runBlocking {
|
||||||
|
val engineList = loadedSearchEngines.await()
|
||||||
|
val installedIdentifiers = installedSearchEngineIdentifiers(context)
|
||||||
|
|
||||||
|
engineList.copy(
|
||||||
|
list = engineList.list.filter {
|
||||||
|
installedIdentifiers.contains(it.identifier)
|
||||||
|
},
|
||||||
|
default = engineList.default?.let {
|
||||||
|
if (installedIdentifiers.contains(it.identifier)) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun allSearchEngineIdentifiers() = runBlocking {
|
||||||
|
loadedSearchEngines.await().list.map { it.identifier }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uninstalledSearchEngines(context: Context): SearchEngineList = runBlocking {
|
||||||
|
val engineList = loadedSearchEngines.await()
|
||||||
|
val installedIdentifiers = installedSearchEngineIdentifiers(context)
|
||||||
|
|
||||||
|
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) = runBlocking {
|
||||||
|
val installedIdentifiers = installedSearchEngineIdentifiers(context).toMutableSet()
|
||||||
|
installedIdentifiers.add(searchEngine.identifier)
|
||||||
|
prefs(context).edit().putStringSet(INSTALLED_ENGINES_KEY, installedIdentifiers).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uninstallSearchEngine(context: Context, searchEngine: SearchEngine) = runBlocking {
|
||||||
|
val isCustom = CustomSearchEngineStore.isCustomSearchEngine(context, searchEngine.identifier)
|
||||||
|
|
||||||
|
if (isCustom) {
|
||||||
|
CustomSearchEngineStore.removeSearchEngine(context, searchEngine.identifier)
|
||||||
|
} else {
|
||||||
|
val installedIdentifiers = installedSearchEngineIdentifiers(context).toMutableSet()
|
||||||
|
installedIdentifiers.remove(searchEngine.identifier)
|
||||||
|
prefs(context).edit().putStringSet(INSTALLED_ENGINES_KEY, installedIdentifiers).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reload() {
|
||||||
|
launch {
|
||||||
|
customEngines = async { CustomSearchEngineProvider().loadSearchEngines(context) }
|
||||||
|
loadedSearchEngines = refreshAsync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshAsync() = async {
|
||||||
|
val engineList = defaultEngines.await()
|
||||||
|
val bundledList = bundledEngines.await().list
|
||||||
|
val customList = customEngines.await().list
|
||||||
|
|
||||||
|
engineList.copy(list = engineList.list + bundledList + customList)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prefs(context: Context) = context.getSharedPreferences(
|
||||||
|
PREF_FILE,
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
|
||||||
|
private suspend fun installedSearchEngineIdentifiers(context: Context): Set<String> {
|
||||||
|
val prefs = prefs(context)
|
||||||
|
|
||||||
|
val identifiers = if (!prefs.contains(INSTALLED_ENGINES_KEY)) {
|
||||||
|
val defaultSet = defaultEngines.await()
|
||||||
|
.list
|
||||||
|
.map { it.identifier }
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
prefs.edit().putStringSet(INSTALLED_ENGINES_KEY, defaultSet).apply()
|
||||||
|
defaultSet
|
||||||
|
} else {
|
||||||
|
prefs(context).getStringSet(INSTALLED_ENGINES_KEY, setOf()) ?: setOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
val customEngineIdentifiers = customEngines.await().list.map { it.identifier }.toSet()
|
||||||
|
return identifiers + customEngineIdentifiers
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val BUNDLED_SEARCH_ENGINES = listOf("ecosia", "reddit", "startpage", "yahoo", "youtube")
|
||||||
|
private const val PREF_FILE = "fenix-search-engine-provider"
|
||||||
|
private const val INSTALLED_ENGINES_KEY = "fenix-installed-search-engines"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
/* 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.Log
|
||||||
|
import org.w3c.dom.Document
|
||||||
|
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
|
||||||
|
import android.util.Base64
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -233,10 +233,7 @@ class HomeFragment : Fragment() {
|
||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val iconSize = resources.getDimension(R.dimen.preference_icon_drawable_size).toInt()
|
val iconSize = resources.getDimension(R.dimen.preference_icon_drawable_size).toInt()
|
||||||
|
|
||||||
val searchEngine = requireComponents.search.searchEngineManager.getDefaultSearchEngineAsync(
|
val searchEngine = requireComponents.search.provider.getDefaultEngine(requireContext())
|
||||||
requireContext(),
|
|
||||||
requireContext().settings().defaultSearchEngineName
|
|
||||||
)
|
|
||||||
val searchIcon = BitmapDrawable(resources, searchEngine.icon)
|
val searchIcon = BitmapDrawable(resources, searchEngine.icon)
|
||||||
searchIcon.setBounds(0, 0, iconSize, iconSize)
|
searchIcon.setBounds(0, 0, iconSize, iconSize)
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,7 @@ class SearchFragment : Fragment(), BackHandler {
|
||||||
val view = inflater.inflate(R.layout.fragment_search, container, false)
|
val view = inflater.inflate(R.layout.fragment_search, container, false)
|
||||||
val url = session?.url.orEmpty()
|
val url = session?.url.orEmpty()
|
||||||
val currentSearchEngine = SearchEngineSource.Default(
|
val currentSearchEngine = SearchEngineSource.Default(
|
||||||
requireComponents.search.searchEngineManager.getDefaultSearchEngine(requireContext())
|
requireComponents.search.provider.getDefaultEngine(requireContext())
|
||||||
)
|
)
|
||||||
|
|
||||||
searchStore = StoreProvider.get(this) {
|
searchStore = StoreProvider.get(this) {
|
||||||
|
@ -204,10 +204,7 @@ class SearchFragment : Fragment(), BackHandler {
|
||||||
// The user has the option to go to 'Shortcuts' -> 'Search engine settings' to modify the default search engine.
|
// The user has the option to go to 'Shortcuts' -> 'Search engine settings' to modify the default search engine.
|
||||||
// When returning from that settings screen we need to update it to account for any changes.
|
// When returning from that settings screen we need to update it to account for any changes.
|
||||||
val currentDefaultEngine =
|
val currentDefaultEngine =
|
||||||
requireComponents.search.searchEngineManager.getDefaultSearchEngine(
|
requireComponents.search.provider.getDefaultEngine(requireContext())
|
||||||
requireContext(),
|
|
||||||
requireContext().settings().defaultSearchEngineName
|
|
||||||
)
|
|
||||||
|
|
||||||
if (searchStore.state.defaultEngineSource.searchEngine != currentDefaultEngine) {
|
if (searchStore.state.defaultEngineSource.searchEngine != currentDefaultEngine) {
|
||||||
searchStore.dispatch(
|
searchStore.dispatch(
|
||||||
|
|
|
@ -168,7 +168,7 @@ class AwesomeBarView(
|
||||||
|
|
||||||
shortcutsEnginePickerProvider =
|
shortcutsEnginePickerProvider =
|
||||||
ShortcutsSuggestionProvider(
|
ShortcutsSuggestionProvider(
|
||||||
components.search.searchEngineManager,
|
components.search.provider,
|
||||||
this,
|
this,
|
||||||
interactor::onSearchShortcutEngineSelected,
|
interactor::onSearchShortcutEngineSelected,
|
||||||
interactor::onClickSearchEngineSettings
|
interactor::onClickSearchEngineSettings
|
||||||
|
@ -329,10 +329,7 @@ class AwesomeBarView(
|
||||||
|
|
||||||
searchSuggestionProviderMap.put(
|
searchSuggestionProviderMap.put(
|
||||||
engine, SearchSuggestionProvider(
|
engine, SearchSuggestionProvider(
|
||||||
components.search.searchEngineManager.getDefaultSearchEngine(
|
components.search.provider.getDefaultEngine(this),
|
||||||
this,
|
|
||||||
engine.name
|
|
||||||
),
|
|
||||||
shortcutSearchUseCase,
|
shortcutSearchUseCase,
|
||||||
components.core.client,
|
components.core.client,
|
||||||
limit = 3,
|
limit = 3,
|
||||||
|
|
|
@ -7,16 +7,16 @@ package org.mozilla.fenix.search.awesomebar
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import mozilla.components.browser.search.SearchEngine
|
import mozilla.components.browser.search.SearchEngine
|
||||||
import mozilla.components.browser.search.SearchEngineManager
|
|
||||||
import mozilla.components.concept.awesomebar.AwesomeBar
|
import mozilla.components.concept.awesomebar.AwesomeBar
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [AwesomeBar.SuggestionProvider] implementation that provides search engine suggestions.
|
* A [AwesomeBar.SuggestionProvider] implementation that provides search engine suggestions.
|
||||||
*/
|
*/
|
||||||
class ShortcutsSuggestionProvider(
|
class ShortcutsSuggestionProvider(
|
||||||
private val searchEngineManager: SearchEngineManager,
|
private val searchEngineProvider: FenixSearchEngineProvider,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val selectShortcutEngine: (engine: SearchEngine) -> Unit,
|
private val selectShortcutEngine: (engine: SearchEngine) -> Unit,
|
||||||
private val selectShortcutEngineSettings: () -> Unit
|
private val selectShortcutEngineSettings: () -> Unit
|
||||||
|
@ -33,7 +33,7 @@ class ShortcutsSuggestionProvider(
|
||||||
override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> {
|
override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> {
|
||||||
val suggestions = mutableListOf<AwesomeBar.Suggestion>()
|
val suggestions = mutableListOf<AwesomeBar.Suggestion>()
|
||||||
|
|
||||||
searchEngineManager.getSearchEngines(context).forEach {
|
searchEngineProvider.installedSearchEngines(context).list.forEach {
|
||||||
suggestions.add(
|
suggestions.add(
|
||||||
AwesomeBar.Suggestion(
|
AwesomeBar.Suggestion(
|
||||||
provider = this,
|
provider = this,
|
||||||
|
|
|
@ -30,7 +30,8 @@ object SupportUtils {
|
||||||
TRACKING_PROTECTION("tracking-protection-firefox-preview"),
|
TRACKING_PROTECTION("tracking-protection-firefox-preview"),
|
||||||
WHATS_NEW("whats-new-firefox-preview"),
|
WHATS_NEW("whats-new-firefox-preview"),
|
||||||
SEND_TABS("send-tab-preview"),
|
SEND_TABS("send-tab-preview"),
|
||||||
SET_AS_DEFAULT_BROWSER("set-firefox-preview-default")
|
SET_AS_DEFAULT_BROWSER("set-firefox-preview-default"),
|
||||||
|
CUSTOM_SEARCH_ENGINES("custom-search-engines")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,289 @@
|
||||||
|
/* 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.res.Resources
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import kotlinx.android.synthetic.main.custom_search_engine.*
|
||||||
|
import kotlinx.android.synthetic.main.fragment_add_search_engine.*
|
||||||
|
import kotlinx.android.synthetic.main.search_engine_radio_button.view.*
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import mozilla.components.browser.search.SearchEngine
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.components.FenixSnackbar
|
||||||
|
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.increaseTapArea
|
||||||
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
|
import org.mozilla.fenix.settings.SupportUtils
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@SuppressWarnings("LargeClass", "TooManyFunctions")
|
||||||
|
class AddSearchEngineFragment : Fragment(), CompoundButton.OnCheckedChangeListener {
|
||||||
|
private var availableEngines: List<SearchEngine> = listOf()
|
||||||
|
private var selectedIndex: Int = -1
|
||||||
|
private val engineViews = mutableListOf<View>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
|
||||||
|
availableEngines = runBlocking {
|
||||||
|
requireContext()
|
||||||
|
.components
|
||||||
|
.search
|
||||||
|
.provider
|
||||||
|
.uninstalledSearchEngines(requireContext())
|
||||||
|
.list
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIndex = if (availableEngines.isEmpty()) CUSTOM_INDEX else FIRST_INDEX
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_add_search_engine, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
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 = requireContext().resources
|
||||||
|
)
|
||||||
|
engineItem.id = index
|
||||||
|
engineItem.tag = engineId
|
||||||
|
engineItem.radio_button.isChecked = selectedIndex == index
|
||||||
|
engineViews.add(engineItem)
|
||||||
|
search_engine_group.addView(engineItem, layoutParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
availableEngines.forEachIndexed(setupSearchEngineItem)
|
||||||
|
|
||||||
|
val engineItem = makeCustomButton(layoutInflater)
|
||||||
|
engineItem.id = CUSTOM_INDEX
|
||||||
|
engineItem.radio_button.isChecked = selectedIndex == CUSTOM_INDEX
|
||||||
|
engineViews.add(engineItem)
|
||||||
|
search_engine_group.addView(engineItem, layoutParams)
|
||||||
|
|
||||||
|
toggleCustomForm(selectedIndex == CUSTOM_INDEX)
|
||||||
|
|
||||||
|
custom_search_engines_learn_more.increaseTapArea(DPS_TO_INCREASE)
|
||||||
|
custom_search_engines_learn_more.setOnClickListener {
|
||||||
|
requireContext().let { context ->
|
||||||
|
val intent = SupportUtils.createCustomTabIntent(
|
||||||
|
context,
|
||||||
|
SupportUtils.getSumoURLForTopic(
|
||||||
|
context,
|
||||||
|
SupportUtils.SumoTopic.CUSTOM_SEARCH_ENGINES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
(activity as AppCompatActivity).title = getString(R.string.search_engine_add_custom_search_engine_title)
|
||||||
|
(activity as AppCompatActivity).supportActionBar?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.add_custom_searchengine_menu, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
R.id.add_search_engine -> {
|
||||||
|
when (selectedIndex) {
|
||||||
|
CUSTOM_INDEX -> createCustomEngine()
|
||||||
|
else -> {
|
||||||
|
val engine = availableEngines[selectedIndex]
|
||||||
|
installEngine(engine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCustomEngine() {
|
||||||
|
custom_search_engine_name_field.error = ""
|
||||||
|
custom_search_engine_search_string_field.error = ""
|
||||||
|
|
||||||
|
val name = edit_engine_name.text?.toString() ?: ""
|
||||||
|
val searchString = edit_search_string.text?.toString() ?: ""
|
||||||
|
|
||||||
|
var hasError = false
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
custom_search_engine_name_field.error = resources
|
||||||
|
.getString(R.string.search_add_custom_engine_error_empty_name)
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val existingIdentifiers = requireComponents
|
||||||
|
.search
|
||||||
|
.provider
|
||||||
|
.allSearchEngineIdentifiers()
|
||||||
|
.map { it.toLowerCase(Locale.ROOT) }
|
||||||
|
|
||||||
|
if (existingIdentifiers.contains(name.toLowerCase(Locale.ROOT))) {
|
||||||
|
custom_search_engine_name_field.error = resources
|
||||||
|
.getString(R.string.search_add_custom_engine_error_existing_name, name)
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchString.isEmpty()) {
|
||||||
|
custom_search_engine_search_string_field
|
||||||
|
.error = resources.getString(R.string.search_add_custom_engine_error_empty_search_string)
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchString.contains("%s")) {
|
||||||
|
custom_search_engine_search_string_field
|
||||||
|
.error = resources.getString(R.string.search_add_custom_engine_error_missing_template)
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) { return }
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch(Main) {
|
||||||
|
val result = withContext(IO) {
|
||||||
|
SearchStringValidator.isSearchStringValid(
|
||||||
|
requireComponents.core.client,
|
||||||
|
searchString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (result) {
|
||||||
|
SearchStringValidator.Result.CannotReach -> {
|
||||||
|
custom_search_engine_search_string_field.error = resources
|
||||||
|
.getString(R.string.search_add_custom_engine_error_cannot_reach)
|
||||||
|
}
|
||||||
|
SearchStringValidator.Result.Success -> {
|
||||||
|
CustomSearchEngineStore.addSearchEngine(
|
||||||
|
context = requireContext(),
|
||||||
|
engineName = name,
|
||||||
|
searchQuery = searchString
|
||||||
|
)
|
||||||
|
requireComponents.search.provider.reload()
|
||||||
|
val successMessage = resources
|
||||||
|
.getString(R.string.search_add_custom_engine_success_message, name)
|
||||||
|
|
||||||
|
view?.also {
|
||||||
|
FenixSnackbar.make(it, FenixSnackbar.LENGTH_SHORT)
|
||||||
|
.setText(successMessage)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun installEngine(engine: SearchEngine) {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch(Main) {
|
||||||
|
withContext(IO) {
|
||||||
|
requireContext().components.search.provider.installSearchEngine(
|
||||||
|
requireContext(),
|
||||||
|
engine
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
||||||
|
engineViews.forEach {
|
||||||
|
when (it.radio_button == buttonView) {
|
||||||
|
true -> {
|
||||||
|
selectedIndex = it.id
|
||||||
|
}
|
||||||
|
false -> {
|
||||||
|
it.radio_button.setOnCheckedChangeListener(null)
|
||||||
|
it.radio_button.isChecked = false
|
||||||
|
it.radio_button.setOnCheckedChangeListener(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCustomForm(selectedIndex == -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeCustomButton(layoutInflater: LayoutInflater): View {
|
||||||
|
val wrapper = layoutInflater
|
||||||
|
.inflate(R.layout.custom_search_engine_radio_button, null) as ConstraintLayout
|
||||||
|
wrapper.setOnClickListener { wrapper.radio_button.isChecked = true }
|
||||||
|
wrapper.radio_button.setOnCheckedChangeListener(this)
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleCustomForm(isEnabled: Boolean) {
|
||||||
|
custom_search_engine_form.alpha = if (isEnabled) ENABLED_ALPHA else DISABLED_ALPHA
|
||||||
|
edit_search_string.isEnabled = isEnabled
|
||||||
|
edit_engine_name.isEnabled = isEnabled
|
||||||
|
custom_search_engines_learn_more.isEnabled = isEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeButtonFromSearchEngine(
|
||||||
|
engine: SearchEngine,
|
||||||
|
layoutInflater: LayoutInflater,
|
||||||
|
res: Resources
|
||||||
|
): View {
|
||||||
|
val wrapper = layoutInflater
|
||||||
|
.inflate(R.layout.search_engine_radio_button, null) as ConstraintLayout
|
||||||
|
wrapper.setOnClickListener { wrapper.radio_button.isChecked = true }
|
||||||
|
wrapper.radio_button.setOnCheckedChangeListener(this)
|
||||||
|
wrapper.engine_text.text = engine.name
|
||||||
|
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)
|
||||||
|
wrapper.overflow_menu.visibility = View.GONE
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ENABLED_ALPHA = 1.0f
|
||||||
|
private const val DISABLED_ALPHA = 0.2f
|
||||||
|
private const val CUSTOM_INDEX = -1
|
||||||
|
private const val FIRST_INDEX = 0
|
||||||
|
private const val DPS_TO_INCREASE = 20
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
/* 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.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import kotlinx.android.synthetic.main.custom_search_engine.*
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import mozilla.components.browser.search.SearchEngine
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.components.FenixSnackbar
|
||||||
|
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
|
||||||
|
import org.mozilla.fenix.ext.increaseTapArea
|
||||||
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
|
import org.mozilla.fenix.settings.SupportUtils
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class EditCustomSearchEngineFragment : Fragment(R.layout.fragment_add_search_engine) {
|
||||||
|
private val engineIdentifier: String by lazy {
|
||||||
|
navArgs<EditCustomSearchEngineFragmentArgs>().value.searchEngineIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var searchEngine: SearchEngine
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
searchEngine = CustomSearchEngineStore.loadCustomSearchEngines(requireContext()).first {
|
||||||
|
it.identifier == engineIdentifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
edit_engine_name.setText(searchEngine.name)
|
||||||
|
val decodedUrl = Uri.decode(searchEngine.buildSearchUrl("%s"))
|
||||||
|
edit_search_string.setText(decodedUrl)
|
||||||
|
|
||||||
|
custom_search_engines_learn_more.increaseTapArea(DPS_TO_INCREASE)
|
||||||
|
custom_search_engines_learn_more.setOnClickListener {
|
||||||
|
requireContext().let { context ->
|
||||||
|
val intent = SupportUtils.createCustomTabIntent(
|
||||||
|
context,
|
||||||
|
SupportUtils.getSumoURLForTopic(
|
||||||
|
context,
|
||||||
|
SupportUtils.SumoTopic.CUSTOM_SEARCH_ENGINES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
(activity as AppCompatActivity).title = getString(R.string.search_engine_edit_custom_search_engine_title)
|
||||||
|
(activity as AppCompatActivity).supportActionBar?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.edit_custom_searchengine_menu, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
R.id.save_button -> {
|
||||||
|
saveCustomEngine()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveCustomEngine() {
|
||||||
|
custom_search_engine_name_field.error = ""
|
||||||
|
custom_search_engine_search_string_field.error = ""
|
||||||
|
|
||||||
|
val name = edit_engine_name.text?.toString() ?: ""
|
||||||
|
val searchString = edit_search_string.text?.toString() ?: ""
|
||||||
|
|
||||||
|
var hasError = false
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
custom_search_engine_name_field.error = resources
|
||||||
|
.getString(R.string.search_add_custom_engine_error_empty_name)
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val existingIdentifiers = requireComponents
|
||||||
|
.search
|
||||||
|
.provider
|
||||||
|
.allSearchEngineIdentifiers()
|
||||||
|
.map { it.toLowerCase(Locale.ROOT) }
|
||||||
|
|
||||||
|
val nameHasChanged = name != engineIdentifier
|
||||||
|
|
||||||
|
if (existingIdentifiers.contains(name.toLowerCase(Locale.ROOT)) && nameHasChanged) {
|
||||||
|
custom_search_engine_name_field.error = resources
|
||||||
|
.getString(R.string.search_add_custom_engine_error_existing_name, name)
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchString.isEmpty()) {
|
||||||
|
custom_search_engine_search_string_field
|
||||||
|
.error = resources.getString(R.string.search_add_custom_engine_error_empty_search_string)
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchString.contains("%s")) {
|
||||||
|
custom_search_engine_name_field
|
||||||
|
.error = resources.getString(R.string.search_add_custom_engine_error_missing_template)
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) { return }
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch(Main) {
|
||||||
|
val result = withContext(IO) {
|
||||||
|
SearchStringValidator.isSearchStringValid(
|
||||||
|
requireComponents.core.client,
|
||||||
|
searchString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (result) {
|
||||||
|
SearchStringValidator.Result.CannotReach -> {
|
||||||
|
custom_search_engine_search_string_field.error = resources
|
||||||
|
.getString(R.string.search_add_custom_engine_error_cannot_reach)
|
||||||
|
}
|
||||||
|
SearchStringValidator.Result.Success -> {
|
||||||
|
CustomSearchEngineStore.updateSearchEngine(
|
||||||
|
context = requireContext(),
|
||||||
|
oldEngineName = engineIdentifier,
|
||||||
|
newEngineName = name,
|
||||||
|
searchQuery = searchString
|
||||||
|
)
|
||||||
|
requireComponents.search.provider.reload()
|
||||||
|
val successMessage = resources
|
||||||
|
.getString(R.string.search_edit_custom_engine_success_message, name)
|
||||||
|
|
||||||
|
view?.also {
|
||||||
|
FenixSnackbar.make(it, FenixSnackbar.LENGTH_SHORT)
|
||||||
|
.setText(successMessage)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DPS_TO_INCREASE = 20
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ package org.mozilla.fenix.settings.search
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.preference.SwitchPreference
|
import androidx.preference.SwitchPreference
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
@ -49,10 +51,26 @@ class SearchEngineFragment : PreferenceFragmentCompat() {
|
||||||
isChecked = context.settings().shouldShowClipboardSuggestions
|
isChecked = context.settings().shouldShowClipboardSuggestions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val searchEngineListPreference =
|
||||||
|
findPreference<SearchEngineListPreference>(getPreferenceKey(R.string.pref_key_search_engine_list))
|
||||||
|
|
||||||
|
searchEngineListPreference?.reload(requireContext())
|
||||||
searchSuggestionsPreference?.onPreferenceChangeListener = SharedPreferenceUpdater()
|
searchSuggestionsPreference?.onPreferenceChangeListener = SharedPreferenceUpdater()
|
||||||
showSearchShortcuts?.onPreferenceChangeListener = SharedPreferenceUpdater()
|
showSearchShortcuts?.onPreferenceChangeListener = SharedPreferenceUpdater()
|
||||||
showHistorySuggestions?.onPreferenceChangeListener = SharedPreferenceUpdater()
|
showHistorySuggestions?.onPreferenceChangeListener = SharedPreferenceUpdater()
|
||||||
showBookmarkSuggestions?.onPreferenceChangeListener = SharedPreferenceUpdater()
|
showBookmarkSuggestions?.onPreferenceChangeListener = SharedPreferenceUpdater()
|
||||||
showClipboardSuggestions?.onPreferenceChangeListener = SharedPreferenceUpdater()
|
showClipboardSuggestions?.onPreferenceChangeListener = SharedPreferenceUpdater()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||||
|
when (preference.key) {
|
||||||
|
getPreferenceKey(R.string.pref_key_add_search_engine) -> {
|
||||||
|
val directions = SearchEngineFragmentDirections
|
||||||
|
.actionSearchEngineFragmentToAddSearchEngineFragment()
|
||||||
|
findNavController().navigate(directions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onPreferenceTreeClick(preference)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,13 +14,20 @@ import android.view.ViewGroup
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.RadioGroup
|
import android.widget.RadioGroup
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.navigation.Navigation
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceViewHolder
|
import androidx.preference.PreferenceViewHolder
|
||||||
import kotlinx.android.synthetic.main.search_engine_radio_button.view.*
|
import kotlinx.android.synthetic.main.search_engine_radio_button.view.*
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
import mozilla.components.browser.search.SearchEngine
|
import mozilla.components.browser.search.SearchEngine
|
||||||
|
import mozilla.components.browser.search.provider.SearchEngineList
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.getRootView
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
|
import org.mozilla.fenix.utils.allowUndo
|
||||||
|
|
||||||
abstract class SearchEngineListPreference @JvmOverloads constructor(
|
abstract class SearchEngineListPreference @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -28,7 +35,7 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
||||||
defStyleAttr: Int = android.R.attr.preferenceStyle
|
defStyleAttr: Int = android.R.attr.preferenceStyle
|
||||||
) : Preference(context, attrs, defStyleAttr), CompoundButton.OnCheckedChangeListener {
|
) : Preference(context, attrs, defStyleAttr), CompoundButton.OnCheckedChangeListener {
|
||||||
|
|
||||||
protected var searchEngines: List<SearchEngine> = emptyList()
|
protected lateinit var searchEngineList: SearchEngineList
|
||||||
protected var searchEngineGroup: RadioGroup? = null
|
protected var searchEngineGroup: RadioGroup? = null
|
||||||
|
|
||||||
protected abstract val itemResId: Int
|
protected abstract val itemResId: Int
|
||||||
|
@ -40,11 +47,11 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
||||||
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
|
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
|
||||||
super.onBindViewHolder(holder)
|
super.onBindViewHolder(holder)
|
||||||
searchEngineGroup = holder!!.itemView.findViewById(R.id.search_engine_group)
|
searchEngineGroup = holder!!.itemView.findViewById(R.id.search_engine_group)
|
||||||
val context = searchEngineGroup!!.context
|
reload(searchEngineGroup!!.context)
|
||||||
|
}
|
||||||
searchEngines = context.components.search.searchEngineManager.getSearchEngines(context)
|
|
||||||
.sortedBy { it.name }
|
|
||||||
|
|
||||||
|
fun reload(context: Context) {
|
||||||
|
searchEngineList = context.components.search.provider.installedSearchEngines(context)
|
||||||
refreshSearchEngineViews(context)
|
refreshSearchEngineViews(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,18 +66,10 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// To get the default search engine we have to pass in a name that doesn't exist
|
val defaultEngine = context.components.search.provider.getDefaultEngine(context).identifier
|
||||||
// https://github.com/mozilla-mobile/android-components/issues/3344
|
val selectedEngine = (searchEngineList.list.find {
|
||||||
val defaultSearchEngine = context.components.search.searchEngineManager.getDefaultSearchEngine(
|
it.identifier == defaultEngine
|
||||||
context,
|
} ?: searchEngineList.list.first()).identifier
|
||||||
THIS_IS_A_HACK_FIX_ME
|
|
||||||
)
|
|
||||||
|
|
||||||
val selectedSearchEngine =
|
|
||||||
context.components.search.searchEngineManager.getDefaultSearchEngine(
|
|
||||||
context,
|
|
||||||
context.settings().defaultSearchEngineName
|
|
||||||
).identifier
|
|
||||||
|
|
||||||
searchEngineGroup!!.removeAllViews()
|
searchEngineGroup!!.removeAllViews()
|
||||||
|
|
||||||
|
@ -82,31 +81,57 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
||||||
|
|
||||||
val setupSearchEngineItem: (Int, SearchEngine) -> Unit = { index, engine ->
|
val setupSearchEngineItem: (Int, SearchEngine) -> Unit = { index, engine ->
|
||||||
val engineId = engine.identifier
|
val engineId = engine.identifier
|
||||||
val engineItem = makeButtonFromSearchEngine(engine, layoutInflater, context.resources)
|
val engineItem = makeButtonFromSearchEngine(
|
||||||
engineItem.id = index
|
engine = engine,
|
||||||
|
layoutInflater = layoutInflater,
|
||||||
|
res = context.resources,
|
||||||
|
allowDeletion = searchEngineList.list.size > 1
|
||||||
|
)
|
||||||
|
|
||||||
|
engineItem.id = index + (searchEngineList.default?.let { 1 } ?: 0)
|
||||||
engineItem.tag = engineId
|
engineItem.tag = engineId
|
||||||
if (engineId == selectedSearchEngine) {
|
if (engineId == selectedEngine) {
|
||||||
updateDefaultItem(engineItem.radio_button)
|
updateDefaultItem(engineItem.radio_button)
|
||||||
}
|
}
|
||||||
searchEngineGroup!!.addView(engineItem, layoutParams)
|
searchEngineGroup!!.addView(engineItem, layoutParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
setupSearchEngineItem(0, defaultSearchEngine)
|
searchEngineList.default?.apply {
|
||||||
|
setupSearchEngineItem(0, this)
|
||||||
|
}
|
||||||
|
|
||||||
searchEngines
|
searchEngineList.list
|
||||||
.filter { it.identifier != defaultSearchEngine.identifier }
|
.filter { it.identifier != searchEngineList.default?.identifier }
|
||||||
|
.sortedBy { it.name }
|
||||||
.forEachIndexed(setupSearchEngineItem)
|
.forEachIndexed(setupSearchEngineItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makeButtonFromSearchEngine(
|
private fun makeButtonFromSearchEngine(
|
||||||
engine: SearchEngine,
|
engine: SearchEngine,
|
||||||
layoutInflater: LayoutInflater,
|
layoutInflater: LayoutInflater,
|
||||||
res: Resources
|
res: Resources,
|
||||||
|
allowDeletion: Boolean
|
||||||
): View {
|
): View {
|
||||||
|
val isCustomSearchEngine = CustomSearchEngineStore.isCustomSearchEngine(context, engine.identifier)
|
||||||
|
|
||||||
val wrapper = layoutInflater.inflate(itemResId, null) as ConstraintLayout
|
val wrapper = layoutInflater.inflate(itemResId, null) as ConstraintLayout
|
||||||
wrapper.setOnClickListener { wrapper.radio_button.isChecked = true }
|
wrapper.setOnClickListener { wrapper.radio_button.isChecked = true }
|
||||||
wrapper.radio_button.setOnCheckedChangeListener(this)
|
wrapper.radio_button.setOnCheckedChangeListener(this)
|
||||||
wrapper.engine_text.text = engine.name
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).menuBuilder.build(context).show(wrapper.overflow_menu)
|
||||||
|
}
|
||||||
val iconSize = res.getDimension(R.dimen.preference_icon_drawable_size).toInt()
|
val iconSize = res.getDimension(R.dimen.preference_icon_drawable_size).toInt()
|
||||||
val engineIcon = BitmapDrawable(res, engine.icon)
|
val engineIcon = BitmapDrawable(res, engine.icon)
|
||||||
engineIcon.setBounds(0, 0, iconSize, iconSize)
|
engineIcon.setBounds(0, 0, iconSize, iconSize)
|
||||||
|
@ -115,7 +140,7 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
||||||
searchEngines.forEach { engine ->
|
searchEngineList.list.forEach { engine ->
|
||||||
val wrapper: ConstraintLayout = searchEngineGroup?.findViewWithTag(engine.identifier) ?: return
|
val wrapper: ConstraintLayout = searchEngineGroup?.findViewWithTag(engine.identifier) ?: return
|
||||||
|
|
||||||
when (wrapper.radio_button == buttonView) {
|
when (wrapper.radio_button == buttonView) {
|
||||||
|
@ -129,7 +154,55 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
private fun editCustomSearchEngine(engine: SearchEngine) {
|
||||||
private const val THIS_IS_A_HACK_FIX_ME = "."
|
val directions = SearchEngineFragmentDirections
|
||||||
|
.actionSearchEngineFragmentToEditCustomSearchEngineFragment(engine.identifier)
|
||||||
|
Navigation.findNavController(searchEngineGroup!!).navigate(directions)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteSearchEngine(context: Context, engine: SearchEngine) {
|
||||||
|
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 = {
|
||||||
|
val defaultEngine = context.components.search.provider.getDefaultEngine(context)
|
||||||
|
|
||||||
|
searchEngineList = searchEngineList.copy(
|
||||||
|
list = searchEngineList.list + engine,
|
||||||
|
default = defaultEngine
|
||||||
|
)
|
||||||
|
|
||||||
|
refreshSearchEngineViews(context)
|
||||||
|
},
|
||||||
|
operation = {
|
||||||
|
val defaultEngine = context.components.search.provider.getDefaultEngine(context)
|
||||||
|
context.components.search.provider.uninstallSearchEngine(context, engine)
|
||||||
|
|
||||||
|
if (engine == defaultEngine) {
|
||||||
|
context.settings().defaultSearchEngineName = context
|
||||||
|
.components
|
||||||
|
.search
|
||||||
|
.provider
|
||||||
|
.getDefaultEngine(context)
|
||||||
|
.name
|
||||||
|
}
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
/* 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 mozilla.components.browser.menu.BrowserMenuBuilder
|
||||||
|
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.theme.ThemeManager
|
||||||
|
|
||||||
|
class SearchEngineMenu(
|
||||||
|
private val context: Context,
|
||||||
|
private val allowDeletion: Boolean,
|
||||||
|
private val isCustomSearchEngine: Boolean,
|
||||||
|
private val onItemTapped: (Item) -> Unit = {}
|
||||||
|
) {
|
||||||
|
sealed class Item {
|
||||||
|
object Delete : Item()
|
||||||
|
object Edit : Item()
|
||||||
|
}
|
||||||
|
|
||||||
|
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
|
||||||
|
|
||||||
|
private val menuItems by lazy {
|
||||||
|
val items = mutableListOf<SimpleBrowserMenuItem>()
|
||||||
|
|
||||||
|
if (isCustomSearchEngine) {
|
||||||
|
items.add(
|
||||||
|
SimpleBrowserMenuItem(
|
||||||
|
label = context.getString(R.string.search_engine_edit)
|
||||||
|
) {
|
||||||
|
onItemTapped.invoke(Item.Edit)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowDeletion) {
|
||||||
|
items.add(
|
||||||
|
SimpleBrowserMenuItem(
|
||||||
|
context.getString(R.string.search_engine_delete),
|
||||||
|
textColorResource = ThemeManager.resolveAttribute(R.attr.destructive, context)
|
||||||
|
) {
|
||||||
|
onItemTapped.invoke(Item.Delete)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/* 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.net.Uri
|
||||||
|
import mozilla.components.concept.fetch.Client
|
||||||
|
import mozilla.components.concept.fetch.Request
|
||||||
|
import mozilla.components.concept.fetch.isSuccess
|
||||||
|
import mozilla.components.support.ktx.kotlin.toNormalizedUrl
|
||||||
|
|
||||||
|
object SearchStringValidator {
|
||||||
|
enum class Result { Success, CannotReach }
|
||||||
|
|
||||||
|
fun isSearchStringValid(client: Client, searchString: String): Result {
|
||||||
|
val request = createRequest(searchString)
|
||||||
|
val response = client.fetch(request)
|
||||||
|
return if (response.isSuccess) Result.Success else Result.CannotReach
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRequest(searchString: String): Request {
|
||||||
|
// we should share the code to substitute and normalize the search string (see SearchEngine.buildSearchUrl).
|
||||||
|
val encodedTestQuery = Uri.encode("testSearchEngineValidation")
|
||||||
|
|
||||||
|
val normalizedHttpsSearchUrlStr = searchString.toNormalizedUrl()
|
||||||
|
val searchUrl = normalizedHttpsSearchUrlStr.replace("%s".toRegex(), encodedTestQuery)
|
||||||
|
return Request(searchUrl)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/custom_search_engine_form"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:importantForAutofill="noExcludeDescendants">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/custom_search_engine_name_field"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:hintTextColor="?secondaryText"
|
||||||
|
android:textColorHint="?secondaryText"
|
||||||
|
app:hintTextAppearance="@style/EngineTextField"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
app:errorEnabled="true">
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/edit_engine_name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/search_add_custom_engine_name_hint"
|
||||||
|
android:inputType="text" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/custom_search_engine_search_string_field"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:hintTextColor="?secondaryText"
|
||||||
|
android:textColorHint="?secondaryText"
|
||||||
|
app:hintTextAppearance="@style/EngineTextField"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
app:errorEnabled="true">
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/edit_search_string"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/search_add_custom_engine_search_string_hint"
|
||||||
|
android:inputType="text" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/search_add_custom_engine_search_string_example"
|
||||||
|
android:lineHeight="18sp"
|
||||||
|
android:textColor="@android:color/tertiary_text_dark" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/custom_search_engines_learn_more"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/exceptions_empty_message_learn_more_link"
|
||||||
|
android:textColor="?accent"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/exceptions_empty_message" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress"
|
||||||
|
style="@style/Widget.AppCompat.ProgressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
</LinearLayout>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_height="@dimen/search_engine_radio_button_height"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:background="?android:selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radio_button"
|
||||||
|
android:layout_width="@dimen/search_engine_radio_button_height"
|
||||||
|
android:layout_height="@dimen/search_engine_radio_button_height"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:textAlignment="textStart"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||||
|
android:layout_marginStart="@dimen/radio_button_padding_horizontal"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/engine_text"
|
||||||
|
android:textColor="?primaryText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/radio_button_padding_horizontal"
|
||||||
|
android:text="@string/search_add_custom_engine_label"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/radio_button"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/radio_button"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/radio_button"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/search_engine_scrollview"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context="org.mozilla.fenix.settings.search.AddSearchEngineFragment">
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/search_engine_wrapper"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/search_engine_group"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
<include layout="@layout/custom_search_engine" />
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<include layout="@layout/custom_search_engine" />
|
||||||
|
</ScrollView>
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_height="@dimen/search_engine_radio_button_height"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:background="?android:selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/add_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:textAlignment="textStart"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:src="@drawable/ic_new"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/add_engine_text"
|
||||||
|
android:textColor="?primaryText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="18dp"
|
||||||
|
android:layout_marginEnd="@dimen/radio_button_padding_horizontal"
|
||||||
|
android:text="@string/search_engine_add_custom_search_engine_title"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/add_icon"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,4 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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
|
- 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/. -->
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
|
||||||
|
@ -39,4 +40,14 @@
|
||||||
app:layout_constraintStart_toEndOf="@id/engine_icon"
|
app:layout_constraintStart_toEndOf="@id/engine_icon"
|
||||||
app:layout_constraintTop_toTopOf="@id/engine_icon"
|
app:layout_constraintTop_toTopOf="@id/engine_icon"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/overflow_menu"
|
||||||
|
android:layout_width="@dimen/glyph_button_width"
|
||||||
|
android:layout_height="@dimen/glyph_button_height"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/content_description_menu"
|
||||||
|
android:src="@drawable/ic_menu"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/add_search_engine"
|
||||||
|
android:icon="@drawable/mozac_ic_check"
|
||||||
|
app:iconTint="?primaryText"
|
||||||
|
android:title="@string/search_engine_add_button_content_description"
|
||||||
|
app:showAsAction="always" />
|
||||||
|
</menu>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/save_button"
|
||||||
|
android:icon="@drawable/mozac_ic_check"
|
||||||
|
app:iconTint="?primaryText"
|
||||||
|
android:title="@string/search_engine_add_custom_search_engine_edit_button_content_description"
|
||||||
|
app:showAsAction="always" />
|
||||||
|
</menu>
|
|
@ -428,7 +428,14 @@
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/searchEngineFragment"
|
android:id="@+id/searchEngineFragment"
|
||||||
android:name="org.mozilla.fenix.settings.search.SearchEngineFragment"
|
android:name="org.mozilla.fenix.settings.search.SearchEngineFragment"
|
||||||
android:label="@string/preferences_search" />
|
android:label="@string/preferences_search">
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_searchEngineFragment_to_addSearchEngineFragment"
|
||||||
|
app:destination="@+id/addSearchEngineFragment" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_searchEngineFragment_to_editCustomSearchEngineFragment"
|
||||||
|
app:destination="@+id/editCustomSearchEngineFragment" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/turnOnSyncFragment"
|
android:id="@+id/turnOnSyncFragment"
|
||||||
|
@ -659,4 +666,16 @@
|
||||||
android:name="savedLoginItem"
|
android:name="savedLoginItem"
|
||||||
app:argType="org.mozilla.fenix.logins.SavedLoginsItem" />
|
app:argType="org.mozilla.fenix.logins.SavedLoginsItem" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/addSearchEngineFragment"
|
||||||
|
android:name="org.mozilla.fenix.settings.search.AddSearchEngineFragment"
|
||||||
|
android:label="AddSearchEngineFragment" />
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/editCustomSearchEngineFragment"
|
||||||
|
android:name="org.mozilla.fenix.settings.search.EditCustomSearchEngineFragment"
|
||||||
|
android:label="EditCustomSearchEngineFragment">
|
||||||
|
<argument
|
||||||
|
android:name="searchEngineIdentifier"
|
||||||
|
app:argType="string" />
|
||||||
|
</fragment>
|
||||||
</navigation>
|
</navigation>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<string name="pref_key_make_default_browser" translatable="false">pref_key_make_default_browser</string>
|
<string name="pref_key_make_default_browser" translatable="false">pref_key_make_default_browser</string>
|
||||||
<string name="pref_key_search_settings" translatable="false">pref_key_search_settings</string>
|
<string name="pref_key_search_settings" translatable="false">pref_key_search_settings</string>
|
||||||
<string name="pref_key_search_engine" translatable="false">pref_key_search_engine</string>
|
<string name="pref_key_search_engine" translatable="false">pref_key_search_engine</string>
|
||||||
|
<string name="pref_key_add_search_engine" translatable="false">pref_key_add_Search_engine</string>
|
||||||
<string name="pref_key_passwords" translatable="false">pref_key_passwords</string>
|
<string name="pref_key_passwords" translatable="false">pref_key_passwords</string>
|
||||||
<string name="pref_key_credit_cards_addresses" translatable="false">pref_key_credit_cards_addresses</string>
|
<string name="pref_key_credit_cards_addresses" translatable="false">pref_key_credit_cards_addresses</string>
|
||||||
<string name="pref_key_site_permissions" translatable="false">pref_key_site_permissions</string>
|
<string name="pref_key_site_permissions" translatable="false">pref_key_site_permissions</string>
|
||||||
|
@ -67,6 +68,7 @@
|
||||||
<string name="pref_key_search_widget_installed" translatable="false">pref_key_search_widget_installed</string>
|
<string name="pref_key_search_widget_installed" translatable="false">pref_key_search_widget_installed</string>
|
||||||
|
|
||||||
<!-- Search Settings -->
|
<!-- Search Settings -->
|
||||||
|
<string name="pref_key_search_engine_list" translatable="false">pref_key_search_engine_list</string>
|
||||||
<string name="pref_key_show_search_shortcuts" translatable="false">pref_key_show_search_shortcuts</string>
|
<string name="pref_key_show_search_shortcuts" translatable="false">pref_key_show_search_shortcuts</string>
|
||||||
<string name="pref_key_show_search_suggestions" translatable="false">pref_key_show_search_suggestions</string>
|
<string name="pref_key_show_search_suggestions" translatable="false">pref_key_show_search_suggestions</string>
|
||||||
<string name="pref_key_show_clipboard_suggestions" translatable="false">pref_key_show_clipboard_suggestions</string>
|
<string name="pref_key_show_clipboard_suggestions" translatable="false">pref_key_show_clipboard_suggestions</string>
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
<!--suppress CheckTagEmptyBody This is a default value for places where we don't want a string set-->
|
<!--suppress CheckTagEmptyBody This is a default value for places where we don't want a string set-->
|
||||||
<string name="empty_string" translatable="false"></string>
|
<string name="empty_string" translatable="false"></string>
|
||||||
|
|
||||||
|
|
||||||
<!-- Title for Accessibility Force Enable Zoom Preference -->
|
<!-- Title for Accessibility Force Enable Zoom Preference -->
|
||||||
<string name="preference_accessibility_force_enable_zoom" translatable="false">Zoom on all websites</string>
|
<string name="preference_accessibility_force_enable_zoom" translatable="false">Zoom on all websites</string>
|
||||||
<!-- Summary for Accessibility Force Enable Zoom Preference -->
|
<!-- Summary for Accessibility Force Enable Zoom Preference -->
|
||||||
|
|
|
@ -1014,4 +1014,45 @@
|
||||||
<string name="logins_warning_dialog_set_up_now">Set up now</string>
|
<string name="logins_warning_dialog_set_up_now">Set up now</string>
|
||||||
<!-- Title of PIN verification dialog to direct users to re-enter their device credentials to access their logins -->
|
<!-- Title of PIN verification dialog to direct users to re-enter their device credentials to access their logins -->
|
||||||
<string name="logins_biometric_prompt_message_pin">Unlock your device</string>
|
<string name="logins_biometric_prompt_message_pin">Unlock your device</string>
|
||||||
|
|
||||||
|
<!-- Title of the Add search engine screen -->
|
||||||
|
<string name="search_engine_add_custom_search_engine_title">Add search engine</string>
|
||||||
|
<!-- Title of the Edit search engine screen -->
|
||||||
|
<string name="search_engine_edit_custom_search_engine_title">Edit search engine</string>
|
||||||
|
<!-- Content description (not visible, for screen readers etc.): Title for the button to add a search engine in the action bar -->
|
||||||
|
<string name="search_engine_add_button_content_description">Add</string>
|
||||||
|
<!-- Content description (not visible, for screen readers etc.): Title for the button to save a search engine in the action bar -->
|
||||||
|
<string name="search_engine_add_custom_search_engine_edit_button_content_description">Save</string>
|
||||||
|
<!-- Text for the menu button to edit a search engine -->
|
||||||
|
<string name="search_engine_edit">Edit</string>
|
||||||
|
<!-- Text for the menu button to delete a search engine -->
|
||||||
|
<string name="search_engine_delete">Delete</string>
|
||||||
|
|
||||||
|
<!-- Text for the button to create a custom search engine on the Add search engine screen -->
|
||||||
|
<string name="search_add_custom_engine_label">Custom</string>
|
||||||
|
<!-- Placeholder text shown in the Search Engine Name TextField before a user enters text -->
|
||||||
|
<string name="search_add_custom_engine_name_hint">Name</string>
|
||||||
|
<!-- Placeholder text shown in the Search String TextField before a user enters text -->
|
||||||
|
<string name="search_add_custom_engine_search_string_hint">Search string to use</string>
|
||||||
|
<!-- Description text for the Search String TextField. The %s is part of the string -->
|
||||||
|
<string name="search_add_custom_engine_search_string_example">Replace query with “%s”. Example:\nhttps://www.google.com/search?q=%s</string>
|
||||||
|
<!-- Text for the button to learn more about adding a custom search engine -->
|
||||||
|
<string name="search_add_custom_engine_learn_more_label">Learn More</string>
|
||||||
|
|
||||||
|
<!-- Text shown when a user leaves the name field empty -->
|
||||||
|
<string name="search_add_custom_engine_error_empty_name">Enter search engine name</string>
|
||||||
|
<!-- Text shown when a user tries to add a search engine that already exists -->
|
||||||
|
<string name="search_add_custom_engine_error_existing_name">Search engine with name “%s” already exists.</string>
|
||||||
|
<!-- Text shown when a user leaves the search string field empty -->
|
||||||
|
<string name="search_add_custom_engine_error_empty_search_string">Enter a search string</string>
|
||||||
|
<!-- Text shown when a user leaves out the required template string -->
|
||||||
|
<string name="search_add_custom_engine_error_missing_template">Check that search string matches Example format</string>
|
||||||
|
<!-- Text shown when we aren't able to validate the custom search query. The first parameter is the url of the custom search engine -->
|
||||||
|
<string name="search_add_custom_engine_error_cannot_reach">Error connecting to “%s”</string>
|
||||||
|
<!-- Text shown when a user creates a new search engine -->
|
||||||
|
<string name="search_add_custom_engine_success_message">Created %s</string>
|
||||||
|
<!-- Text shown when a user successfully edits a custom search engine -->
|
||||||
|
<string name="search_edit_custom_engine_success_message">Saved %s</string>
|
||||||
|
<!-- Text shown when a user successfully deletes a custom search engine -->
|
||||||
|
<string name="search_delete_search_engine_success_message">Deleted %s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -354,4 +354,8 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="ShareDialogStyle" parent="DialogStyleBase"/>
|
<style name="ShareDialogStyle" parent="DialogStyleBase"/>
|
||||||
|
|
||||||
|
<style name="EngineTextField" parent="TextAppearance.AppCompat">
|
||||||
|
<item name="android:textSize">14sp</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -10,7 +10,12 @@
|
||||||
android:selectable="false"
|
android:selectable="false"
|
||||||
app:iconSpaceReserved="false">
|
app:iconSpaceReserved="false">
|
||||||
<org.mozilla.fenix.settings.search.RadioSearchEngineListPreference
|
<org.mozilla.fenix.settings.search.RadioSearchEngineListPreference
|
||||||
|
android:key="@string/pref_key_search_engine_list"
|
||||||
android:selectable="false"/>
|
android:selectable="false"/>
|
||||||
|
<Preference
|
||||||
|
android:key="@string/pref_key_add_search_engine"
|
||||||
|
android:title="@string/search_engine_add_custom_search_engine_title"
|
||||||
|
android:layout="@layout/preference_search_add_engine"/>
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:defaultValue="true"
|
android:defaultValue="true"
|
||||||
android:key="@string/pref_key_show_search_suggestions"
|
android:key="@string/pref_key_show_search_suggestions"
|
||||||
|
|
Loading…
Reference in New Issue