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
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