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"
|
||||
applicationIdSuffix ".firefox"
|
||||
manifestPlaceholders = [
|
||||
// This release type is meant to replace Firefox (Release channel) and therefore needs to inherit
|
||||
// its sharedUserId for all eternity. See:
|
||||
// https://searchfox.org/mozilla-central/search?q=moz_android_shared_id&case=false®exp=false&path=
|
||||
// Shipping an app update without sharedUserId can have
|
||||
// fatal consequences. For example see:
|
||||
// - https://issuetracker.google.com/issues/36924841
|
||||
// - https://issuetracker.google.com/issues/36905922
|
||||
"sharedUserId": "org.mozilla.firefox.sharedID"
|
||||
// This release type is meant to replace Firefox (Release channel) and therefore needs to inherit
|
||||
// its sharedUserId for all eternity. See:
|
||||
// https://searchfox.org/mozilla-central/search?q=moz_android_shared_id&case=false®exp=false&path=
|
||||
// Shipping an app update without sharedUserId can have
|
||||
// fatal consequences. For example see:
|
||||
// - https://issuetracker.google.com/issues/36924841
|
||||
// - https://issuetracker.google.com/issues/36905922
|
||||
"sharedUserId": "org.mozilla.firefox.sharedID"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ android {
|
|||
|
||||
sourceSets {
|
||||
androidTest {
|
||||
resources.srcDirs += ['src/androidTest/resources']
|
||||
resources.srcDirs += ['src/androidTest/resources']
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -544,22 +544,22 @@ if (project.hasProperty("coverage")) {
|
|||
task printVariants {
|
||||
doLast {
|
||||
def variants = android.applicationVariants.collect {[
|
||||
apks: it.variantData.outputScope.apkDatas.collect {[
|
||||
abi: it.filters.find { it.filterType == 'ABI' }.identifier,
|
||||
fileName: it.outputFileName,
|
||||
]},
|
||||
build_type: it.buildType.name,
|
||||
engine: it.productFlavors.find { it.dimension == 'engine' }.name,
|
||||
name: it.name,
|
||||
apks: it.variantData.outputScope.apkDatas.collect {[
|
||||
abi: it.filters.find { it.filterType == 'ABI' }.identifier,
|
||||
fileName: it.outputFileName,
|
||||
]},
|
||||
build_type: it.buildType.name,
|
||||
engine: it.productFlavors.find { it.dimension == 'engine' }.name,
|
||||
name: it.name,
|
||||
]}
|
||||
println 'variants: ' + groovy.json.JsonOutput.toJson(variants)
|
||||
}
|
||||
}
|
||||
|
||||
def glean_android_components_tag = (
|
||||
Versions.mozilla_android_components.endsWith('-SNAPSHOT') ?
|
||||
'master' :
|
||||
'v' + Versions.mozilla_android_components
|
||||
Versions.mozilla_android_components.endsWith('-SNAPSHOT') ?
|
||||
'master' :
|
||||
'v' + Versions.mozilla_android_components
|
||||
)
|
||||
|
||||
// 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.launch
|
||||
import mozilla.components.browser.search.SearchEngineManager
|
||||
import mozilla.components.browser.search.provider.AssetsSearchEngineProvider
|
||||
import mozilla.components.browser.search.provider.localization.LocaleSearchLocalizationProvider
|
||||
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.test.Mockable
|
||||
|
||||
|
@ -19,15 +18,15 @@ import org.mozilla.fenix.test.Mockable
|
|||
*/
|
||||
@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 lazy {
|
||||
SearchEngineManager(
|
||||
coroutineContext = IO, providers = listOf(
|
||||
AssetsSearchEngineProvider(LocaleSearchLocalizationProvider())
|
||||
)
|
||||
coroutineContext = IO,
|
||||
providers = listOf(provider)
|
||||
).apply {
|
||||
registerForLocaleUpdates(context)
|
||||
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) {
|
||||
val iconSize = resources.getDimension(R.dimen.preference_icon_drawable_size).toInt()
|
||||
|
||||
val searchEngine = requireComponents.search.searchEngineManager.getDefaultSearchEngineAsync(
|
||||
requireContext(),
|
||||
requireContext().settings().defaultSearchEngineName
|
||||
)
|
||||
val searchEngine = requireComponents.search.provider.getDefaultEngine(requireContext())
|
||||
val searchIcon = BitmapDrawable(resources, searchEngine.icon)
|
||||
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 url = session?.url.orEmpty()
|
||||
val currentSearchEngine = SearchEngineSource.Default(
|
||||
requireComponents.search.searchEngineManager.getDefaultSearchEngine(requireContext())
|
||||
requireComponents.search.provider.getDefaultEngine(requireContext())
|
||||
)
|
||||
|
||||
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.
|
||||
// When returning from that settings screen we need to update it to account for any changes.
|
||||
val currentDefaultEngine =
|
||||
requireComponents.search.searchEngineManager.getDefaultSearchEngine(
|
||||
requireContext(),
|
||||
requireContext().settings().defaultSearchEngineName
|
||||
)
|
||||
requireComponents.search.provider.getDefaultEngine(requireContext())
|
||||
|
||||
if (searchStore.state.defaultEngineSource.searchEngine != currentDefaultEngine) {
|
||||
searchStore.dispatch(
|
||||
|
|
|
@ -168,7 +168,7 @@ class AwesomeBarView(
|
|||
|
||||
shortcutsEnginePickerProvider =
|
||||
ShortcutsSuggestionProvider(
|
||||
components.search.searchEngineManager,
|
||||
components.search.provider,
|
||||
this,
|
||||
interactor::onSearchShortcutEngineSelected,
|
||||
interactor::onClickSearchEngineSettings
|
||||
|
@ -329,10 +329,7 @@ class AwesomeBarView(
|
|||
|
||||
searchSuggestionProviderMap.put(
|
||||
engine, SearchSuggestionProvider(
|
||||
components.search.searchEngineManager.getDefaultSearchEngine(
|
||||
this,
|
||||
engine.name
|
||||
),
|
||||
components.search.provider.getDefaultEngine(this),
|
||||
shortcutSearchUseCase,
|
||||
components.core.client,
|
||||
limit = 3,
|
||||
|
|
|
@ -7,16 +7,16 @@ package org.mozilla.fenix.search.awesomebar
|
|||
import android.content.Context
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import mozilla.components.browser.search.SearchEngine
|
||||
import mozilla.components.browser.search.SearchEngineManager
|
||||
import mozilla.components.concept.awesomebar.AwesomeBar
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* A [AwesomeBar.SuggestionProvider] implementation that provides search engine suggestions.
|
||||
*/
|
||||
class ShortcutsSuggestionProvider(
|
||||
private val searchEngineManager: SearchEngineManager,
|
||||
private val searchEngineProvider: FenixSearchEngineProvider,
|
||||
private val context: Context,
|
||||
private val selectShortcutEngine: (engine: SearchEngine) -> Unit,
|
||||
private val selectShortcutEngineSettings: () -> Unit
|
||||
|
@ -33,7 +33,7 @@ class ShortcutsSuggestionProvider(
|
|||
override suspend fun onInputChanged(text: String): List<AwesomeBar.Suggestion> {
|
||||
val suggestions = mutableListOf<AwesomeBar.Suggestion>()
|
||||
|
||||
searchEngineManager.getSearchEngines(context).forEach {
|
||||
searchEngineProvider.installedSearchEngines(context).list.forEach {
|
||||
suggestions.add(
|
||||
AwesomeBar.Suggestion(
|
||||
provider = this,
|
||||
|
|
|
@ -30,7 +30,8 @@ object SupportUtils {
|
|||
TRACKING_PROTECTION("tracking-protection-firefox-preview"),
|
||||
WHATS_NEW("whats-new-firefox-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 androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SwitchPreference
|
||||
import org.mozilla.fenix.R
|
||||
|
@ -49,10 +51,26 @@ class SearchEngineFragment : PreferenceFragmentCompat() {
|
|||
isChecked = context.settings().shouldShowClipboardSuggestions
|
||||
}
|
||||
|
||||
val searchEngineListPreference =
|
||||
findPreference<SearchEngineListPreference>(getPreferenceKey(R.string.pref_key_search_engine_list))
|
||||
|
||||
searchEngineListPreference?.reload(requireContext())
|
||||
searchSuggestionsPreference?.onPreferenceChangeListener = SharedPreferenceUpdater()
|
||||
showSearchShortcuts?.onPreferenceChangeListener = SharedPreferenceUpdater()
|
||||
showHistorySuggestions?.onPreferenceChangeListener = SharedPreferenceUpdater()
|
||||
showBookmarkSuggestions?.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.RadioGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
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.*
|
||||
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.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
|
||||
|
||||
abstract class SearchEngineListPreference @JvmOverloads constructor(
|
||||
context: Context,
|
||||
|
@ -28,7 +35,7 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
|||
defStyleAttr: Int = android.R.attr.preferenceStyle
|
||||
) : Preference(context, attrs, defStyleAttr), CompoundButton.OnCheckedChangeListener {
|
||||
|
||||
protected var searchEngines: List<SearchEngine> = emptyList()
|
||||
protected lateinit var searchEngineList: SearchEngineList
|
||||
protected var searchEngineGroup: RadioGroup? = null
|
||||
|
||||
protected abstract val itemResId: Int
|
||||
|
@ -40,11 +47,11 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
|||
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
|
||||
super.onBindViewHolder(holder)
|
||||
searchEngineGroup = holder!!.itemView.findViewById(R.id.search_engine_group)
|
||||
val context = searchEngineGroup!!.context
|
||||
|
||||
searchEngines = context.components.search.searchEngineManager.getSearchEngines(context)
|
||||
.sortedBy { it.name }
|
||||
reload(searchEngineGroup!!.context)
|
||||
}
|
||||
|
||||
fun reload(context: Context) {
|
||||
searchEngineList = context.components.search.provider.installedSearchEngines(context)
|
||||
refreshSearchEngineViews(context)
|
||||
}
|
||||
|
||||
|
@ -59,18 +66,10 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
|||
return
|
||||
}
|
||||
|
||||
// To get the default search engine we have to pass in a name that doesn't exist
|
||||
// https://github.com/mozilla-mobile/android-components/issues/3344
|
||||
val defaultSearchEngine = context.components.search.searchEngineManager.getDefaultSearchEngine(
|
||||
context,
|
||||
THIS_IS_A_HACK_FIX_ME
|
||||
)
|
||||
|
||||
val selectedSearchEngine =
|
||||
context.components.search.searchEngineManager.getDefaultSearchEngine(
|
||||
context,
|
||||
context.settings().defaultSearchEngineName
|
||||
).identifier
|
||||
val defaultEngine = context.components.search.provider.getDefaultEngine(context).identifier
|
||||
val selectedEngine = (searchEngineList.list.find {
|
||||
it.identifier == defaultEngine
|
||||
} ?: searchEngineList.list.first()).identifier
|
||||
|
||||
searchEngineGroup!!.removeAllViews()
|
||||
|
||||
|
@ -82,31 +81,57 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
|||
|
||||
val setupSearchEngineItem: (Int, SearchEngine) -> Unit = { index, engine ->
|
||||
val engineId = engine.identifier
|
||||
val engineItem = makeButtonFromSearchEngine(engine, layoutInflater, context.resources)
|
||||
engineItem.id = index
|
||||
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 == selectedSearchEngine) {
|
||||
if (engineId == selectedEngine) {
|
||||
updateDefaultItem(engineItem.radio_button)
|
||||
}
|
||||
searchEngineGroup!!.addView(engineItem, layoutParams)
|
||||
}
|
||||
|
||||
setupSearchEngineItem(0, defaultSearchEngine)
|
||||
searchEngineList.default?.apply {
|
||||
setupSearchEngineItem(0, this)
|
||||
}
|
||||
|
||||
searchEngines
|
||||
.filter { it.identifier != defaultSearchEngine.identifier }
|
||||
searchEngineList.list
|
||||
.filter { it.identifier != searchEngineList.default?.identifier }
|
||||
.sortedBy { it.name }
|
||||
.forEachIndexed(setupSearchEngineItem)
|
||||
}
|
||||
|
||||
private fun makeButtonFromSearchEngine(
|
||||
engine: SearchEngine,
|
||||
layoutInflater: LayoutInflater,
|
||||
res: Resources
|
||||
res: Resources,
|
||||
allowDeletion: Boolean
|
||||
): View {
|
||||
val isCustomSearchEngine = CustomSearchEngineStore.isCustomSearchEngine(context, engine.identifier)
|
||||
|
||||
val wrapper = layoutInflater.inflate(itemResId, null) as ConstraintLayout
|
||||
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)
|
||||
}
|
||||
}
|
||||
).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)
|
||||
|
@ -115,7 +140,7 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
||||
searchEngines.forEach { engine ->
|
||||
searchEngineList.list.forEach { engine ->
|
||||
val wrapper: ConstraintLayout = searchEngineGroup?.findViewWithTag(engine.identifier) ?: return
|
||||
|
||||
when (wrapper.radio_button == buttonView) {
|
||||
|
@ -129,7 +154,55 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val THIS_IS_A_HACK_FIX_ME = "."
|
||||
private fun editCustomSearchEngine(engine: SearchEngine) {
|
||||
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
|
||||
- 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_constraintTop_toTopOf="@id/engine_icon"
|
||||
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>
|
||||
|
|
|
@ -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
|
||||
android:id="@+id/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
|
||||
android:id="@+id/turnOnSyncFragment"
|
||||
|
@ -659,4 +666,16 @@
|
|||
android:name="savedLoginItem"
|
||||
app:argType="org.mozilla.fenix.logins.SavedLoginsItem" />
|
||||
</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>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
<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_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_credit_cards_addresses" translatable="false">pref_key_credit_cards_addresses</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>
|
||||
|
||||
<!-- 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_suggestions" translatable="false">pref_key_show_search_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-->
|
||||
<string name="empty_string" translatable="false"></string>
|
||||
|
||||
|
||||
<!-- Title for Accessibility Force Enable Zoom Preference -->
|
||||
<string name="preference_accessibility_force_enable_zoom" translatable="false">Zoom on all websites</string>
|
||||
<!-- Summary for Accessibility Force Enable Zoom Preference -->
|
||||
|
|
|
@ -1014,4 +1014,45 @@
|
|||
<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 -->
|
||||
<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>
|
||||
|
|
|
@ -354,4 +354,8 @@
|
|||
</style>
|
||||
|
||||
<style name="ShareDialogStyle" parent="DialogStyleBase"/>
|
||||
|
||||
<style name="EngineTextField" parent="TextAppearance.AppCompat">
|
||||
<item name="android:textSize">14sp</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
@ -10,7 +10,12 @@
|
|||
android:selectable="false"
|
||||
app:iconSpaceReserved="false">
|
||||
<org.mozilla.fenix.settings.search.RadioSearchEngineListPreference
|
||||
android:key="@string/pref_key_search_engine_list"
|
||||
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
|
||||
android:defaultValue="true"
|
||||
android:key="@string/pref_key_show_search_suggestions"
|
||||
|
|
Loading…
Reference in New Issue