Merge pull request #1368 from vector-im/feature/switch_language

Feature/switch language
This commit is contained in:
Benoit Marty 2020-05-18 16:22:07 +02:00 committed by GitHub
commit adac80062c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 607 additions and 268 deletions

View File

@ -2,13 +2,13 @@ Changes in RiotX 0.21.0 (2020-XX-XX)
===================================================
Features ✨:
-
- Switch language support (#41)
Improvements 🙌:
- Better connectivity lost indicator when airplane mode is on
Bugfix 🐛:
-
- Fix issues with FontScale switch (#69, #645)
Translations 🗣:
-

View File

@ -6,6 +6,7 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
<#if createViewEvents>
@ -38,7 +39,8 @@ class ${viewModelClass} @AssistedInject constructor(@Assisted initialState: ${vi
}
override fun handle(action: ${actionClass}) {
//TODO
}
when (action) {
}.exhaustive
}
}

View File

@ -61,8 +61,6 @@ import im.vector.riotx.features.login.LoginSplashFragment
import im.vector.riotx.features.login.LoginWaitForEmailFragment
import im.vector.riotx.features.login.LoginWebFragment
import im.vector.riotx.features.login.terms.LoginTermsFragment
import im.vector.riotx.features.userdirectory.KnownUsersFragment
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
import im.vector.riotx.features.qrcode.QrCodeScannerFragment
import im.vector.riotx.features.reactions.EmojiChooserFragment
import im.vector.riotx.features.reactions.EmojiSearchResultFragment
@ -92,9 +90,12 @@ import im.vector.riotx.features.settings.devtools.IncomingKeyRequestListFragment
import im.vector.riotx.features.settings.devtools.KeyRequestsFragment
import im.vector.riotx.features.settings.devtools.OutgoingKeyRequestListFragment
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
import im.vector.riotx.features.settings.locale.LocalePickerFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment
import im.vector.riotx.features.share.IncomingShareFragment
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
import im.vector.riotx.features.userdirectory.KnownUsersFragment
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
@Module
interface FragmentModule {
@ -109,6 +110,11 @@ interface FragmentModule {
@FragmentKey(RoomListFragment::class)
fun bindRoomListFragment(fragment: RoomListFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LocalePickerFragment::class)
fun bindLocalePickerFragment(fragment: LocalePickerFragment): Fragment
@Binds
@IntoMap
@FragmentKey(GroupListFragment::class)

View File

@ -130,9 +130,9 @@ interface VectorComponent {
fun emojiDataSource(): EmojiDataSource
fun alertManager() : PopupAlertManager
fun alertManager(): PopupAlertManager
fun reAuthHelper() : ReAuthHelper
fun reAuthHelper(): ReAuthHelper
@Component.Factory
interface Factory {

View File

@ -16,6 +16,7 @@
package im.vector.riotx.core.extensions
import android.app.Activity
import android.os.Parcelable
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
@ -59,3 +60,8 @@ fun <T : Fragment> VectorBaseActivity.addFragmentToBackstack(frameId: Int,
fun VectorBaseActivity.hideKeyboard() {
currentFocus?.hideKeyboard()
}
fun Activity.restart() {
startActivity(intent)
finish()
}

View File

@ -179,7 +179,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
}
})
sessionListener = getVectorComponent().sessionListener()
sessionListener = vectorComponent.sessionListener()
sessionListener.globalErrorLiveData.observeEvent(this) {
handleGlobalError(it)
}

View File

@ -33,9 +33,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import im.vector.riotx.R
import im.vector.riotx.features.notifications.NotificationUtils
import im.vector.riotx.features.settings.VectorLocale
import timber.log.Timber
import java.util.Locale
/**
* Tells if the application ignores battery optimizations.
@ -94,24 +91,6 @@ fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = t
}
}
/**
* Provides the device locale
*
* @return the device locale
*/
fun getDeviceLocale(context: Context): Locale {
return try {
val packageManager = context.packageManager
val resources = packageManager.getResourcesForApplication("android")
@Suppress("DEPRECATION")
resources.configuration.locale
} catch (e: Exception) {
Timber.e(e, "## getDeviceLocale() failed")
// Fallback to application locale
VectorLocale.applicationLocale
}
}
/**
* Shows notification settings for the current app.
* In android O will directly opens the notification settings, in lower version it will show the App settings

View File

@ -30,62 +30,30 @@ import javax.inject.Inject
/**
* Handle locale configuration change, such as theme, font size and locale chosen by the user
*/
class VectorConfiguration @Inject constructor(private val context: Context) {
// TODO Import mLanguageReceiver From Riot?
fun onConfigurationChanged() {
if (Locale.getDefault().toString() != VectorLocale.applicationLocale.toString()) {
Timber.v("## onConfigurationChanged(): the locale has been updated to ${Locale.getDefault()}")
Timber.v("## onConfigurationChanged(): restore the expected value ${VectorLocale.applicationLocale}")
updateApplicationSettings(VectorLocale.applicationLocale,
FontScale.getFontScalePrefValue(context),
ThemeUtils.getApplicationTheme(context))
Locale.setDefault(VectorLocale.applicationLocale)
}
}
private fun updateApplicationSettings(locale: Locale, textSize: String, theme: String) {
VectorLocale.saveApplicationLocale(context, locale)
FontScale.saveFontScale(context, textSize)
Locale.setDefault(locale)
val config = Configuration(context.resources.configuration)
@Suppress("DEPRECATION")
config.locale = locale
config.fontScale = FontScale.getFontScale(context)
@Suppress("DEPRECATION")
context.resources.updateConfiguration(config, context.resources.displayMetrics)
ThemeUtils.setApplicationTheme(context, theme)
// TODO PhoneNumberUtils.onLocaleUpdate()
}
/**
* Update the application theme
*
* @param theme the new theme
*/
fun updateApplicationTheme(theme: String) {
ThemeUtils.setApplicationTheme(context, theme)
updateApplicationSettings(VectorLocale.applicationLocale,
FontScale.getFontScalePrefValue(context),
theme)
}
/**
* Init the configuration from the saved one
*/
fun initConfiguration() {
VectorLocale.init(context)
val locale = VectorLocale.applicationLocale
val fontScale = FontScale.getFontScale(context)
val fontScale = FontScale.getFontScaleValue(context)
val theme = ThemeUtils.getApplicationTheme(context)
Locale.setDefault(locale)
val config = Configuration(context.resources.configuration)
@Suppress("DEPRECATION")
config.locale = locale
config.fontScale = fontScale
config.fontScale = fontScale.scale
@Suppress("DEPRECATION")
context.resources.updateConfiguration(config, context.resources.displayMetrics)
@ -93,16 +61,6 @@ class VectorConfiguration @Inject constructor(private val context: Context) {
ThemeUtils.setApplicationTheme(context, theme)
}
/**
* Update the application locale
*
* @param locale
*/
// TODO Call from LanguagePickerActivity
fun updateApplicationLocale(locale: Locale) {
updateApplicationSettings(locale, FontScale.getFontScalePrefValue(context), ThemeUtils.getApplicationTheme(context))
}
/**
* Compute a localised context
*
@ -115,7 +73,7 @@ class VectorConfiguration @Inject constructor(private val context: Context) {
val resources = context.resources
val locale = VectorLocale.applicationLocale
val configuration = resources.configuration
configuration.fontScale = FontScale.getFontScale(context)
configuration.fontScale = FontScale.getFontScaleValue(context).scale
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
configuration.setLocale(locale)
@ -142,10 +100,9 @@ class VectorConfiguration @Inject constructor(private val context: Context) {
* Compute the locale status value
* @return the local status value
*/
// TODO Create data class for this
fun getHash(): String {
return (VectorLocale.applicationLocale.toString()
+ "_" + FontScale.getFontScalePrefValue(context)
+ "_" + FontScale.getFontScaleValue(context).preferenceValue
+ "_" + ThemeUtils.getApplicationTheme(context))
}
}

View File

@ -164,16 +164,16 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment()
@OnClick(R.id.keys_backup_setup_step2_button)
fun doNext() {
when {
viewModel.passphrase.value.isNullOrEmpty() -> {
viewModel.passphrase.value.isNullOrEmpty() -> {
viewModel.passphraseError.value = context?.getString(R.string.passphrase_empty_error_message)
}
viewModel.passphrase.value != viewModel.confirmPassphrase.value -> {
viewModel.confirmPassphraseError.value = context?.getString(R.string.passphrase_passphrase_does_not_match)
}
viewModel.passwordStrength.value?.score ?: 0 < 4 -> {
viewModel.passwordStrength.value?.score ?: 0 < 4 -> {
viewModel.passphraseError.value = context?.getString(R.string.passphrase_passphrase_too_weak)
}
else -> {
else -> {
viewModel.megolmBackupCreationInfo = null
viewModel.prepareRecoveryKey(activity!!, viewModel.passphrase.value)
@ -190,7 +190,7 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment()
viewModel.prepareRecoveryKey(activity!!, null)
}
else -> {
else -> {
// User has entered a passphrase but want to skip this step.
viewModel.passphraseError.value = context?.getString(R.string.keys_backup_passphrase_not_empty_error_message)
}

View File

@ -31,9 +31,9 @@ import im.vector.riotx.BuildConfig
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.toOnOff
import im.vector.riotx.core.utils.getDeviceLocale
import im.vector.riotx.features.settings.VectorLocale
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.settings.locale.SystemLocaleProvider
import im.vector.riotx.features.themes.ThemeUtils
import im.vector.riotx.features.version.VersionProvider
import okhttp3.Call
@ -58,10 +58,13 @@ import javax.inject.Singleton
* BugReporter creates and sends the bug reports.
*/
@Singleton
class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val versionProvider: VersionProvider,
private val vectorPreferences: VectorPreferences,
private val vectorFileLogger: VectorFileLogger) {
class BugReporter @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
private val versionProvider: VersionProvider,
private val vectorPreferences: VectorPreferences,
private val vectorFileLogger: VectorFileLogger,
private val systemLocaleProvider: SystemLocaleProvider
) {
var inMultiWindowMode = false
companion object {
@ -240,7 +243,7 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes
+ Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME)
.addFormDataPart("locale", Locale.getDefault().toString())
.addFormDataPart("app_language", VectorLocale.applicationLocale.toString())
.addFormDataPart("default_app_language", getDeviceLocale(context).toString())
.addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString())
.addFormDataPart("theme", ThemeUtils.getApplicationTheme(context))
val buildNumber = context.getString(R.string.build_number)

View File

@ -17,7 +17,7 @@
package im.vector.riotx.features.settings
import android.content.Context
import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import im.vector.riotx.R
@ -29,124 +29,59 @@ object FontScale {
// Key for the SharedPrefs
private const val APPLICATION_FONT_SCALE_KEY = "APPLICATION_FONT_SCALE_KEY"
// Possible values for the SharedPrefs
private const val FONT_SCALE_TINY = "FONT_SCALE_TINY"
private const val FONT_SCALE_SMALL = "FONT_SCALE_SMALL"
private const val FONT_SCALE_NORMAL = "FONT_SCALE_NORMAL"
private const val FONT_SCALE_LARGE = "FONT_SCALE_LARGE"
private const val FONT_SCALE_LARGER = "FONT_SCALE_LARGER"
private const val FONT_SCALE_LARGEST = "FONT_SCALE_LARGEST"
private const val FONT_SCALE_HUGE = "FONT_SCALE_HUGE"
private val fontScaleToPrefValue = mapOf(
0.70f to FONT_SCALE_TINY,
0.85f to FONT_SCALE_SMALL,
1.00f to FONT_SCALE_NORMAL,
1.15f to FONT_SCALE_LARGE,
1.30f to FONT_SCALE_LARGER,
1.45f to FONT_SCALE_LARGEST,
1.60f to FONT_SCALE_HUGE
data class FontScaleValue(
val index: Int,
// Possible values for the SharedPrefs
val preferenceValue: String,
val scale: Float,
@StringRes
val nameResId: Int
)
private val prefValueToNameResId = mapOf(
FONT_SCALE_TINY to R.string.tiny,
FONT_SCALE_SMALL to R.string.small,
FONT_SCALE_NORMAL to R.string.normal,
FONT_SCALE_LARGE to R.string.large,
FONT_SCALE_LARGER to R.string.larger,
FONT_SCALE_LARGEST to R.string.largest,
FONT_SCALE_HUGE to R.string.huge
private val fontScaleValues = listOf(
FontScaleValue(0, "FONT_SCALE_TINY", 0.70f, R.string.tiny),
FontScaleValue(1, "FONT_SCALE_SMALL", 0.85f, R.string.small),
FontScaleValue(2, "FONT_SCALE_NORMAL", 1.00f, R.string.normal),
FontScaleValue(3, "FONT_SCALE_LARGE", 1.15f, R.string.large),
FontScaleValue(4, "FONT_SCALE_LARGER", 1.30f, R.string.larger),
FontScaleValue(5, "FONT_SCALE_LARGEST", 1.45f, R.string.largest),
FontScaleValue(6, "FONT_SCALE_HUGE", 1.60f, R.string.huge)
)
private val normalFontScaleValue = fontScaleValues[2]
/**
* Get the font scale value from SharedPrefs. Init the SharedPrefs if necessary
*
* @return the font scale
* @return the font scale value
*/
fun getFontScalePrefValue(context: Context): String {
fun getFontScaleValue(context: Context): FontScaleValue {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
var scalePreferenceValue: String
if (APPLICATION_FONT_SCALE_KEY !in preferences) {
return if (APPLICATION_FONT_SCALE_KEY !in preferences) {
val fontScale = context.resources.configuration.fontScale
scalePreferenceValue = FONT_SCALE_NORMAL
if (fontScaleToPrefValue.containsKey(fontScale)) {
scalePreferenceValue = fontScaleToPrefValue[fontScale] as String
}
preferences.edit {
putString(APPLICATION_FONT_SCALE_KEY, scalePreferenceValue)
}
(fontScaleValues.firstOrNull { it.scale == fontScale } ?: normalFontScaleValue)
.also { preferences.edit { putString(APPLICATION_FONT_SCALE_KEY, it.preferenceValue) } }
} else {
scalePreferenceValue = preferences.getString(APPLICATION_FONT_SCALE_KEY, FONT_SCALE_NORMAL)!!
val pref = preferences.getString(APPLICATION_FONT_SCALE_KEY, null)
fontScaleValues.firstOrNull { it.preferenceValue == pref } ?: normalFontScaleValue
}
}
return scalePreferenceValue
fun updateFontScale(context: Context, index: Int) {
fontScaleValues.getOrNull(index)?.let {
saveFontScaleValue(context, it)
}
}
/**
* Provides the font scale value
* Store the font scale vale
*
* @return the font scale
* @param fontScaleValue the font scale value to store
*/
fun getFontScale(context: Context): Float {
val fontScale = getFontScalePrefValue(context)
if (fontScaleToPrefValue.containsValue(fontScale)) {
for ((key, value) in fontScaleToPrefValue) {
if (value == fontScale) {
return key
}
}
}
return 1.0f
}
/**
* Provides the font scale description
*
* @return the font description
*/
fun getFontScaleDescription(context: Context): String {
val fontScale = getFontScalePrefValue(context)
return if (prefValueToNameResId.containsKey(fontScale)) {
context.getString(prefValueToNameResId[fontScale] as Int)
} else context.getString(R.string.normal)
}
/**
* Update the font size from the locale description.
*
* @param fontScaleDescription the font scale description
*/
fun updateFontScale(context: Context, fontScaleDescription: String) {
for ((key, value) in prefValueToNameResId) {
if (context.getString(value) == fontScaleDescription) {
saveFontScale(context, key)
}
}
val config = Configuration(context.resources.configuration)
config.fontScale = getFontScale(context)
@Suppress("DEPRECATION")
context.resources.updateConfiguration(config, context.resources.displayMetrics)
}
/**
* Save the new font scale
*
* @param scaleValue the text scale
*/
fun saveFontScale(context: Context, scaleValue: String) {
if (scaleValue.isNotEmpty()) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit {
putString(APPLICATION_FONT_SCALE_KEY, scaleValue)
}
}
private fun saveFontScaleValue(context: Context, fontScaleValue: FontScaleValue) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit { putString(APPLICATION_FONT_SCALE_KEY, fontScaleValue.preferenceValue) }
}
}

View File

@ -19,13 +19,11 @@ package im.vector.riotx.features.settings
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import androidx.preference.PreferenceManager
import androidx.core.content.edit
import im.vector.riotx.BuildConfig
import androidx.preference.PreferenceManager
import im.vector.riotx.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.Locale
@ -41,10 +39,9 @@ object VectorLocale {
private val defaultLocale = Locale("en", "US")
/**
* The supported application languages
* The cache of supported application languages
*/
var supportedLocales = ArrayList<Locale>()
private set
private val supportedLocales = mutableListOf<Locale>()
/**
* Provides the current application locale
@ -52,10 +49,13 @@ object VectorLocale {
var applicationLocale = defaultLocale
private set
lateinit var context: Context
/**
* Init this object
*/
fun init(context: Context) {
this.context = context
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
if (preferences.contains(APPLICATION_LOCALE_LANGUAGE_KEY)) {
@ -72,19 +72,14 @@ object VectorLocale {
applicationLocale = defaultLocale
}
saveApplicationLocale(context, applicationLocale)
}
// init the known locales in background, using kotlin coroutines
GlobalScope.launch(Dispatchers.IO) {
initApplicationLocales(context)
saveApplicationLocale(applicationLocale)
}
}
/**
* Save the new application locale.
*/
fun saveApplicationLocale(context: Context, locale: Locale) {
fun saveApplicationLocale(locale: Locale) {
applicationLocale = locale
PreferenceManager.getDefaultSharedPreferences(context).edit {
@ -144,6 +139,7 @@ object VectorLocale {
} else {
val resources = context.resources
val conf = resources.configuration
@Suppress("DEPRECATION")
val savedLocale = conf.locale
@Suppress("DEPRECATION")
@ -165,11 +161,9 @@ object VectorLocale {
}
/**
* Provides the supported application locales list
*
* @param context the context
* Init the supported application locales list
*/
private fun initApplicationLocales(context: Context) {
private fun initApplicationLocales() {
val knownLocalesSet = HashSet<Triple<String, String, String>>()
try {
@ -195,9 +189,7 @@ object VectorLocale {
)
}
supportedLocales.clear()
knownLocalesSet.mapTo(supportedLocales) { (language, country, script) ->
val list = knownLocalesSet.map { (language, country, script) ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Locale.Builder()
.setLanguage(language)
@ -208,9 +200,11 @@ object VectorLocale {
Locale(language, country)
}
}
// sort by human display names
.sortedBy { localeToLocalisedString(it).toLowerCase(it) }
// sort by human display names
supportedLocales.sortWith(Comparator { lhs, rhs -> localeToLocalisedString(lhs).compareTo(localeToLocalisedString(rhs)) })
supportedLocales.clear()
supportedLocales.addAll(list)
}
/**
@ -235,22 +229,39 @@ object VectorLocale {
append(locale.getDisplayCountry(locale))
append(")")
}
// In debug mode, also display information about the locale in the current locale.
if (BuildConfig.DEBUG) {
append("\n[")
append(locale.displayLanguage)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && locale.script != "Latn") {
append(" - ")
append(locale.displayScript)
}
if (locale.displayCountry.isNotEmpty()) {
append(" (")
append(locale.displayCountry)
append(")")
}
append("]")
}
}
}
/**
* Information about the locale in the current locale
*
* @param locale the locale to get info from
* @return the string
*/
fun localeToLocalisedStringInfo(locale: Locale): String {
return buildString {
append("[")
append(locale.displayLanguage)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && locale.script != "Latn") {
append(" - ")
append(locale.displayScript)
}
if (locale.displayCountry.isNotEmpty()) {
append(" (")
append(locale.displayCountry)
append(")")
}
append("]")
}
}
suspend fun getSupportedLocales(): List<Locale> {
if (supportedLocales.isEmpty()) {
// init the known locales in background
withContext(Dispatchers.IO) {
initApplicationLocales()
}
}
return supportedLocales
}
}

View File

@ -18,13 +18,13 @@ package im.vector.riotx.features.settings
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.widget.CheckedTextView
import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.SwitchPreference
import im.vector.riotx.R
import im.vector.riotx.core.extensions.restart
import im.vector.riotx.core.preference.VectorListPreference
import im.vector.riotx.core.preference.VectorPreference
import im.vector.riotx.features.configuration.VectorConfiguration
@ -54,13 +54,9 @@ class VectorSettingsPreferencesFragment @Inject constructor(
findPreference<VectorListPreference>(ThemeUtils.APPLICATION_THEME_KEY)!!
.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
if (newValue is String) {
vectorConfiguration.updateApplicationTheme(newValue)
ThemeUtils.setApplicationTheme(requireContext(), newValue)
// Restart the Activity
activity?.let {
// Note: recreate does not apply the color correctly
it.startActivity(it.intent)
it.finish()
}
activity?.restart()
true
} else {
false
@ -129,21 +125,6 @@ class VectorSettingsPreferencesFragment @Inject constructor(
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
REQUEST_LOCALE -> {
activity?.let {
startActivity(it.intent)
it.finish()
}
}
}
}
}
// ==============================================================================================================
// user interface management
// ==============================================================================================================
@ -152,14 +133,8 @@ class VectorSettingsPreferencesFragment @Inject constructor(
// Selected language
selectedLanguagePreference.summary = VectorLocale.localeToLocalisedString(VectorLocale.applicationLocale)
selectedLanguagePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
notImplemented()
// TODO startActivityForResult(LanguagePickerActivity.getIntent(activity), REQUEST_LOCALE)
true
}
// Text size
textSizePreference.summary = FontScale.getFontScaleDescription(activity!!)
textSizePreference.summary = getString(FontScale.getFontScaleValue(activity!!).nameResId)
textSizePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
activity?.let { displayTextSizeSelection(it) }
@ -182,25 +157,20 @@ class VectorSettingsPreferencesFragment @Inject constructor(
val childCount = linearLayout.childCount
val scaleText = FontScale.getFontScaleDescription(activity)
val index = FontScale.getFontScaleValue(activity).index
for (i in 0 until childCount) {
val v = linearLayout.getChildAt(i)
if (v is CheckedTextView) {
v.isChecked = v.text == scaleText
v.isChecked = i == index
v.setOnClickListener {
dialog.dismiss()
FontScale.updateFontScale(activity, v.text.toString())
activity.startActivity(activity.intent)
activity.finish()
FontScale.updateFontScale(activity, i)
activity.restart()
}
}
}
}
companion object {
private const val REQUEST_LOCALE = 777
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.locale
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.ClickListener
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.epoxy.onClick
import im.vector.riotx.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_locale)
abstract class LocaleItem : VectorEpoxyModel<LocaleItem.Holder>() {
@EpoxyAttribute var title: String? = null
@EpoxyAttribute var subtitle: String? = null
@EpoxyAttribute var clickListener: ClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.onClick { clickListener?.invoke() }
holder.titleView.setTextOrHide(title)
holder.subtitleView.setTextOrHide(subtitle)
}
class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.localeTitle)
val subtitleView by bind<TextView>(R.id.localeSubtitle)
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.locale
import im.vector.riotx.core.platform.VectorViewModelAction
import java.util.Locale
sealed class LocalePickerAction : VectorViewModelAction {
data class SelectLocale(val locale: Locale) : LocalePickerAction()
}

View File

@ -0,0 +1,93 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.locale
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.epoxy.noResultItem
import im.vector.riotx.core.epoxy.profiles.profileSectionItem
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.settings.VectorLocale
import im.vector.riotx.features.settings.VectorPreferences
import java.util.Locale
import javax.inject.Inject
class LocalePickerController @Inject constructor(
private val vectorPreferences: VectorPreferences,
private val stringProvider: StringProvider
) : TypedEpoxyController<LocalePickerViewState>() {
var listener: Listener? = null
@ExperimentalStdlibApi
override fun buildModels(data: LocalePickerViewState?) {
val list = data?.locales ?: return
profileSectionItem {
id("currentTitle")
title(stringProvider.getString(R.string.choose_locale_current_locale_title))
}
localeItem {
id(data.currentLocale.toString())
title(VectorLocale.localeToLocalisedString(data.currentLocale).capitalize(data.currentLocale))
if (vectorPreferences.developerMode()) {
subtitle(VectorLocale.localeToLocalisedStringInfo(data.currentLocale))
}
clickListener { listener?.onUseCurrentClicked() }
}
profileSectionItem {
id("otherTitle")
title(stringProvider.getString(R.string.choose_locale_other_locales_title))
}
when (list) {
is Incomplete -> {
loadingItem {
id("loading")
loadingText(stringProvider.getString(R.string.choose_locale_loading_locales))
}
}
is Success ->
if (list().isEmpty()) {
noResultItem {
id("noResult")
text(stringProvider.getString(R.string.no_result_placeholder))
}
} else {
list()
.filter { it.toString() != data.currentLocale.toString() }
.forEach {
localeItem {
id(it.toString())
title(VectorLocale.localeToLocalisedString(it).capitalize(it))
if (vectorPreferences.developerMode()) {
subtitle(VectorLocale.localeToLocalisedStringInfo(it))
}
clickListener { listener?.onLocaleClicked(it) }
}
}
}
}
}
interface Listener {
fun onUseCurrentClicked()
fun onLocaleClicked(locale: Locale)
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.locale
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.restart
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_locale_picker.*
import java.util.Locale
import javax.inject.Inject
class LocalePickerFragment @Inject constructor(
private val viewModelFactory: LocalePickerViewModel.Factory,
private val controller: LocalePickerController
) : VectorBaseFragment(), LocalePickerViewModel.Factory by viewModelFactory, LocalePickerController.Listener {
private val viewModel: LocalePickerViewModel by fragmentViewModel()
override fun getLayoutResId() = R.layout.fragment_locale_picker
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
localeRecyclerView.configureWith(controller)
controller.listener = this
viewModel.observeViewEvents {
when (it) {
LocalePickerViewEvents.RestartActivity -> {
activity?.restart()
}
}.exhaustive
}
}
override fun onDestroyView() {
super.onDestroyView()
localeRecyclerView.cleanup()
controller.listener = null
}
override fun invalidate() = withState(viewModel) { state ->
controller.setData(state)
}
override fun onUseCurrentClicked() {
// Just close the fragment
parentFragmentManager.popBackStack()
}
override fun onLocaleClicked(locale: Locale) {
viewModel.handle(LocalePickerAction.SelectLocale(locale))
}
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_select_language)
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.locale
import im.vector.riotx.core.platform.VectorViewEvents
sealed class LocalePickerViewEvents : VectorViewEvents {
object RestartActivity : LocalePickerViewEvents()
}

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.locale
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.settings.VectorLocale
import kotlinx.coroutines.launch
class LocalePickerViewModel @AssistedInject constructor(
@Assisted initialState: LocalePickerViewState
) : VectorViewModel<LocalePickerViewState, LocalePickerAction, LocalePickerViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: LocalePickerViewState): LocalePickerViewModel
}
init {
viewModelScope.launch {
val result = VectorLocale.getSupportedLocales()
setState {
copy(
locales = Success(result)
)
}
}
}
companion object : MvRxViewModelFactory<LocalePickerViewModel, LocalePickerViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: LocalePickerViewState): LocalePickerViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
}
}
override fun handle(action: LocalePickerAction) {
when (action) {
is LocalePickerAction.SelectLocale -> handleSelectLocale(action)
}.exhaustive
}
private fun handleSelectLocale(action: LocalePickerAction.SelectLocale) {
VectorLocale.saveApplicationLocale(action.locale)
_viewEvents.post(LocalePickerViewEvents.RestartActivity)
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.locale
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.riotx.features.settings.VectorLocale
import java.util.Locale
data class LocalePickerViewState(
val currentLocale: Locale = VectorLocale.applicationLocale,
val locales: Async<List<Locale>> = Uninitialized
) : MvRxState

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.locale
import android.content.Context
import timber.log.Timber
import java.util.Locale
import javax.inject.Inject
class SystemLocaleProvider @Inject constructor(
private val context: Context
) {
/**
* Provides the device locale
*
* @return the device locale, or null in case of error
*/
fun getSystemLocale(): Locale? {
return try {
val packageManager = context.packageManager
val resources = packageManager.getResourcesForApplication("android")
@Suppress("DEPRECATION")
resources.configuration.locale
} catch (e: Exception) {
Timber.e(e, "## getDeviceLocale() failed")
null
}
}
}

View File

@ -52,7 +52,7 @@ object ThemeUtils {
*/
fun getApplicationTheme(context: Context): String {
return PreferenceManager.getDefaultSharedPreferences(context)
.getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE)!!
.getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) ?: THEME_LIGHT_VALUE
}
/**

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/localeRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background"
tools:listitem="@layout/item_locale" />

View File

@ -0,0 +1,41 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:minHeight="64dp">
<TextView
android:id="@+id/localeTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@+id/localeSubtitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="English" />
<TextView
android:id="@+id/localeSubtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:textColor="?riotx_text_secondary"
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/localeTitle"
tools:text="details"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2393,4 +2393,8 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
</plurals>
<string name="invite_users_to_room_failure">We could not invite users. Please check the users you want to invite and try again.</string>
<string name="choose_locale_current_locale_title">Current language</string>
<string name="choose_locale_other_locales_title">Other available languages</string>
<string name="choose_locale_loading_locales">Loading available languages…</string>
</resources>

View File

@ -7,9 +7,10 @@
android:title="@string/settings_user_interface">
<im.vector.riotx.core.preference.VectorPreference
android:dialogTitle="@string/settings_select_language"
android:key="SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY"
android:title="@string/settings_interface_language" />
android:persistent="false"
android:title="@string/settings_interface_language"
app:fragment="im.vector.riotx.features.settings.locale.LocalePickerFragment" />
<im.vector.riotx.core.preference.VectorListPreference
android:defaultValue="light"
@ -23,6 +24,7 @@
<im.vector.riotx.core.preference.VectorPreference
android:dialogTitle="@string/font_size"
android:key="SETTINGS_INTERFACE_TEXT_SIZE_KEY"
android:persistent="false"
android:title="@string/font_size" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>