support Android 13 per-app languages (#2829)
* support Android 13 per-app languages * fix tests * fix language ids in locales_config.xml * fix language setting default in ComposeActivity
This commit is contained in:
parent
9f7cd2fa32
commit
c96a81571c
11 changed files with 165 additions and 89 deletions
|
@ -16,7 +16,6 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
|
@ -92,11 +91,6 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
|||
requesters = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(TuskyApplication.getLocaleManager().setLocale(base));
|
||||
}
|
||||
|
||||
protected boolean requiresLogin() {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.util.Log
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.WorkManager
|
||||
|
@ -44,6 +42,9 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
|||
@Inject
|
||||
lateinit var notificationWorkerFactory: NotificationWorkerFactory
|
||||
|
||||
@Inject
|
||||
lateinit var localeManager: LocaleManager
|
||||
|
||||
override fun onCreate() {
|
||||
// Uncomment me to get StrictMode violation logs
|
||||
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
|
@ -74,6 +75,8 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
|||
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
||||
ThemeUtils.setAppNightMode(theme)
|
||||
|
||||
localeManager.setLocale()
|
||||
|
||||
RxJavaPlugins.setErrorHandler {
|
||||
Log.w("RxJava", "undeliverable exception", it)
|
||||
}
|
||||
|
@ -86,20 +89,5 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
|||
)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
localeManager = LocaleManager(base)
|
||||
super.attachBaseContext(localeManager.setLocale(base))
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
localeManager.setLocale(this)
|
||||
}
|
||||
|
||||
override fun androidInjector() = androidInjector
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
lateinit var localeManager: LocaleManager
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ import androidx.annotation.ColorInt
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
|
@ -561,7 +562,7 @@ class ComposeActivity :
|
|||
private fun getInitialLanguage(language: String? = null): String {
|
||||
return if (language.isNullOrEmpty()) {
|
||||
// Setting the application ui preference sets the default locale
|
||||
Locale.getDefault().language
|
||||
AppCompatDelegate.getApplicationLocales()[0]?.language ?: Locale.getDefault().language
|
||||
} else {
|
||||
language
|
||||
}
|
||||
|
|
|
@ -72,30 +72,17 @@ class PreferencesActivity :
|
|||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
||||
val fragmentTag = "preference_fragment_$EXTRA_PREFERENCE_TYPE"
|
||||
val preferenceType = intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)
|
||||
|
||||
val fragmentTag = "preference_fragment_$preferenceType"
|
||||
|
||||
val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag)
|
||||
?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
|
||||
GENERAL_PREFERENCES -> {
|
||||
setTitle(R.string.action_view_preferences)
|
||||
PreferencesFragment.newInstance()
|
||||
}
|
||||
ACCOUNT_PREFERENCES -> {
|
||||
setTitle(R.string.action_view_account_preferences)
|
||||
AccountPreferencesFragment.newInstance()
|
||||
}
|
||||
NOTIFICATION_PREFERENCES -> {
|
||||
setTitle(R.string.pref_title_edit_notification_settings)
|
||||
NotificationPreferencesFragment.newInstance()
|
||||
}
|
||||
TAB_FILTER_PREFERENCES -> {
|
||||
setTitle(R.string.pref_title_post_tabs)
|
||||
TabFilterPreferencesFragment.newInstance()
|
||||
}
|
||||
PROXY_PREFERENCES -> {
|
||||
setTitle(R.string.pref_title_http_proxy_settings)
|
||||
ProxyPreferencesFragment.newInstance()
|
||||
}
|
||||
?: when (preferenceType) {
|
||||
GENERAL_PREFERENCES -> PreferencesFragment.newInstance()
|
||||
ACCOUNT_PREFERENCES -> AccountPreferencesFragment.newInstance()
|
||||
NOTIFICATION_PREFERENCES -> NotificationPreferencesFragment.newInstance()
|
||||
TAB_FILTER_PREFERENCES -> TabFilterPreferencesFragment.newInstance()
|
||||
PROXY_PREFERENCES -> ProxyPreferencesFragment.newInstance()
|
||||
else -> throw IllegalArgumentException("preferenceType not known")
|
||||
}
|
||||
|
||||
|
@ -103,6 +90,14 @@ class PreferencesActivity :
|
|||
replace(R.id.fragment_container, fragment, fragmentTag)
|
||||
}
|
||||
|
||||
when (preferenceType) {
|
||||
GENERAL_PREFERENCES -> setTitle(R.string.action_view_preferences)
|
||||
ACCOUNT_PREFERENCES -> setTitle(R.string.action_view_account_preferences)
|
||||
NOTIFICATION_PREFERENCES -> setTitle(R.string.pref_title_edit_notification_settings)
|
||||
TAB_FILTER_PREFERENCES -> setTitle(R.string.pref_title_post_tabs)
|
||||
PROXY_PREFERENCES -> setTitle(R.string.pref_title_http_proxy_settings)
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, restartActivitiesOnBackPressedCallback)
|
||||
restartActivitiesOnBackPressedCallback.isEnabled = savedInstanceState?.getBoolean(EXTRA_RESTART_ON_BACK, false) ?: false
|
||||
}
|
||||
|
@ -141,10 +136,6 @@ class PreferencesActivity :
|
|||
"enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> {
|
||||
restartActivitiesOnBackPressedCallback.isEnabled = true
|
||||
}
|
||||
"language" -> {
|
||||
restartActivitiesOnBackPressedCallback.isEnabled = true
|
||||
this.restartCurrentActivity()
|
||||
}
|
||||
}
|
||||
|
||||
eventHub.dispatch(PreferenceChangedEvent(key))
|
||||
|
|
|
@ -30,6 +30,7 @@ import com.keylesspalace.tusky.settings.makePreferenceScreen
|
|||
import com.keylesspalace.tusky.settings.preference
|
||||
import com.keylesspalace.tusky.settings.preferenceCategory
|
||||
import com.keylesspalace.tusky.settings.switchPreference
|
||||
import com.keylesspalace.tusky.util.LocaleManager
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.deserialize
|
||||
import com.keylesspalace.tusky.util.getNonNullString
|
||||
|
@ -46,6 +47,9 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
@Inject
|
||||
lateinit var accountManager: AccountManager
|
||||
|
||||
@Inject
|
||||
lateinit var localeManager: LocaleManager
|
||||
|
||||
private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
|
||||
private var httpProxyPref: Preference? = null
|
||||
|
||||
|
@ -71,10 +75,11 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
setDefaultValue("default")
|
||||
setEntries(R.array.language_entries)
|
||||
setEntryValues(R.array.language_values)
|
||||
key = PrefKeys.LANGUAGE
|
||||
key = PrefKeys.LANGUAGE + "_" // deliberately not the actual key, the real handling happens in LocaleManager
|
||||
setSummaryProvider { entry }
|
||||
setTitle(R.string.pref_title_language)
|
||||
icon = makeIcon(GoogleMaterial.Icon.gmd_translate)
|
||||
preferenceDataStore = localeManager
|
||||
}
|
||||
|
||||
listPreference {
|
||||
|
|
|
@ -17,25 +17,89 @@ package com.keylesspalace.tusky.util
|
|||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import androidx.preference.PreferenceManager
|
||||
import java.util.Locale
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
class LocaleManager(context: Context) {
|
||||
@Singleton
|
||||
class LocaleManager @Inject constructor(
|
||||
val context: Context
|
||||
) : PreferenceDataStore() {
|
||||
|
||||
private var prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
fun setLocale(context: Context): Context {
|
||||
val language = prefs.getNonNullString("language", "default")
|
||||
if (language == "default") {
|
||||
return context
|
||||
}
|
||||
val locale = Locale.forLanguageTag(language)
|
||||
Locale.setDefault(locale)
|
||||
fun setLocale() {
|
||||
val language = prefs.getNonNullString(PrefKeys.LANGUAGE, DEFAULT)
|
||||
|
||||
val res = context.resources
|
||||
val config = Configuration(res.configuration)
|
||||
config.setLocale(locale)
|
||||
return context.createConfigurationContext(config)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (language != HANDLED_BY_SYSTEM) {
|
||||
// app is being opened on Android 13+ for the first time
|
||||
// hand over the old setting to the system and save a dummy value in Shared Preferences
|
||||
applyLanguageToApp(language)
|
||||
|
||||
prefs.edit()
|
||||
.putString(PrefKeys.LANGUAGE, HANDLED_BY_SYSTEM)
|
||||
.apply()
|
||||
}
|
||||
} else {
|
||||
// on Android < 13 we have to apply the language at every app start
|
||||
applyLanguageToApp(language)
|
||||
}
|
||||
}
|
||||
|
||||
override fun putString(key: String?, value: String?) {
|
||||
|
||||
// if we are on Android < 13 we have to save the selected language so we can apply it at appstart
|
||||
// on Android 13+ the system handles it for us
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
prefs.edit()
|
||||
.putString(PrefKeys.LANGUAGE, value)
|
||||
.apply()
|
||||
}
|
||||
applyLanguageToApp(value)
|
||||
}
|
||||
|
||||
override fun getString(key: String?, defValue: String?): String? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val selectedLanguage = AppCompatDelegate.getApplicationLocales()
|
||||
|
||||
if (selectedLanguage.isEmpty) {
|
||||
DEFAULT
|
||||
} else {
|
||||
// Android lets users select all variants of languages we support in the system settings,
|
||||
// so we need to find the closest match
|
||||
// it should not happen that we find no match, but returning null is fine (picker will show default)
|
||||
|
||||
val availableLanguages = context.resources.getStringArray(R.array.language_values)
|
||||
|
||||
return availableLanguages.find { it == selectedLanguage[0]!!.toLanguageTag() }
|
||||
?: availableLanguages.find { language ->
|
||||
language.startsWith(selectedLanguage[0]!!.language)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
prefs.getNonNullString(PrefKeys.LANGUAGE, DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyLanguageToApp(language: String?) {
|
||||
val localeList = if (language == DEFAULT) {
|
||||
LocaleListCompat.getEmptyLocaleList()
|
||||
} else {
|
||||
LocaleListCompat.forLanguageTags(language)
|
||||
}
|
||||
|
||||
AppCompatDelegate.setApplicationLocales(localeList)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT = "default"
|
||||
private const val HANDLED_BY_SYSTEM = "handled_by_system"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue