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 statestaging
parent
8abf580579
commit
607c3d4c87
@ -0,0 +1,14 @@
|
|||||||
|
<?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>Ecosia</ShortName>
|
||||||
|
<Description>Search Ecosia</Description>
|
||||||
|
<InputEncoding>UTF-8</InputEncoding>
|
||||||
|
<Contact>info@ecosia.org</Contact>
|
||||||
|
<LongName>Ecosia Search</LongName>
|
||||||
|
<Image width="16" height="16">data:image/png;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAACMuAAAjLgAAAAAAAAAAAAAAAAAAAAAAAAAAAAC8qzQBuaw3UrmsN6u5rDfruaw37bmsN+25rDfSuaw3fLmsNyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC2rTokrLFGurqsNv+5rDf/uaw3/7msN/+5rDf/uaw3/7urNP/AqS7suqw2aAAAAAAAAAAAAAAAAAAAAAC/qjApkbpn4mvJlf/EqCr/uaw3/7msN/+5rDf/uaw3/7urNP+rsUj/ib5x/7qsNv+9qzKBAAAAAAAAAAC5rDcLwKkvzom9cf813Nb/lrlh/8KoLP+5rDf/uaw3/7msN//BqS3/eMSF/yXj6v+BwHv/lbli/7atO1IAAAAAuaw3bsCqL/+Rumb/K+Di/z3ZzP+dtln/vqox/7msN/+5rDf/waku/23Ikv8s4OH/ONvS/5m4Xv+7qzXZuaw3CbmsN9DBqS7/hL93/zDe3f8v393/RdbD/7OuPv+7qzX/uqw2/8WoKf99wn//Lt/e/y/e3f99wn//v6ow/7msN0+7qzT7s64+/0bWwf8y3tn/L97d/03TuP+usET/vKoz/7isOP+vr0P/XM6n/zDe3P813Nb/L97d/5O6Zf/EpymOu6s0/7OuPv8+2cv/J+Hn/1HStP+0rjz/vasy/76qMP9zxYr/NtzV/zTd1/823NX/NtzV/zLd2f9I1b//mbheqsGpLf+gtVX/bseR/3fEhv+wr0L/vaoy/7msN/+/qjD/Wc+q/yvg4/813Nb/Md7b/zfc1P833NT/Mt7a/zbc1aqHvnT6bMiT/522WP+wr0L/vqox/7msN/+5rDf/vaoy/6C1VP8/2cr/N9zT/2vJlf9hzKD/NtzU/zbc1f813NaONdzWz3HGjv9ky53/prNN/8SoKv+8qzT/uaw3/7msOP/EqCr/ecOE/0HYx/9V0K//N9vT/zXc1v823NX/NtzVTjXc120w3tz/Lt/e/0zUu/+Fv3X/rrBF/7msN/+7qzX/vaoy/6qxSf9G1sH/L9/d/zPd2P8x3tv/L9/e2C/f3Qk23NUKNtzVzDbc1v823NX/OdvQ/0nVvv+xr0H/ta07/7+qL/+7qzT/r69D/2LMoP823NX/VNGx/2TLnVEAAAAAAAAAADbc1Sc03dfgQNnJ/2bKm/862tD/pLRP/1vOqf9S0rP/ib1x/8CpL/+4rDj/qLJM/7qsNn4AAAAAAAAAAAAAAAAAAAAAM93YI0vUvLtux5H/VdGw/3DHj/9Zz6r/Xc2m/3rDgv+5rDf/u6s1672rM2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyaYjUburNaytsUbZuK056cGpLuS/qjDGuaw3gLmsNx4AAAAAAAAAAAAAAAAAAAAA+D8AAOAPAADAAwAAgAMAAIABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAACAAQAAgAMAAMAHAADgDwAA+B8AAA==</Image>
|
||||||
|
<Url type="text/html" method="get" template="https://www.ecosia.org/search?q={searchTerms}&addon=opensearch"/>
|
||||||
|
<Url type="application/x-suggestions+json" template="https://ac.ecosia.org/autocomplete?q={searchTerms}&type=list"/>
|
||||||
|
</SearchPlugin>
|
@ -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/. -->
|
||||||
|
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
|
||||||
|
<ShortName>Reddit</ShortName>
|
||||||
|
<Description>Search Reddit</Description>
|
||||||
|
<LongName>Reddit Search</LongName>
|
||||||
|
<Image width="16" height="16">data:image/png;base64,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</Image>
|
||||||
|
<Url type="text/html" method="get" template="https://www.reddit.com/search/?q={searchTerms}"/>
|
||||||
|
</SearchPlugin>
|
@ -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>
|
@ -0,0 +1,15 @@
|
|||||||
|
<?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>Yahoo</ShortName>
|
||||||
|
<Description>Get the best of the web with Yahoo</Description>
|
||||||
|
<InputEncoding>UTF-8</InputEncoding>
|
||||||
|
<LongName>Yahoo</LongName>
|
||||||
|
<Language>*</Language>
|
||||||
|
<Image height="16" width="16">data:image/png;base64,AAABAAIAEBAAAAEAIABoBAAAJgAAAAgIAAABACAASAEAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewE4824BNLJvADJ1cAQ3UW8IOCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwCDknbQQ1UW8AM3VuATSyegE383sAN/91ADT/dQA1/3QANf90ATX/dAI2/3QDNv90Azb/dAM2/3QDNv9zAjb/cwI1/3MBNP93ADb/dAA0/3oAN/99ADj/ewA3/3sBN/99Ajn/gAM7/4IEPf+BBT3/egAy/3kAMv9/BTz/gAU8/30EO/+AAjv/fQE5/3gANv98ADj/fgA4/4IAO/+EAjz/hgM9/4kEQP+LBkH/hwA6//Tl7P/05u3/hgA6/4oGQf+JBUD/iQQ//4cCPf+BATr/fgA4/4EAOf+LAT//jANA/44EQv+SBkT/kgdF/5IAQf/s0t7/7dTf/5IAQf+VCEf/kwdG/5IFRP+PBEH/iQI+/34BOf+AADr/lAFC/5YDRf+ZBUb/mwdJ/50IS/+bA0b/6srZ/+vM2v+bA0b/mwpK/5sISf+bBkj/mQVG/4wDQP+AADr/hgA8/5oCRf+gBEj/owZL/6UITf+nCk//pgVM/+nB0//pwtT/pgVN/6kKUf+lCU7/owdM/54FSP+WA0X/gQE6/4YAPP+fAkj/qgRO/6wHT/+uCFH/rgtS/60BTv/z2eT/8tnl/60CTv+wC1X/rQlS/6wIUP+pBk7/lgRF/4YBPP+HAD3/pwNL/7MFUf+0BlP/tAhU/7cLVv/HQ33////////////KRYD/tgtW/7cJVv+0CFP/rAVP/5sER/+GAT3/iAE9/60CTv++BFb/vwZY/8AIWf+9AFH/9tzn//TX5P/01uT/993p/7wAUv/ACFr/vwdY/7QEUv+hA0n/gwE7/4oAPf+0AlD/xQNZ/8QFWf/EB1v/2ViR///////RQID/0T+A///////ZW5L/xwhd/8YGW/+9BFb/pQJK/4EAOv+NAD//twFS/8gCWv/JBFv/xgBT//34+v/wvdP/xABX/8UBV//wu9P//vn7/8cAVP/KBF3/wANX/6kCTP+BADr/iwA+/7cAUv/HAVr/xQFY/+OIsP//////xBxn/74GWP+/B1n/xBpm///////jirH/xgFZ/8ICV/+vAE//igA9/4gAPP+qAEz/tABR/68AS//SfqT/xlmK/6oBTP+qBU7/qQRN/6gCTP/GV4j/0nui/7EATP+vAU7/mgBF/4MAO/+CADr/jAA+/5wARf+IAD3/jgFA/4ABO/99Ajr/gwI9/4cCPv9/Ajv/jAFA/4oBPv+UAEL/mQBE/4cAPf9/ADn/fAE483oBOLR1ADZ2cwQ3UnAHOCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqCDUndQQ4UoAAOnZ7ATi0fAE48z/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAoAAAACAAAABAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbwAz/3IANMpzAjWIagApiGoAKYhzAjWIcgA0ym8AM/93ADX/ggI6/4IDPP/Tkq3/05Gs/4IDPP+DAjr/dwA1/38BOPyVBET/lwZH/8uDo//Lg6P/mAZH/5UERP9/ATj8iAE976YFS/+nBU7/4KC4/96ctf+oBU7/pQVM/4gBPe+MAT/vtgVT/7sWW//8+fj/+evw/7sWXP+1BVL/jAE/75UBQ/zFAFX/68LV/9ZYj//WV43/5a3F/8YAVv+UAUL8kwBA/8tUhP/12OX/tQBN/7YATf/22+j/ylGB/5IAQP93ADb/hAA4yoIAMomCAjuHggI6iIIAMoiDADfKdgA2/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</Image>
|
||||||
|
<Url rel="results" type="text/html" method="get" template="https://search.yahoo.com/search?p={searchTerms}&fr=opensearch"/>
|
||||||
|
<Url rel="suggestions" type="application/json" template="https://search.yahoo.com/sugg/os?command={searchTerms}&output=fxjson&fr=opensearch"/>
|
||||||
|
<Url rel="self" type="application/opensearchdescription+xml" template="https://search.yahoo.com/opensearch.xml"/>
|
||||||
|
</SearchPlugin>
|
@ -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>YouTube</ShortName>
|
||||||
|
<Description>Search for videos on YouTube</Description>
|
||||||
|
<Tags>youtube video</Tags>
|
||||||
|
<Image height="16" width="16">data:image/png;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAD/EAAA/0AAAP9AAAD/cAAA/4AAAP+AAAD/gAAA/4AAAP+AAAD/QAAA/0AAAP8Q////AP///wD///8AAAD/YAAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA/2D///8AAAD/MAAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/MAAA/1AAAP//AAD//wAA//8AAP//AAD//wAA//8QEP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA/2AAAP+AAAD//wAA//8AAP//AAD//wAA//8AAP//4OD//1BQ//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP+AAAD/gAAA//8AAP//AAD//wAA//8AAP//AAD/////////////wMD//yAg//8AAP//AAD//wAA//8AAP//AAD/gAAA/4AAAP//AAD//wAA//8AAP//AAD//wAA/////////////7Cw//8gIP//AAD//wAA//8AAP//AAD//wAA/4AAAP+AAAD//wAA//8AAP//AAD//wAA//8AAP//4OD//0BA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP+AAAD/UAAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/YAAA/zAAAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA/zD///8AAAD/YAAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA/2D///8A////AP///wAAAP8QAAD/QAAA/0AAAP+AAAD/gAAA/4AAAP+AAAD/gAAA/4AAAP9AAAD/QAAA/xD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A//8AAP//AADAAwAAgAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAEAAMADAAD//wAA//8AAA==</Image>
|
||||||
|
<Url type="text/html" template="https://www.youtube.com/results?search_query={searchTerms}&page={startPage?}&utm_source=opensearch" />
|
||||||
|
<Query role="example" searchTerms="cat" />
|
||||||
|
</SearchPlugin>
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -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>
|
Loading…
Reference in New Issue