2019-11-20 01:30:56 +01:00
|
|
|
/* 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 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
|
2020-02-06 17:14:32 +01:00
|
|
|
import androidx.fragment.app.Fragment
|
2019-11-20 01:30:56 +01:00
|
|
|
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
|
2020-01-16 19:03:18 +01:00
|
|
|
import org.mozilla.fenix.HomeActivity
|
2019-11-20 01:30:56 +01:00
|
|
|
import org.mozilla.fenix.R
|
|
|
|
import org.mozilla.fenix.components.FenixSnackbar
|
2019-12-03 14:54:57 +01:00
|
|
|
import org.mozilla.fenix.components.metrics.Event
|
2019-11-20 01:30:56 +01:00
|
|
|
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)
|
2020-01-16 19:03:18 +01:00
|
|
|
(activity as HomeActivity).getSupportActionBarAndInflateIfNecessary().show()
|
2019-11-20 01:30:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-10 03:10:24 +01:00
|
|
|
@Suppress("ComplexMethod")
|
2019-11-20 01:30:56 +01:00
|
|
|
private fun createCustomEngine() {
|
|
|
|
custom_search_engine_name_field.error = ""
|
|
|
|
custom_search_engine_search_string_field.error = ""
|
|
|
|
|
2020-03-25 15:39:05 +01:00
|
|
|
val name = edit_engine_name.text?.toString()?.trim() ?: ""
|
2019-11-20 01:30:56 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-12-10 03:10:24 +01:00
|
|
|
custom_search_engine_search_string_field.error = when {
|
|
|
|
searchString.isEmpty() ->
|
|
|
|
resources.getString(R.string.search_add_custom_engine_error_empty_search_string)
|
2020-01-10 11:20:43 +01:00
|
|
|
!searchString.contains("%s") ->
|
2019-12-10 03:10:24 +01:00
|
|
|
resources.getString(R.string.search_add_custom_engine_error_missing_template)
|
|
|
|
else -> null
|
2019-11-20 01:30:56 +01:00
|
|
|
}
|
|
|
|
|
2019-12-10 03:10:24 +01:00
|
|
|
if (custom_search_engine_search_string_field.error != null) {
|
2019-11-20 01:30:56 +01:00
|
|
|
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
|
2019-12-20 09:27:05 +01:00
|
|
|
.getString(R.string.search_add_custom_engine_error_cannot_reach, name)
|
2019-11-20 01:30:56 +01:00
|
|
|
}
|
|
|
|
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 {
|
2020-04-02 21:30:13 +02:00
|
|
|
FenixSnackbar.make(view = it,
|
|
|
|
duration = FenixSnackbar.LENGTH_SHORT,
|
2020-04-13 21:16:01 +02:00
|
|
|
isDisplayedWithBrowserToolbar = false
|
2020-04-02 21:30:13 +02:00
|
|
|
)
|
2019-11-20 01:30:56 +01:00
|
|
|
.setText(successMessage)
|
|
|
|
.show()
|
|
|
|
}
|
|
|
|
|
2019-12-03 14:54:57 +01:00
|
|
|
context?.components?.analytics?.metrics?.track(Event.CustomEngineAdded)
|
2019-11-20 01:30:56 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|