mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-09 19:10:42 +00:00
Feature/#220 language menu (#7070)
* For #220 - Added advanced header + locale settings item in the settings fragment * For #220 - Added locale selection page with lib state + handling of locale changes * For #220 - Removed registering for locale changes in the manifest, allow system to restart activity in that scenario * For #220 - Added unit tests for locale settings page * For #220: fixed an outdated unit test ga-a Co-authored-by: Severin Rudie <Baron-Severin@users.noreply.github.com>
This commit is contained in:
parent
9cbc3f7a4a
commit
ea2411a88b
@ -429,6 +429,7 @@ dependencies {
|
|||||||
implementation Deps.mozilla_support_ktx
|
implementation Deps.mozilla_support_ktx
|
||||||
implementation Deps.mozilla_support_rustlog
|
implementation Deps.mozilla_support_rustlog
|
||||||
implementation Deps.mozilla_support_utils
|
implementation Deps.mozilla_support_utils
|
||||||
|
implementation Deps.mozilla_support_locale
|
||||||
|
|
||||||
// We only care about support-migration in builds that will be overwriting Fennec.
|
// We only care about support-migration in builds that will be overwriting Fennec.
|
||||||
fennecProductionImplementation Deps.mozilla_support_migration
|
fennecProductionImplementation Deps.mozilla_support_migration
|
||||||
@ -602,6 +603,23 @@ task printVariants {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
task buildTranslationArray {
|
||||||
|
def foundLocales = new StringBuilder()
|
||||||
|
foundLocales.append("new String[]{")
|
||||||
|
|
||||||
|
fileTree("src/main/res").visit { FileVisitDetails details ->
|
||||||
|
if(details.file.path.endsWith("/strings.xml")){
|
||||||
|
def languageCode = details.file.parent.tokenize('/').last().replaceAll('values-','').replaceAll('-r','-')
|
||||||
|
languageCode = (languageCode == "values") ? "en-US" : languageCode
|
||||||
|
foundLocales.append("\"").append(languageCode).append("\"").append(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foundLocales.append("}")
|
||||||
|
def foundLocalesString = foundLocales.toString().replaceAll(',}','}')
|
||||||
|
android.defaultConfig.buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", foundLocalesString
|
||||||
|
}
|
||||||
|
|
||||||
def glean_android_components_tag = (
|
def glean_android_components_tag = (
|
||||||
Versions.mozilla_android_components.endsWith('-SNAPSHOT') ?
|
Versions.mozilla_android_components.endsWith('-SNAPSHOT') ?
|
||||||
'master' :
|
'master' :
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".HomeActivity"
|
android:name=".HomeActivity"
|
||||||
android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|locale|layoutDirection|smallestScreenSize|screenLayout"
|
android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|layoutDirection|smallestScreenSize|screenLayout"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@ -81,7 +81,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".customtabs.ExternalAppBrowserActivity"
|
android:name=".customtabs.ExternalAppBrowserActivity"
|
||||||
android:autoRemoveFromRecents="false"
|
android:autoRemoveFromRecents="false"
|
||||||
android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|locale|layoutDirection|smallestScreenSize|screenLayout"
|
android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|layoutDirection|smallestScreenSize|screenLayout"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:persistableMode="persistNever"
|
android:persistableMode="persistNever"
|
||||||
@ -151,7 +151,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".settings.account.AuthCustomTabActivity"
|
android:name=".settings.account.AuthCustomTabActivity"
|
||||||
android:autoRemoveFromRecents="false"
|
android:autoRemoveFromRecents="false"
|
||||||
android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|locale|layoutDirection|smallestScreenSize|screenLayout"
|
android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|layoutDirection|smallestScreenSize|screenLayout"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
|
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
|
||||||
|
@ -5,19 +5,18 @@
|
|||||||
package org.mozilla.fenix
|
package org.mozilla.fenix
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import mozilla.appservices.Megazord
|
import mozilla.appservices.Megazord
|
||||||
import mozilla.components.concept.push.PushProcessor
|
import mozilla.components.concept.push.PushProcessor
|
||||||
import mozilla.components.service.experiments.Experiments
|
import mozilla.components.service.experiments.Experiments
|
||||||
@ -29,6 +28,7 @@ import mozilla.components.support.base.log.logger.Logger
|
|||||||
import mozilla.components.support.base.log.sink.AndroidLogSink
|
import mozilla.components.support.base.log.sink.AndroidLogSink
|
||||||
import mozilla.components.support.ktx.android.content.isMainProcess
|
import mozilla.components.support.ktx.android.content.isMainProcess
|
||||||
import mozilla.components.support.ktx.android.content.runOnlyInMainProcess
|
import mozilla.components.support.ktx.android.content.runOnlyInMainProcess
|
||||||
|
import mozilla.components.support.locale.LocaleAwareApplication
|
||||||
import mozilla.components.support.rusthttp.RustHttpConfig
|
import mozilla.components.support.rusthttp.RustHttpConfig
|
||||||
import mozilla.components.support.rustlog.RustLog
|
import mozilla.components.support.rustlog.RustLog
|
||||||
import org.mozilla.fenix.GleanMetrics.ExperimentsMetrics
|
import org.mozilla.fenix.GleanMetrics.ExperimentsMetrics
|
||||||
@ -40,7 +40,7 @@ import java.io.File
|
|||||||
|
|
||||||
@SuppressLint("Registered")
|
@SuppressLint("Registered")
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
open class FenixApplication : Application() {
|
open class FenixApplication : LocaleAwareApplication() {
|
||||||
lateinit var fretboard: Fretboard
|
lateinit var fretboard: Fretboard
|
||||||
lateinit var experimentLoader: Deferred<Boolean>
|
lateinit var experimentLoader: Deferred<Boolean>
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ import androidx.annotation.CallSuper
|
|||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.annotation.VisibleForTesting.PROTECTED
|
import androidx.annotation.VisibleForTesting.PROTECTED
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavDestination
|
import androidx.navigation.NavDestination
|
||||||
@ -30,6 +29,7 @@ import mozilla.components.service.fxa.sync.SyncReason
|
|||||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||||
import mozilla.components.support.ktx.kotlin.isUrl
|
import mozilla.components.support.ktx.kotlin.isUrl
|
||||||
import mozilla.components.support.ktx.kotlin.toNormalizedUrl
|
import mozilla.components.support.ktx.kotlin.toNormalizedUrl
|
||||||
|
import mozilla.components.support.locale.LocaleAwareAppCompatActivity
|
||||||
import mozilla.components.support.utils.SafeIntent
|
import mozilla.components.support.utils.SafeIntent
|
||||||
import mozilla.components.support.utils.toSafeIntent
|
import mozilla.components.support.utils.toSafeIntent
|
||||||
import org.mozilla.fenix.browser.UriOpenedObserver
|
import org.mozilla.fenix.browser.UriOpenedObserver
|
||||||
@ -62,7 +62,7 @@ import org.mozilla.fenix.theme.DefaultThemeManager
|
|||||||
import org.mozilla.fenix.theme.ThemeManager
|
import org.mozilla.fenix.theme.ThemeManager
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
open class HomeActivity : AppCompatActivity() {
|
open class HomeActivity : LocaleAwareAppCompatActivity() {
|
||||||
|
|
||||||
lateinit var themeManager: ThemeManager
|
lateinit var themeManager: ThemeManager
|
||||||
lateinit var browsingModeManager: BrowsingModeManager
|
lateinit var browsingModeManager: BrowsingModeManager
|
||||||
@ -239,8 +239,9 @@ open class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun openToBrowser(from: BrowserDirection, customTabSessionId: String? = null) {
|
fun openToBrowser(from: BrowserDirection, customTabSessionId: String? = null) {
|
||||||
if (sessionObserver == null)
|
if (sessionObserver == null) {
|
||||||
sessionObserver = UriOpenedObserver(this)
|
sessionObserver = UriOpenedObserver(this)
|
||||||
|
}
|
||||||
|
|
||||||
if (navHost.navController.alreadyOnDestination(R.id.browserFragment)) return
|
if (navHost.navController.alreadyOnDestination(R.id.browserFragment)) return
|
||||||
@IdRes val fragmentId = if (from.fragmentId != 0) from.fragmentId else null
|
@IdRes val fragmentId = if (from.fragmentId != 0) from.fragmentId else null
|
||||||
|
@ -11,9 +11,9 @@ import mozilla.components.lib.state.State
|
|||||||
import mozilla.components.lib.state.Store
|
import mozilla.components.lib.state.Store
|
||||||
|
|
||||||
class BookmarkFragmentStore(
|
class BookmarkFragmentStore(
|
||||||
initalState: BookmarkFragmentState
|
initialState: BookmarkFragmentState
|
||||||
) : Store<BookmarkFragmentState, BookmarkFragmentAction>(
|
) : Store<BookmarkFragmentState, BookmarkFragmentAction>(
|
||||||
initalState, ::bookmarkFragmentStateReducer
|
initialState, ::bookmarkFragmentStateReducer
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,7 +62,6 @@ import org.mozilla.fenix.ext.settings
|
|||||||
import org.mozilla.fenix.ext.showToolbar
|
import org.mozilla.fenix.ext.showToolbar
|
||||||
import org.mozilla.fenix.settings.account.AccountAuthErrorPreference
|
import org.mozilla.fenix.settings.account.AccountAuthErrorPreference
|
||||||
import org.mozilla.fenix.settings.account.AccountPreference
|
import org.mozilla.fenix.settings.account.AccountPreference
|
||||||
import org.mozilla.fenix.utils.ItsNotBrokenSnack
|
|
||||||
|
|
||||||
@Suppress("LargeClass")
|
@Suppress("LargeClass")
|
||||||
class SettingsFragment : PreferenceFragmentCompat() {
|
class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
@ -210,9 +209,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||||||
SettingsFragmentDirections.actionSettingsFragmentToAccessibilityFragment()
|
SettingsFragmentDirections.actionSettingsFragmentToAccessibilityFragment()
|
||||||
}
|
}
|
||||||
resources.getString(pref_key_language) -> {
|
resources.getString(pref_key_language) -> {
|
||||||
// TODO #220
|
SettingsFragmentDirections.actionSettingsFragmentToLocaleSettingsFragment()
|
||||||
ItsNotBrokenSnack(requireContext()).showSnackbar(issueNumber = "220")
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
resources.getString(pref_key_make_default_browser) -> {
|
resources.getString(pref_key_make_default_browser) -> {
|
||||||
SettingsFragmentDirections.actionSettingsFragmentToDefaultBrowserSettingsFragment()
|
SettingsFragmentDirections.actionSettingsFragmentToDefaultBrowserSettingsFragment()
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
/* 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.advanced
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import mozilla.components.support.locale.LocaleManager
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
interface LocaleSettingsController {
|
||||||
|
fun handleLocaleSelected(locale: Locale)
|
||||||
|
fun handleSearchQueryTyped(query: String)
|
||||||
|
fun handleDefaultLocaleSelected()
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultLocaleSettingsController(
|
||||||
|
private val context: Context,
|
||||||
|
private val localeSettingsStore: LocaleSettingsStore
|
||||||
|
) : LocaleSettingsController {
|
||||||
|
|
||||||
|
override fun handleLocaleSelected(locale: Locale) {
|
||||||
|
if (localeSettingsStore.state.selectedLocale == locale) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
localeSettingsStore.dispatch(LocaleSettingsAction.Select(locale))
|
||||||
|
LocaleManager.setNewLocale(context, locale.toLanguageTag())
|
||||||
|
(context as Activity).recreate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleDefaultLocaleSelected() {
|
||||||
|
localeSettingsStore.dispatch(LocaleSettingsAction.Select(localeSettingsStore.state.localeList[0]))
|
||||||
|
LocaleManager.resetToSystemDefault(context)
|
||||||
|
(context as Activity).recreate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleSearchQueryTyped(query: String) {
|
||||||
|
localeSettingsStore.dispatch(LocaleSettingsAction.Search(query))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,150 @@
|
|||||||
|
/* 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.advanced
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.android.synthetic.main.locale_settings_item.view.locale_selected_icon
|
||||||
|
import kotlinx.android.synthetic.main.locale_settings_item.view.locale_subtitle_text
|
||||||
|
import kotlinx.android.synthetic.main.locale_settings_item.view.locale_title_text
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class LocaleAdapter(private val interactor: LocaleSettingsViewInteractor) :
|
||||||
|
RecyclerView.Adapter<BaseLocaleViewHolder>() {
|
||||||
|
|
||||||
|
private var localeList: List<Locale> = listOf()
|
||||||
|
private var selectedLocale: Locale = Locale.getDefault()
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseLocaleViewHolder {
|
||||||
|
val view =
|
||||||
|
LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.locale_settings_item, parent, false)
|
||||||
|
|
||||||
|
return when (viewType) {
|
||||||
|
ItemType.DEFAULT.ordinal -> SystemLocaleViewHolder(
|
||||||
|
view,
|
||||||
|
interactor,
|
||||||
|
selectedLocale
|
||||||
|
)
|
||||||
|
ItemType.LOCALE.ordinal -> LocaleViewHolder(
|
||||||
|
view,
|
||||||
|
interactor,
|
||||||
|
selectedLocale
|
||||||
|
)
|
||||||
|
else -> throw IllegalStateException("ViewType $viewType does not match to a ViewHolder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return localeList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: BaseLocaleViewHolder, position: Int) {
|
||||||
|
holder.bind(localeList[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return when (position) {
|
||||||
|
0 -> ItemType.DEFAULT
|
||||||
|
else -> ItemType.LOCALE
|
||||||
|
}.ordinal
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateData(localeList: List<Locale>, selectedLocale: Locale) {
|
||||||
|
val diffUtil = DiffUtil.calculateDiff(
|
||||||
|
LocaleDiffUtil(
|
||||||
|
this.localeList,
|
||||||
|
localeList,
|
||||||
|
this.selectedLocale,
|
||||||
|
selectedLocale
|
||||||
|
)
|
||||||
|
)
|
||||||
|
this.localeList = localeList
|
||||||
|
this.selectedLocale = selectedLocale
|
||||||
|
|
||||||
|
diffUtil.dispatchUpdatesTo(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class LocaleDiffUtil(
|
||||||
|
private val old: List<Locale>,
|
||||||
|
private val new: List<Locale>,
|
||||||
|
private val oldSelectedLocale: Locale,
|
||||||
|
private val newSelectedLocale: Locale
|
||||||
|
) : DiffUtil.Callback() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
val selectionChanged =
|
||||||
|
old[oldItemPosition] == oldSelectedLocale && oldSelectedLocale != newSelectedLocale
|
||||||
|
return old[oldItemPosition] == new[newItemPosition] && !selectionChanged
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||||
|
old[oldItemPosition].toLanguageTag() == new[newItemPosition].toLanguageTag()
|
||||||
|
|
||||||
|
override fun getOldListSize(): Int = old.size
|
||||||
|
override fun getNewListSize(): Int = new.size
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ItemType {
|
||||||
|
DEFAULT, LOCALE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocaleViewHolder(
|
||||||
|
view: View,
|
||||||
|
private val interactor: LocaleSettingsViewInteractor,
|
||||||
|
private val selectedLocale: Locale
|
||||||
|
) : BaseLocaleViewHolder(view) {
|
||||||
|
private val icon = view.locale_selected_icon
|
||||||
|
private val title = view.locale_title_text
|
||||||
|
private val subtitle = view.locale_subtitle_text
|
||||||
|
|
||||||
|
override fun bind(locale: Locale) {
|
||||||
|
// capitalisation is done using the rules of the appropriate locale (endonym and exonym)
|
||||||
|
title.text = locale.getDisplayName(locale).capitalize(locale)
|
||||||
|
subtitle.text = locale.displayName.capitalize(Locale.getDefault())
|
||||||
|
icon.isVisible = locale === selectedLocale
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
interactor.onLocaleSelected(locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SystemLocaleViewHolder(
|
||||||
|
view: View,
|
||||||
|
private val interactor: LocaleSettingsViewInteractor,
|
||||||
|
private val selectedLocale: Locale
|
||||||
|
) : BaseLocaleViewHolder(view) {
|
||||||
|
private val icon = view.locale_selected_icon
|
||||||
|
private val title = view.locale_title_text
|
||||||
|
private val subtitle = view.locale_subtitle_text
|
||||||
|
|
||||||
|
override fun bind(locale: Locale) {
|
||||||
|
title.text = itemView.context.getString(R.string.default_locale_text)
|
||||||
|
subtitle.visibility = View.GONE
|
||||||
|
icon.isVisible = locale === selectedLocale
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
interactor.onDefaultLocaleSelected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BaseLocaleViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
abstract fun bind(locale: Locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to Kotlin's capitalize with locale parameter, but that method is currently experimental
|
||||||
|
*/
|
||||||
|
private fun String.capitalize(locale: Locale): String {
|
||||||
|
return substring(0, 1).toUpperCase(locale) + substring(1)
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
/* 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.advanced
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import mozilla.components.support.locale.LocaleManager
|
||||||
|
import mozilla.components.support.locale.toLocale
|
||||||
|
import org.mozilla.fenix.BuildConfig
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of currently supported locales, with the system default set as the first one
|
||||||
|
*/
|
||||||
|
fun LocaleManager.getSupportedLocales(): List<Locale> {
|
||||||
|
val resultLocaleList: MutableList<Locale> = ArrayList()
|
||||||
|
resultLocaleList.add(0, getSystemDefault() ?: Locale.getDefault())
|
||||||
|
|
||||||
|
resultLocaleList.addAll(BuildConfig.SUPPORTED_LOCALE_ARRAY
|
||||||
|
.toList()
|
||||||
|
.map {
|
||||||
|
it.toLocale()
|
||||||
|
}.sortedWith(compareBy(
|
||||||
|
{ it.displayLanguage },
|
||||||
|
{ it.displayCountry }
|
||||||
|
)))
|
||||||
|
return resultLocaleList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the locale that corresponds to the language stored locally by us. If no suitable one is found,
|
||||||
|
* return default.
|
||||||
|
*/
|
||||||
|
fun LocaleManager.getSelectedLocale(
|
||||||
|
context: Context,
|
||||||
|
localeList: List<Locale> = getSupportedLocales()
|
||||||
|
): Locale {
|
||||||
|
val selectedLocale = getCurrentLocale(context)?.toLanguageTag()
|
||||||
|
val defaultLocale = getSystemDefault() ?: Locale.getDefault()
|
||||||
|
|
||||||
|
return if (selectedLocale == null) {
|
||||||
|
defaultLocale
|
||||||
|
} else {
|
||||||
|
val supportedMatch = localeList
|
||||||
|
.firstOrNull { it.toLanguageTag() == selectedLocale }
|
||||||
|
supportedMatch ?: defaultLocale
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
/* 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.advanced
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import kotlinx.android.synthetic.main.fragment_locale_settings.view.locale_container
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import mozilla.components.lib.state.ext.consumeFrom
|
||||||
|
import mozilla.components.support.ktx.android.view.hideKeyboard
|
||||||
|
import mozilla.components.support.locale.LocaleManager
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
|
import org.mozilla.fenix.ext.showToolbar
|
||||||
|
|
||||||
|
class LocaleSettingsFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var store: LocaleSettingsStore
|
||||||
|
private lateinit var interactor: LocaleSettingsInteractor
|
||||||
|
private lateinit var localeView: LocaleSettingsView
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_locale_settings, container, false)
|
||||||
|
|
||||||
|
store = getStore()
|
||||||
|
interactor = LocaleSettingsInteractor(
|
||||||
|
controller = DefaultLocaleSettingsController(
|
||||||
|
context = requireContext(),
|
||||||
|
localeSettingsStore = store
|
||||||
|
)
|
||||||
|
)
|
||||||
|
localeView = LocaleSettingsView(view.locale_container, interactor)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
localeView.onResume()
|
||||||
|
showToolbar(getString(R.string.preferences_language))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
view?.hideKeyboard()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
consumeFrom(store) {
|
||||||
|
localeView.update(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStore(): LocaleSettingsStore {
|
||||||
|
val supportedLocales = LocaleManager.getSupportedLocales()
|
||||||
|
val selectedLocale = LocaleManager.getSelectedLocale(requireContext())
|
||||||
|
|
||||||
|
return StoreProvider.get(this) {
|
||||||
|
LocaleSettingsStore(
|
||||||
|
LocaleSettingsState(
|
||||||
|
supportedLocales,
|
||||||
|
supportedLocales,
|
||||||
|
selectedLocale
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.advanced
|
||||||
|
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class LocaleSettingsInteractor(private val controller: LocaleSettingsController) :
|
||||||
|
LocaleSettingsViewInteractor {
|
||||||
|
|
||||||
|
override fun onLocaleSelected(locale: Locale) {
|
||||||
|
controller.handleLocaleSelected(locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDefaultLocaleSelected() {
|
||||||
|
controller.handleDefaultLocaleSelected()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSearchQueryTyped(query: String) {
|
||||||
|
controller.handleSearchQueryTyped(query)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/* 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.advanced
|
||||||
|
|
||||||
|
import mozilla.components.lib.state.Action
|
||||||
|
import mozilla.components.lib.state.State
|
||||||
|
import mozilla.components.lib.state.Store
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class LocaleSettingsStore(
|
||||||
|
initialState: LocaleSettingsState
|
||||||
|
) : Store<LocaleSettingsState, LocaleSettingsAction>(
|
||||||
|
initialState, ::localeSettingsStateReducer
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state of the language selection page
|
||||||
|
* @property localeList The full list of locales available
|
||||||
|
* @property searchedLocaleList The list of locales starting with a search query
|
||||||
|
* @property selectedLocale The current selected locale
|
||||||
|
*/
|
||||||
|
data class LocaleSettingsState(
|
||||||
|
val localeList: List<Locale>,
|
||||||
|
val searchedLocaleList: List<Locale>,
|
||||||
|
val selectedLocale: Locale
|
||||||
|
) : State
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions to dispatch through the `LocaleSettingsStore` to modify `LocaleSettingsState` through the reducer.
|
||||||
|
*/
|
||||||
|
sealed class LocaleSettingsAction : Action {
|
||||||
|
data class Select(val selectedItem: Locale) : LocaleSettingsAction()
|
||||||
|
data class Search(val query: String) : LocaleSettingsAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduces the locale state from the current state and an action performed on it.
|
||||||
|
* @param state the current locale state
|
||||||
|
* @param action the action to perform
|
||||||
|
* @return the new locale state
|
||||||
|
*/
|
||||||
|
private fun localeSettingsStateReducer(
|
||||||
|
state: LocaleSettingsState,
|
||||||
|
action: LocaleSettingsAction
|
||||||
|
): LocaleSettingsState {
|
||||||
|
return when (action) {
|
||||||
|
is LocaleSettingsAction.Select -> {
|
||||||
|
state.copy(selectedLocale = action.selectedItem)
|
||||||
|
}
|
||||||
|
is LocaleSettingsAction.Search -> {
|
||||||
|
val searchedItems = state.localeList.filter {
|
||||||
|
it.getDisplayLanguage(it).startsWith(action.query, ignoreCase = true) ||
|
||||||
|
it.displayLanguage.startsWith(action.query, ignoreCase = true) ||
|
||||||
|
it === state.localeList[0]
|
||||||
|
}
|
||||||
|
state.copy(searchedLocaleList = searchedItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
/* 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.advanced
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.SearchView
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import kotlinx.android.synthetic.main.component_locale_settings.view.locale_list
|
||||||
|
import kotlinx.android.synthetic.main.component_locale_settings.view.toolbar_container
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
interface LocaleSettingsViewInteractor {
|
||||||
|
|
||||||
|
fun onLocaleSelected(locale: Locale)
|
||||||
|
|
||||||
|
fun onDefaultLocaleSelected()
|
||||||
|
|
||||||
|
fun onSearchQueryTyped(query: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocaleSettingsView(
|
||||||
|
container: ViewGroup,
|
||||||
|
val interactor: LocaleSettingsViewInteractor
|
||||||
|
) {
|
||||||
|
|
||||||
|
val view: View = LayoutInflater.from(container.context)
|
||||||
|
.inflate(R.layout.component_locale_settings, container, true)
|
||||||
|
|
||||||
|
private val localeAdapter: LocaleAdapter
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.locale_list.apply {
|
||||||
|
localeAdapter = LocaleAdapter(interactor)
|
||||||
|
adapter = localeAdapter
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
}
|
||||||
|
val searchView: SearchView = view.toolbar_container
|
||||||
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String): Boolean {
|
||||||
|
interactor.onSearchQueryTyped(newText)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(state: LocaleSettingsState) {
|
||||||
|
localeAdapter.updateData(state.searchedLocaleList, state.selectedLocale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onResume() {
|
||||||
|
view.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.speech.RecognizerIntent
|
import android.speech.RecognizerIntent
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import mozilla.components.support.locale.LocaleManager
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.IntentReceiverActivity
|
import org.mozilla.fenix.IntentReceiverActivity
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
@ -57,6 +58,7 @@ class VoiceSearchActivity : AppCompatActivity() {
|
|||||||
private fun displaySpeechRecognizer() {
|
private fun displaySpeechRecognizer() {
|
||||||
val intentSpeech = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
|
val intentSpeech = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
|
||||||
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
|
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
|
||||||
|
putExtra(RecognizerIntent.EXTRA_LANGUAGE, LocaleManager.getCurrentLocale(this@VoiceSearchActivity))
|
||||||
}
|
}
|
||||||
metrics.track(Event.SearchWidgetVoiceSearchPressed)
|
metrics.track(Event.SearchWidgetVoiceSearchPressed)
|
||||||
|
|
||||||
|
4
app/src/main/res/drawable/locale_search_background.xml
Normal file
4
app/src/main/res/drawable/locale_search_background.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="?foundation" />
|
||||||
|
</shape>
|
36
app/src/main/res/layout/component_locale_settings.xml
Normal file
36
app/src/main/res/layout/component_locale_settings.xml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true">
|
||||||
|
|
||||||
|
<SearchView
|
||||||
|
android:id="@+id/toolbar_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/locale_search_bar_margin"
|
||||||
|
android:background="@drawable/search_url_background"
|
||||||
|
android:closeIcon="@drawable/ic_close"
|
||||||
|
android:iconifiedByDefault="false"
|
||||||
|
android:paddingStart="@dimen/locale_search_bar_padding_start"
|
||||||
|
android:paddingEnd="0dp"
|
||||||
|
android:queryBackground="@android:color/transparent"
|
||||||
|
android:queryHint="@string/locale_search_hint"
|
||||||
|
android:searchIcon="@drawable/ic_search"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/locale_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_margin="@dimen/locale_list_margin"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/toolbar_container" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
16
app/src/main/res/layout/fragment_locale_settings.xml
Normal file
16
app/src/main/res/layout/fragment_locale_settings.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/locale_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
52
app/src/main/res/layout/locale_settings_item.xml
Normal file
52
app/src/main/res/layout/locale_settings_item.xml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/locale_selected_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/locale_item_vertical_margin"
|
||||||
|
android:layout_marginBottom="@dimen/locale_item_vertical_margin"
|
||||||
|
android:contentDescription="@string/a11y_selected_locale_content_description"
|
||||||
|
android:src="@drawable/mozac_ic_check"
|
||||||
|
android:tint="?primaryText"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/locale_title_text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/locale_item_text_margin_start"
|
||||||
|
android:layout_marginTop="@dimen/locale_item_vertical_margin"
|
||||||
|
android:textColor="?primaryText"
|
||||||
|
app:layout_goneMarginStart="@dimen/locale_item_text_margin_gone_start"
|
||||||
|
android:textSize="@dimen/locale_item_title_size"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/locale_subtitle_text"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/locale_selected_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
app:layout_goneMarginBottom="@dimen/locale_item_vertical_margin" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/locale_subtitle_text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/locale_item_text_margin_start"
|
||||||
|
android:layout_marginBottom="@dimen/locale_item_vertical_margin"
|
||||||
|
android:textColor="?secondaryText"
|
||||||
|
android:textSize="@dimen/locale_item_subtitle_size"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_goneMarginStart="@dimen/locale_item_text_margin_gone_start"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/locale_selected_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/locale_title_text"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -397,6 +397,9 @@
|
|||||||
<action
|
<action
|
||||||
android:id="@+id/action_settingsFragment_to_toolbarSettingsFragment"
|
android:id="@+id/action_settingsFragment_to_toolbarSettingsFragment"
|
||||||
app:destination="@id/toolbarSettingsFragment" />
|
app:destination="@id/toolbarSettingsFragment" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_settingsFragment_to_localeSettingsFragment"
|
||||||
|
app:destination="@id/localeSettingsFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/dataChoicesFragment"
|
android:id="@+id/dataChoicesFragment"
|
||||||
@ -687,4 +690,8 @@
|
|||||||
android:id="@+id/toolbarSettingsFragment"
|
android:id="@+id/toolbarSettingsFragment"
|
||||||
android:name="org.mozilla.fenix.settings.ToolbarSettingsFragment"
|
android:name="org.mozilla.fenix.settings.ToolbarSettingsFragment"
|
||||||
android:label="ToolbarSettingsFragment" />
|
android:label="ToolbarSettingsFragment" />
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/localeSettingsFragment"
|
||||||
|
android:name="org.mozilla.fenix.settings.advanced.LocaleSettingsFragment"
|
||||||
|
android:label="LanguageSettingsFragment" />
|
||||||
</navigation>
|
</navigation>
|
||||||
|
@ -93,4 +93,15 @@
|
|||||||
<dimen name="about_items_text_size">16sp</dimen>
|
<dimen name="about_items_text_size">16sp</dimen>
|
||||||
<dimen name="about_header_text_line_spacing_extra">4dp</dimen>
|
<dimen name="about_header_text_line_spacing_extra">4dp</dimen>
|
||||||
|
|
||||||
|
<!-- Locale Settings Fragment -->
|
||||||
|
<dimen name="locale_search_bar_margin">8dp</dimen>
|
||||||
|
<dimen name="locale_search_bar_padding_start">-12dp</dimen>
|
||||||
|
<dimen name="locale_list_margin">16dp</dimen>
|
||||||
|
<dimen name="locale_item_vertical_margin">8dp</dimen>
|
||||||
|
<dimen name="locale_item_text_margin_start">16dp</dimen>
|
||||||
|
<dimen name="locale_item_text_margin_gone_start">40dp</dimen>
|
||||||
|
<dimen name="locale_item_title_size">16sp</dimen>
|
||||||
|
<dimen name="locale_item_subtitle_size">12sp</dimen>
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -108,6 +108,16 @@
|
|||||||
<!-- Browser menu button to configure reader mode appearance e.g. the used font type and size -->
|
<!-- Browser menu button to configure reader mode appearance e.g. the used font type and size -->
|
||||||
<string name="browser_menu_read_appearance">Appearance</string>
|
<string name="browser_menu_read_appearance">Appearance</string>
|
||||||
|
|
||||||
|
<!-- Locale Settings Fragment -->
|
||||||
|
<!-- Content description for tick mark on selected language -->
|
||||||
|
<string name="a11y_selected_locale_content_description">Selected language</string>
|
||||||
|
<!-- Content description for search icon -->
|
||||||
|
<string name="a11y_search_icon_content_description">Search</string>
|
||||||
|
<!-- Text for default locale item -->
|
||||||
|
<string name="default_locale_text">Follow device language</string>
|
||||||
|
<!-- Placeholder text shown in the search bar before a user enters text -->
|
||||||
|
<string name="locale_search_hint">Search language</string>
|
||||||
|
|
||||||
<!-- Search Fragment -->
|
<!-- Search Fragment -->
|
||||||
<!-- Button in the search view that lets a user search by scanning a QR code -->
|
<!-- Button in the search view that lets a user search by scanning a QR code -->
|
||||||
<string name="search_scan_button">Scan</string>
|
<string name="search_scan_button">Scan</string>
|
||||||
|
@ -101,6 +101,15 @@
|
|||||||
app:isPreferenceVisible="@bool/IS_DEBUG" />
|
app:isPreferenceVisible="@bool/IS_DEBUG" />
|
||||||
</androidx.preference.PreferenceCategory>
|
</androidx.preference.PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
android:title="@string/preferences_category_advanced"
|
||||||
|
app:iconSpaceReserved="false">
|
||||||
|
<androidx.preference.Preference
|
||||||
|
android:icon="@drawable/ic_language"
|
||||||
|
android:key="@string/pref_key_language"
|
||||||
|
android:title="@string/preferences_language" />
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:title="@string/developer_tools_category"
|
android:title="@string/developer_tools_category"
|
||||||
app:iconSpaceReserved="false">
|
app:iconSpaceReserved="false">
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
/* 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.advanced
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkObject
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import mozilla.components.support.locale.LocaleManager
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.BuildConfig
|
||||||
|
import org.mozilla.fenix.TestApplication
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(application = TestApplication::class)
|
||||||
|
class LocaleManagerExtensionTest {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
mockkStatic("org.mozilla.fenix.settings.advanced.LocaleManagerExtensionKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en-rUS")
|
||||||
|
fun `build supported locale list`() {
|
||||||
|
val list = LocaleManager.getSupportedLocales()
|
||||||
|
|
||||||
|
// Expect all supported locales + 'follow default option'
|
||||||
|
val expectedSize = BuildConfig.SUPPORTED_LOCALE_ARRAY.size + 1
|
||||||
|
|
||||||
|
assertEquals(expectedSize, list.size)
|
||||||
|
assertTrue(list.isNotEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en-rUS")
|
||||||
|
fun `match current stored locale string with a Locale from our list`() {
|
||||||
|
val context: Context = mockk()
|
||||||
|
mockkObject(LocaleManager)
|
||||||
|
val otherLocale = Locale("fr")
|
||||||
|
val selectedLocale = Locale("en", "UK")
|
||||||
|
val localeList = ArrayList<Locale>()
|
||||||
|
localeList.add(otherLocale)
|
||||||
|
localeList.add(selectedLocale)
|
||||||
|
|
||||||
|
every { LocaleManager.getCurrentLocale(context) } returns selectedLocale
|
||||||
|
|
||||||
|
assertEquals(selectedLocale, LocaleManager.getSelectedLocale(context, localeList))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en-rUS")
|
||||||
|
fun `match null stored locale with the default Locale from our list`() {
|
||||||
|
val context: Context = mockk()
|
||||||
|
mockkObject(LocaleManager)
|
||||||
|
val firstLocale = Locale("fr")
|
||||||
|
val secondLocale = Locale("en", "UK")
|
||||||
|
val localeList = ArrayList<Locale>()
|
||||||
|
localeList.add(firstLocale)
|
||||||
|
localeList.add(secondLocale)
|
||||||
|
|
||||||
|
every { LocaleManager.getCurrentLocale(context) } returns null
|
||||||
|
|
||||||
|
assertEquals("en-US", LocaleManager.getSelectedLocale(context, localeList).toLanguageTag())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
/* 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.advanced
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import io.mockk.Runs
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkObject
|
||||||
|
import io.mockk.verify
|
||||||
|
import mozilla.components.support.locale.LocaleManager
|
||||||
|
import mozilla.components.support.test.mock
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class LocaleSettingsControllerTest {
|
||||||
|
|
||||||
|
private val context: Context = mockk<Activity>(relaxed = true)
|
||||||
|
private val localeSettingsStore: LocaleSettingsStore = mockk(relaxed = true)
|
||||||
|
|
||||||
|
private lateinit var controller: LocaleSettingsController
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
controller = DefaultLocaleSettingsController(context, localeSettingsStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `set a new locale from the list`() {
|
||||||
|
val selectedLocale = Locale("en", "UK")
|
||||||
|
val otherLocale: Locale = mock()
|
||||||
|
every { localeSettingsStore.state } returns LocaleSettingsState(
|
||||||
|
mockk(),
|
||||||
|
mockk(),
|
||||||
|
otherLocale
|
||||||
|
)
|
||||||
|
mockkObject(LocaleManager)
|
||||||
|
every {
|
||||||
|
LocaleManager.setNewLocale(
|
||||||
|
context,
|
||||||
|
selectedLocale.toLanguageTag()
|
||||||
|
)
|
||||||
|
} returns context
|
||||||
|
|
||||||
|
controller.handleLocaleSelected(selectedLocale)
|
||||||
|
|
||||||
|
verify { localeSettingsStore.dispatch(LocaleSettingsAction.Select(selectedLocale)) }
|
||||||
|
verify { LocaleManager.setNewLocale(context, selectedLocale.toLanguageTag()) }
|
||||||
|
verify { (context as Activity).recreate() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `set the default locale as the new locale`() {
|
||||||
|
val selectedLocale = Locale("en", "UK")
|
||||||
|
val localeList = ArrayList<Locale>()
|
||||||
|
localeList.add(selectedLocale)
|
||||||
|
every { localeSettingsStore.state } returns LocaleSettingsState(
|
||||||
|
localeList,
|
||||||
|
mockk(),
|
||||||
|
mockk()
|
||||||
|
)
|
||||||
|
mockkObject(LocaleManager)
|
||||||
|
every { LocaleManager.resetToSystemDefault(context) } just Runs
|
||||||
|
|
||||||
|
controller.handleDefaultLocaleSelected()
|
||||||
|
|
||||||
|
verify { localeSettingsStore.dispatch(LocaleSettingsAction.Select(selectedLocale)) }
|
||||||
|
verify { LocaleManager.resetToSystemDefault(context) }
|
||||||
|
verify { (context as Activity).recreate() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `handle search query typed`() {
|
||||||
|
val query = "Eng"
|
||||||
|
|
||||||
|
controller.handleSearchQueryTyped(query)
|
||||||
|
|
||||||
|
verify { localeSettingsStore.dispatch(LocaleSettingsAction.Search(query)) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/* 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.advanced
|
||||||
|
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class LocaleSettingsInteractorTest {
|
||||||
|
|
||||||
|
private lateinit var interactor: LocaleSettingsInteractor
|
||||||
|
private val controller: LocaleSettingsController = mockk(relaxed = true)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
interactor = LocaleSettingsInteractor(controller)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `locale was selected from list`() {
|
||||||
|
val locale: Locale = mockk()
|
||||||
|
|
||||||
|
interactor.onLocaleSelected(locale)
|
||||||
|
|
||||||
|
verify { controller.handleLocaleSelected(locale) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `default locale was selected from list`() {
|
||||||
|
interactor.onDefaultLocaleSelected()
|
||||||
|
|
||||||
|
verify { controller.handleDefaultLocaleSelected() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search query was typed`() {
|
||||||
|
val query = "Eng"
|
||||||
|
|
||||||
|
interactor.onSearchQueryTyped(query)
|
||||||
|
|
||||||
|
verify { controller.handleSearchQueryTyped(query) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/* 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.advanced
|
||||||
|
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class LocaleSettingsStoreTest {
|
||||||
|
|
||||||
|
private lateinit var localeSettingsStore: LocaleSettingsStore
|
||||||
|
private val selectedLocale = Locale("en", "UK")
|
||||||
|
private val otherLocale = Locale("fr")
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
val localeList = ArrayList<Locale>()
|
||||||
|
localeList.add(Locale("fr")) // default
|
||||||
|
localeList.add(otherLocale)
|
||||||
|
localeList.add(selectedLocale)
|
||||||
|
|
||||||
|
localeSettingsStore =
|
||||||
|
LocaleSettingsStore(LocaleSettingsState(localeList, localeList, selectedLocale))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `change selected locale`() = runBlocking {
|
||||||
|
localeSettingsStore.dispatch(LocaleSettingsAction.Select(otherLocale)).join()
|
||||||
|
|
||||||
|
assertEquals(otherLocale, localeSettingsStore.state.selectedLocale)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `change selected list by search query`() = runBlocking {
|
||||||
|
localeSettingsStore.dispatch(LocaleSettingsAction.Search("Eng")).join()
|
||||||
|
|
||||||
|
assertEquals(2, (localeSettingsStore.state.searchedLocaleList as ArrayList).size)
|
||||||
|
assertEquals(selectedLocale, localeSettingsStore.state.searchedLocaleList[1])
|
||||||
|
}
|
||||||
|
}
|
@ -147,6 +147,7 @@ object Deps {
|
|||||||
const val mozilla_support_utils = "org.mozilla.components:support-utils:${Versions.mozilla_android_components}"
|
const val mozilla_support_utils = "org.mozilla.components:support-utils:${Versions.mozilla_android_components}"
|
||||||
const val mozilla_support_test = "org.mozilla.components:support-test:${Versions.mozilla_android_components}"
|
const val mozilla_support_test = "org.mozilla.components:support-test:${Versions.mozilla_android_components}"
|
||||||
const val mozilla_support_migration = "org.mozilla.components:support-migration:${Versions.mozilla_android_components}"
|
const val mozilla_support_migration = "org.mozilla.components:support-migration:${Versions.mozilla_android_components}"
|
||||||
|
const val mozilla_support_locale = "org.mozilla.components:support-locale:${Versions.mozilla_android_components}"
|
||||||
|
|
||||||
const val sentry = "io.sentry:sentry-android:${Versions.sentry}"
|
const val sentry = "io.sentry:sentry-android:${Versions.sentry}"
|
||||||
const val leakcanary = "com.squareup.leakcanary:leakcanary-android:${Versions.leakcanary}"
|
const val leakcanary = "com.squareup.leakcanary:leakcanary-android:${Versions.leakcanary}"
|
||||||
|
Loading…
Reference in New Issue
Block a user