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 ✨: Features ✨:
- - Switch language support (#41)
Improvements 🙌: Improvements 🙌:
- Better connectivity lost indicator when airplane mode is on - Better connectivity lost indicator when airplane mode is on
Bugfix 🐛: Bugfix 🐛:
- - Fix issues with FontScale switch (#69, #645)
Translations 🗣: Translations 🗣:
- -

View File

@ -6,6 +6,7 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
<#if createViewEvents> <#if createViewEvents>
@ -38,7 +39,8 @@ class ${viewModelClass} @AssistedInject constructor(@Assisted initialState: ${vi
} }
override fun handle(action: ${actionClass}) { 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.LoginWaitForEmailFragment
import im.vector.riotx.features.login.LoginWebFragment import im.vector.riotx.features.login.LoginWebFragment
import im.vector.riotx.features.login.terms.LoginTermsFragment 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.qrcode.QrCodeScannerFragment
import im.vector.riotx.features.reactions.EmojiChooserFragment import im.vector.riotx.features.reactions.EmojiChooserFragment
import im.vector.riotx.features.reactions.EmojiSearchResultFragment 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.KeyRequestsFragment
import im.vector.riotx.features.settings.devtools.OutgoingKeyRequestListFragment import im.vector.riotx.features.settings.devtools.OutgoingKeyRequestListFragment
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment 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.settings.push.PushGatewaysFragment
import im.vector.riotx.features.share.IncomingShareFragment import im.vector.riotx.features.share.IncomingShareFragment
import im.vector.riotx.features.signout.soft.SoftLogoutFragment import im.vector.riotx.features.signout.soft.SoftLogoutFragment
import im.vector.riotx.features.userdirectory.KnownUsersFragment
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
@Module @Module
interface FragmentModule { interface FragmentModule {
@ -109,6 +110,11 @@ interface FragmentModule {
@FragmentKey(RoomListFragment::class) @FragmentKey(RoomListFragment::class)
fun bindRoomListFragment(fragment: RoomListFragment): Fragment fun bindRoomListFragment(fragment: RoomListFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LocalePickerFragment::class)
fun bindLocalePickerFragment(fragment: LocalePickerFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(GroupListFragment::class) @FragmentKey(GroupListFragment::class)

View File

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

View File

@ -16,6 +16,7 @@
package im.vector.riotx.core.extensions package im.vector.riotx.core.extensions
import android.app.Activity
import android.os.Parcelable import android.os.Parcelable
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.FragmentTransaction
@ -59,3 +60,8 @@ fun <T : Fragment> VectorBaseActivity.addFragmentToBackstack(frameId: Int,
fun VectorBaseActivity.hideKeyboard() { fun VectorBaseActivity.hideKeyboard() {
currentFocus?.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) { sessionListener.globalErrorLiveData.observeEvent(this) {
handleGlobalError(it) handleGlobalError(it)
} }

View File

@ -33,9 +33,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.notifications.NotificationUtils 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. * 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. * 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 * 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 * Handle locale configuration change, such as theme, font size and locale chosen by the user
*/ */
class VectorConfiguration @Inject constructor(private val context: Context) { class VectorConfiguration @Inject constructor(private val context: Context) {
// TODO Import mLanguageReceiver From Riot?
fun onConfigurationChanged() { fun onConfigurationChanged() {
if (Locale.getDefault().toString() != VectorLocale.applicationLocale.toString()) { if (Locale.getDefault().toString() != VectorLocale.applicationLocale.toString()) {
Timber.v("## onConfigurationChanged(): the locale has been updated to ${Locale.getDefault()}") Timber.v("## onConfigurationChanged(): the locale has been updated to ${Locale.getDefault()}")
Timber.v("## onConfigurationChanged(): restore the expected value ${VectorLocale.applicationLocale}") Timber.v("## onConfigurationChanged(): restore the expected value ${VectorLocale.applicationLocale}")
updateApplicationSettings(VectorLocale.applicationLocale, Locale.setDefault(VectorLocale.applicationLocale)
FontScale.getFontScalePrefValue(context),
ThemeUtils.getApplicationTheme(context))
} }
} }
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 * Init the configuration from the saved one
*/ */
fun initConfiguration() { fun initConfiguration() {
VectorLocale.init(context) VectorLocale.init(context)
val locale = VectorLocale.applicationLocale val locale = VectorLocale.applicationLocale
val fontScale = FontScale.getFontScale(context) val fontScale = FontScale.getFontScaleValue(context)
val theme = ThemeUtils.getApplicationTheme(context) val theme = ThemeUtils.getApplicationTheme(context)
Locale.setDefault(locale) Locale.setDefault(locale)
val config = Configuration(context.resources.configuration) val config = Configuration(context.resources.configuration)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
config.locale = locale config.locale = locale
config.fontScale = fontScale config.fontScale = fontScale.scale
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
context.resources.updateConfiguration(config, context.resources.displayMetrics) context.resources.updateConfiguration(config, context.resources.displayMetrics)
@ -93,16 +61,6 @@ class VectorConfiguration @Inject constructor(private val context: Context) {
ThemeUtils.setApplicationTheme(context, theme) 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 * Compute a localised context
* *
@ -115,7 +73,7 @@ class VectorConfiguration @Inject constructor(private val context: Context) {
val resources = context.resources val resources = context.resources
val locale = VectorLocale.applicationLocale val locale = VectorLocale.applicationLocale
val configuration = resources.configuration val configuration = resources.configuration
configuration.fontScale = FontScale.getFontScale(context) configuration.fontScale = FontScale.getFontScaleValue(context).scale
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
configuration.setLocale(locale) configuration.setLocale(locale)
@ -142,10 +100,9 @@ class VectorConfiguration @Inject constructor(private val context: Context) {
* Compute the locale status value * Compute the locale status value
* @return the local status value * @return the local status value
*/ */
// TODO Create data class for this
fun getHash(): String { fun getHash(): String {
return (VectorLocale.applicationLocale.toString() return (VectorLocale.applicationLocale.toString()
+ "_" + FontScale.getFontScalePrefValue(context) + "_" + FontScale.getFontScaleValue(context).preferenceValue
+ "_" + ThemeUtils.getApplicationTheme(context)) + "_" + ThemeUtils.getApplicationTheme(context))
} }
} }

View File

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

View File

@ -17,7 +17,7 @@
package im.vector.riotx.features.settings package im.vector.riotx.features.settings
import android.content.Context import android.content.Context
import android.content.res.Configuration import androidx.annotation.StringRes
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import im.vector.riotx.R import im.vector.riotx.R
@ -29,124 +29,59 @@ object FontScale {
// Key for the SharedPrefs // Key for the SharedPrefs
private const val APPLICATION_FONT_SCALE_KEY = "APPLICATION_FONT_SCALE_KEY" private const val APPLICATION_FONT_SCALE_KEY = "APPLICATION_FONT_SCALE_KEY"
data class FontScaleValue(
val index: Int,
// Possible values for the SharedPrefs // Possible values for the SharedPrefs
private const val FONT_SCALE_TINY = "FONT_SCALE_TINY" val preferenceValue: String,
private const val FONT_SCALE_SMALL = "FONT_SCALE_SMALL" val scale: Float,
private const val FONT_SCALE_NORMAL = "FONT_SCALE_NORMAL" @StringRes
private const val FONT_SCALE_LARGE = "FONT_SCALE_LARGE" val nameResId: Int
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
) )
private val prefValueToNameResId = mapOf( private val fontScaleValues = listOf(
FONT_SCALE_TINY to R.string.tiny, FontScaleValue(0, "FONT_SCALE_TINY", 0.70f, R.string.tiny),
FONT_SCALE_SMALL to R.string.small, FontScaleValue(1, "FONT_SCALE_SMALL", 0.85f, R.string.small),
FONT_SCALE_NORMAL to R.string.normal, FontScaleValue(2, "FONT_SCALE_NORMAL", 1.00f, R.string.normal),
FONT_SCALE_LARGE to R.string.large, FontScaleValue(3, "FONT_SCALE_LARGE", 1.15f, R.string.large),
FONT_SCALE_LARGER to R.string.larger, FontScaleValue(4, "FONT_SCALE_LARGER", 1.30f, R.string.larger),
FONT_SCALE_LARGEST to R.string.largest, FontScaleValue(5, "FONT_SCALE_LARGEST", 1.45f, R.string.largest),
FONT_SCALE_HUGE to R.string.huge 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 * 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) 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 val fontScale = context.resources.configuration.fontScale
scalePreferenceValue = FONT_SCALE_NORMAL (fontScaleValues.firstOrNull { it.scale == fontScale } ?: normalFontScaleValue)
.also { preferences.edit { putString(APPLICATION_FONT_SCALE_KEY, it.preferenceValue) } }
if (fontScaleToPrefValue.containsKey(fontScale)) {
scalePreferenceValue = fontScaleToPrefValue[fontScale] as String
}
preferences.edit {
putString(APPLICATION_FONT_SCALE_KEY, scalePreferenceValue)
}
} else { } 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 { private fun saveFontScaleValue(context: Context, fontScaleValue: FontScaleValue) {
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) PreferenceManager.getDefaultSharedPreferences(context)
.edit { .edit { putString(APPLICATION_FONT_SCALE_KEY, fontScaleValue.preferenceValue) }
putString(APPLICATION_FONT_SCALE_KEY, scaleValue)
}
}
} }
} }

View File

@ -19,13 +19,11 @@ package im.vector.riotx.features.settings
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import androidx.preference.PreferenceManager
import androidx.core.content.edit import androidx.core.content.edit
import im.vector.riotx.BuildConfig import androidx.preference.PreferenceManager
import im.vector.riotx.R import im.vector.riotx.R
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.withContext
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.Locale import java.util.Locale
@ -41,10 +39,9 @@ object VectorLocale {
private val defaultLocale = Locale("en", "US") private val defaultLocale = Locale("en", "US")
/** /**
* The supported application languages * The cache of supported application languages
*/ */
var supportedLocales = ArrayList<Locale>() private val supportedLocales = mutableListOf<Locale>()
private set
/** /**
* Provides the current application locale * Provides the current application locale
@ -52,10 +49,13 @@ object VectorLocale {
var applicationLocale = defaultLocale var applicationLocale = defaultLocale
private set private set
lateinit var context: Context
/** /**
* Init this object * Init this object
*/ */
fun init(context: Context) { fun init(context: Context) {
this.context = context
val preferences = PreferenceManager.getDefaultSharedPreferences(context) val preferences = PreferenceManager.getDefaultSharedPreferences(context)
if (preferences.contains(APPLICATION_LOCALE_LANGUAGE_KEY)) { if (preferences.contains(APPLICATION_LOCALE_LANGUAGE_KEY)) {
@ -72,19 +72,14 @@ object VectorLocale {
applicationLocale = defaultLocale applicationLocale = defaultLocale
} }
saveApplicationLocale(context, applicationLocale) saveApplicationLocale(applicationLocale)
}
// init the known locales in background, using kotlin coroutines
GlobalScope.launch(Dispatchers.IO) {
initApplicationLocales(context)
} }
} }
/** /**
* Save the new application locale. * Save the new application locale.
*/ */
fun saveApplicationLocale(context: Context, locale: Locale) { fun saveApplicationLocale(locale: Locale) {
applicationLocale = locale applicationLocale = locale
PreferenceManager.getDefaultSharedPreferences(context).edit { PreferenceManager.getDefaultSharedPreferences(context).edit {
@ -144,6 +139,7 @@ object VectorLocale {
} else { } else {
val resources = context.resources val resources = context.resources
val conf = resources.configuration val conf = resources.configuration
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val savedLocale = conf.locale val savedLocale = conf.locale
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ -165,11 +161,9 @@ object VectorLocale {
} }
/** /**
* Provides the supported application locales list * Init the supported application locales list
*
* @param context the context
*/ */
private fun initApplicationLocales(context: Context) { private fun initApplicationLocales() {
val knownLocalesSet = HashSet<Triple<String, String, String>>() val knownLocalesSet = HashSet<Triple<String, String, String>>()
try { try {
@ -195,9 +189,7 @@ object VectorLocale {
) )
} }
supportedLocales.clear() val list = knownLocalesSet.map { (language, country, script) ->
knownLocalesSet.mapTo(supportedLocales) { (language, country, script) ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Locale.Builder() Locale.Builder()
.setLanguage(language) .setLanguage(language)
@ -208,9 +200,11 @@ object VectorLocale {
Locale(language, country) Locale(language, country)
} }
} }
// sort by human display names // sort by human display names
supportedLocales.sortWith(Comparator { lhs, rhs -> localeToLocalisedString(lhs).compareTo(localeToLocalisedString(rhs)) }) .sortedBy { localeToLocalisedString(it).toLowerCase(it) }
supportedLocales.clear()
supportedLocales.addAll(list)
} }
/** /**
@ -235,10 +229,18 @@ object VectorLocale {
append(locale.getDisplayCountry(locale)) append(locale.getDisplayCountry(locale))
append(")") append(")")
} }
}
}
// In debug mode, also display information about the locale in the current locale. /**
if (BuildConfig.DEBUG) { * Information about the locale in the current locale
append("\n[") *
* @param locale the locale to get info from
* @return the string
*/
fun localeToLocalisedStringInfo(locale: Locale): String {
return buildString {
append("[")
append(locale.displayLanguage) append(locale.displayLanguage)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && locale.script != "Latn") { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && locale.script != "Latn") {
append(" - ") append(" - ")
@ -252,5 +254,14 @@ object VectorLocale {
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.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.widget.CheckedTextView import android.widget.CheckedTextView
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import im.vector.riotx.R 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.VectorListPreference
import im.vector.riotx.core.preference.VectorPreference import im.vector.riotx.core.preference.VectorPreference
import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.configuration.VectorConfiguration
@ -54,13 +54,9 @@ class VectorSettingsPreferencesFragment @Inject constructor(
findPreference<VectorListPreference>(ThemeUtils.APPLICATION_THEME_KEY)!! findPreference<VectorListPreference>(ThemeUtils.APPLICATION_THEME_KEY)!!
.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
if (newValue is String) { if (newValue is String) {
vectorConfiguration.updateApplicationTheme(newValue) ThemeUtils.setApplicationTheme(requireContext(), newValue)
// Restart the Activity // Restart the Activity
activity?.let { activity?.restart()
// Note: recreate does not apply the color correctly
it.startActivity(it.intent)
it.finish()
}
true true
} else { } else {
false 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 // user interface management
// ============================================================================================================== // ==============================================================================================================
@ -152,14 +133,8 @@ class VectorSettingsPreferencesFragment @Inject constructor(
// Selected language // Selected language
selectedLanguagePreference.summary = VectorLocale.localeToLocalisedString(VectorLocale.applicationLocale) selectedLanguagePreference.summary = VectorLocale.localeToLocalisedString(VectorLocale.applicationLocale)
selectedLanguagePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
notImplemented()
// TODO startActivityForResult(LanguagePickerActivity.getIntent(activity), REQUEST_LOCALE)
true
}
// Text size // Text size
textSizePreference.summary = FontScale.getFontScaleDescription(activity!!) textSizePreference.summary = getString(FontScale.getFontScaleValue(activity!!).nameResId)
textSizePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { textSizePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
activity?.let { displayTextSizeSelection(it) } activity?.let { displayTextSizeSelection(it) }
@ -182,25 +157,20 @@ class VectorSettingsPreferencesFragment @Inject constructor(
val childCount = linearLayout.childCount val childCount = linearLayout.childCount
val scaleText = FontScale.getFontScaleDescription(activity) val index = FontScale.getFontScaleValue(activity).index
for (i in 0 until childCount) { for (i in 0 until childCount) {
val v = linearLayout.getChildAt(i) val v = linearLayout.getChildAt(i)
if (v is CheckedTextView) { if (v is CheckedTextView) {
v.isChecked = v.text == scaleText v.isChecked = i == index
v.setOnClickListener { v.setOnClickListener {
dialog.dismiss() dialog.dismiss()
FontScale.updateFontScale(activity, v.text.toString()) FontScale.updateFontScale(activity, i)
activity.startActivity(activity.intent) activity.restart()
activity.finish()
} }
} }
} }
} }
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 { fun getApplicationTheme(context: Context): String {
return PreferenceManager.getDefaultSharedPreferences(context) 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> </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="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> </resources>

View File

@ -7,9 +7,10 @@
android:title="@string/settings_user_interface"> android:title="@string/settings_user_interface">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:dialogTitle="@string/settings_select_language"
android:key="SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY" 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 <im.vector.riotx.core.preference.VectorListPreference
android:defaultValue="light" android:defaultValue="light"
@ -23,6 +24,7 @@
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:dialogTitle="@string/font_size" android:dialogTitle="@string/font_size"
android:key="SETTINGS_INTERFACE_TEXT_SIZE_KEY" android:key="SETTINGS_INTERFACE_TEXT_SIZE_KEY"
android:persistent="false"
android:title="@string/font_size" /> android:title="@string/font_size" />
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>