diff --git a/app/src/main/java/org/mozilla/fenix/components/Search.kt b/app/src/main/java/org/mozilla/fenix/components/Search.kt index 8e85ba147..cf31a6ba0 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Search.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Search.kt @@ -19,6 +19,7 @@ class Search(private val context: Context) { */ val searchEngineManager by lazy { SearchEngineManager().apply { + registerForLocaleUpdates(context) GlobalScope.launch { load(context).await() } diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index c3a6131a1..9f279e78e 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -45,6 +45,7 @@ import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getManagedEmitter import org.mozilla.fenix.settings.SupportUtils +import org.mozilla.fenix.utils.Settings import kotlin.math.roundToInt fun SessionBundleStorage.archive(sessionManager: SessionManager) { @@ -110,7 +111,10 @@ class HomeFragment : Fragment() { getManagedEmitter().onNext(SessionsChange.Changed(archivedSessions)) }) - val searchIcon = requireComponents.search.searchEngineManager.getDefaultSearchEngine(requireContext()).let { + val searchIcon = requireComponents.search.searchEngineManager.getDefaultSearchEngine( + requireContext(), + Settings.getInstance(requireContext()).defaultSearchEngineName + ).let { BitmapDrawable(resources, it.icon) } diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarUIView.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarUIView.kt index 18e2bfd00..29dbb60f9 100644 --- a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarUIView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarUIView.kt @@ -1,4 +1,5 @@ package org.mozilla.fenix.search.awesomebar + /* 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/. */ @@ -25,7 +26,11 @@ class AwesomeBarUIView( actionEmitter: Observer, changesObservable: Observable ) : - UIView(container, actionEmitter, changesObservable) { + UIView( + container, + actionEmitter, + changesObservable + ) { override val view: BrowserAwesomeBar = LayoutInflater.from(container.context) .inflate(R.layout.component_awesomebar, container, true) .findViewById(R.id.awesomeBar) diff --git a/app/src/main/java/org/mozilla/fenix/settings/RadioSearchEngineListPreference.kt b/app/src/main/java/org/mozilla/fenix/settings/RadioSearchEngineListPreference.kt new file mode 100644 index 000000000..cc1ccd96c --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/RadioSearchEngineListPreference.kt @@ -0,0 +1,50 @@ +/* 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 + +import android.content.Context +import android.util.AttributeSet +import android.widget.CompoundButton +import android.widget.RadioGroup +import androidx.preference.PreferenceViewHolder +import org.mozilla.fenix.R +import org.mozilla.fenix.utils.Settings + +class RadioSearchEngineListPreference : SearchEngineListPreference, + RadioGroup.OnCheckedChangeListener { + + override val itemResId: Int + get() = R.layout.search_engine_radio_button + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + override fun onBindViewHolder(holder: PreferenceViewHolder?) { + super.onBindViewHolder(holder) + searchEngineGroup!!.setOnCheckedChangeListener(this) + } + + override fun updateDefaultItem(defaultButton: CompoundButton) { + defaultButton.isChecked = true + } + + override fun onCheckedChanged(group: RadioGroup, checkedId: Int) { + /* onCheckedChanged is called intermittently before the search engine table is full, so we + must check these conditions to prevent crashes and inconsistent states. */ + if (group.childCount != searchEngines.count() || group.getChildAt(checkedId) == null || + !group.getChildAt(checkedId).isPressed + ) { + return + } + + val newDefaultEngine = searchEngines[checkedId] + Settings.getInstance(group.context).setDefaultSearchEngineByName(newDefaultEngine.name) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/SearchEngineFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/SearchEngineFragment.kt new file mode 100644 index 000000000..58c694270 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/SearchEngineFragment.kt @@ -0,0 +1,23 @@ +/* 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 + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.PreferenceFragmentCompat +import org.mozilla.fenix.R + +class SearchEngineFragment : PreferenceFragmentCompat() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (activity as AppCompatActivity).title = getString(R.string.preferences_search_engine) + (activity as AppCompatActivity).supportActionBar?.show() + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.search_engine_preferences, rootKey) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/SearchEngineListPreference.kt b/app/src/main/java/org/mozilla/fenix/settings/SearchEngineListPreference.kt new file mode 100644 index 000000000..937d5f557 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/SearchEngineListPreference.kt @@ -0,0 +1,121 @@ +/* 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 + +import android.content.Context +import android.content.res.Resources +import android.graphics.drawable.BitmapDrawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.CompoundButton +import android.widget.RadioGroup +import androidx.core.content.ContextCompat +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import mozilla.components.browser.search.SearchEngine +import org.mozilla.fenix.DefaultThemeManager +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.utils.Settings +import kotlin.coroutines.CoroutineContext + +abstract class SearchEngineListPreference : Preference, CoroutineScope { + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + protected var searchEngines: List = emptyList() + protected var searchEngineGroup: RadioGroup? = null + + protected abstract val itemResId: Int + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + layoutResource = R.layout.preference_search_engine_chooser + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + layoutResource = R.layout.preference_search_engine_chooser + } + + 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 } + + refreshSearchEngineViews(context) + } + + override fun onDetached() { + job.cancel() + super.onDetached() + } + + protected abstract fun updateDefaultItem(defaultButton: CompoundButton) + + private fun refreshSearchEngineViews(context: Context) { + if (searchEngineGroup == null) { + // We want to refresh the search engine list of this preference in onResume, + // but the first time this preference is created onResume is called before onCreateView + // so searchEngineGroup is not set yet. + return + } + + val defaultSearchEngine = + context.components.search.searchEngineManager.getDefaultSearchEngine( + context, + Settings.getInstance(context).defaultSearchEngineName + ).identifier + + searchEngineGroup!!.removeAllViews() + + val layoutInflater = LayoutInflater.from(context) + val layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + + for (i in searchEngines.indices) { + val engine = searchEngines[i] + val engineId = engine.identifier + val engineItem = makeButtonFromSearchEngine(engine, layoutInflater, context.resources) + engineItem.id = i + engineItem.tag = engineId + if (engineId == defaultSearchEngine) { + updateDefaultItem(engineItem) + } + searchEngineGroup!!.addView(engineItem, layoutParams) + } + } + + private fun makeButtonFromSearchEngine( + engine: SearchEngine, + layoutInflater: LayoutInflater, + res: Resources + ): CompoundButton { + val buttonItem = layoutInflater.inflate(itemResId, null) as CompoundButton + buttonItem.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) + val attr = + DefaultThemeManager.resolveAttribute(android.R.attr.listChoiceIndicatorSingle, context) + val buttonDrawable = ContextCompat.getDrawable(context, attr) + buttonDrawable.apply { + this?.setBounds(0, 0, this.intrinsicWidth, this.intrinsicHeight) + } + buttonItem.setCompoundDrawables(engineIcon, null, buttonDrawable, null) + return buttonItem + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt index d4e37187b..63ede1c8a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -48,6 +48,7 @@ import org.mozilla.fenix.R.string.pref_key_about import org.mozilla.fenix.R.string.pref_key_sign_in import org.mozilla.fenix.R.string.pref_key_account import org.mozilla.fenix.R.string.pref_key_account_category +import org.mozilla.fenix.R.string.pref_key_search_engine_settings @SuppressWarnings("TooManyFunctions") class SettingsFragment : PreferenceFragmentCompat(), CoroutineScope, AccountObserver { @@ -76,6 +77,9 @@ class SettingsFragment : PreferenceFragmentCompat(), CoroutineScope, AccountObse @Suppress("ComplexMethod") override fun onPreferenceTreeClick(preference: Preference): Boolean { when (preference.key) { + resources.getString(pref_key_search_engine_settings) -> { + navigateToSearchEngineSettings() + } resources.getString(pref_key_site_permissions) -> { navigateToSitePermissions() } @@ -206,6 +210,11 @@ class SettingsFragment : PreferenceFragmentCompat(), CoroutineScope, AccountObse } } + private fun navigateToSearchEngineSettings() { + val directions = SettingsFragmentDirections.actionSettingsFragmentToSearchEngineFragment() + Navigation.findNavController(view!!).navigate(directions) + } + private fun navigateToSitePermissions() { val directions = SettingsFragmentDirections.actionSettingsFragmentToSitePermissionsFragment() Navigation.findNavController(view!!).navigate(directions) diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt new file mode 100644 index 000000000..a155d37b7 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -0,0 +1,52 @@ +package org.mozilla.fenix.utils + +/* 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/. */ + +import android.content.Context +import android.content.SharedPreferences +import android.preference.PreferenceManager +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.getPreferenceKey + +/** + * A simple wrapper for SharedPreferences that makes reading preference a little bit easier. + */ +class Settings private constructor(context: Context) { + + companion object { + var instance: Settings? = null + + @JvmStatic + @Synchronized + fun getInstance(context: Context): Settings { + if (instance == null) { + instance = Settings(context.applicationContext) + } + return instance ?: throw AssertionError("Instance cleared") + } + } + + private val appContext = context.applicationContext + + private val preferences: SharedPreferences = + PreferenceManager.getDefaultSharedPreferences(context) + + val defaultSearchEngineName: String + get() = preferences.getString( + appContext.getPreferenceKey(R.string.pref_key_search_engine), + "" + ) ?: "" + + fun setDefaultSearchEngineByName(name: String) { + preferences.edit() + .putString(appContext.getPreferenceKey(R.string.pref_key_search_engine), name) + .apply() + } + + fun showSearchSuggestions(): Boolean = preferences.getBoolean( + appContext.getPreferenceKey(R.string.pref_key_show_search_suggestions), + true + ) +} diff --git a/app/src/main/res/layout/preference_search_engine_chooser.xml b/app/src/main/res/layout/preference_search_engine_chooser.xml new file mode 100644 index 000000000..650f700f4 --- /dev/null +++ b/app/src/main/res/layout/preference_search_engine_chooser.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/search_engine_radio_button.xml b/app/src/main/res/layout/search_engine_radio_button.xml new file mode 100644 index 000000000..fb7564808 --- /dev/null +++ b/app/src/main/res/layout/search_engine_radio_button.xml @@ -0,0 +1,20 @@ + + + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 92a582a30..8d62dc57f 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -98,6 +98,9 @@ app:destination="@id/accessibilityFragment"/> + @@ -108,4 +111,8 @@ android:label="AccessibilityFragment"/> + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 93a25a74e..99bc7ef48 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -15,4 +15,9 @@ 16dp 2dp 8dp + 24dp + 48dp + 32dp + 16dp + 12dp \ No newline at end of file diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 6620a3ff2..733c12d2b 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -3,6 +3,7 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> pref_key_make_default_browser + pref_key_search_engine_settings pref_key_search_engine pref_key_passwords pref_key_credit_cards_addresses @@ -32,4 +33,7 @@ pref_key_sync_history pref_key_sign_out + + pref_key_show_search_suggestions + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5b5d130e0..227ed9624 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -102,6 +102,9 @@ Leak Canary + + Show search suggestions + Account Settings diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 26fea4f0a..9078011b1 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -29,7 +29,7 @@ app:iconSpaceReserved="false"> + + + + + +