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>fennec/nightly
parent
9cbc3f7a4a
commit
ea2411a88b
@ -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()
|
||||
}
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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])
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue