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
|
@ -18,7 +18,8 @@
|
|||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/TuskyTheme"
|
||||
android:usesCleartextTraffic="false">
|
||||
android:usesCleartextTraffic="false"
|
||||
android:localeConfig="@xml/locales_config">
|
||||
|
||||
<activity
|
||||
android:name=".SplashActivity"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
<item>cs</item>
|
||||
<item>cy</item>
|
||||
<item>de</item>
|
||||
<item>en-gb</item>
|
||||
<item>en-GB</item>
|
||||
<item>en</item>
|
||||
<item>eo</item>
|
||||
<item>es</item>
|
||||
|
@ -103,7 +103,7 @@
|
|||
<item>it</item>
|
||||
<item>hu</item>
|
||||
<item>nl</item>
|
||||
<item>nb-no</item>
|
||||
<item>nb-NO</item>
|
||||
<item>oc</item>
|
||||
<item>pl</item>
|
||||
<item>pt-BR</item>
|
||||
|
@ -118,8 +118,8 @@
|
|||
<item>uk</item>
|
||||
<item>ar</item>
|
||||
<item>ckb</item>
|
||||
<item>bn-bd</item>
|
||||
<item>bn-in</item>
|
||||
<item>bn-BD</item>
|
||||
<item>bn-IN</item>
|
||||
<item>fa</item>
|
||||
<item>hi</item>
|
||||
<item>sa</item>
|
||||
|
|
50
app/src/main/res/xml/locales_config.xml
Normal file
50
app/src/main/res/xml/locales_config.xml
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<locale android:name="en"/>
|
||||
<locale android:name="ca"/>
|
||||
<locale android:name="cs"/>
|
||||
<locale android:name="cy"/>
|
||||
<locale android:name="de"/>
|
||||
<locale android:name="en-GB"/>
|
||||
<locale android:name="en"/>
|
||||
<locale android:name="eo"/>
|
||||
<locale android:name="es"/>
|
||||
<locale android:name="eu"/>
|
||||
<locale android:name="fr"/>
|
||||
<locale android:name="ga"/>
|
||||
<locale android:name="gd"/>
|
||||
<locale android:name="gl"/>
|
||||
<locale android:name="is"/>
|
||||
<locale android:name="it"/>
|
||||
<locale android:name="hu"/>
|
||||
<locale android:name="nl"/>
|
||||
<locale android:name="nb-NO"/>
|
||||
<locale android:name="oc"/>
|
||||
<locale android:name="pl"/>
|
||||
<locale android:name="pt-BR"/>
|
||||
<locale android:name="pt-PT"/>
|
||||
<locale android:name="sl"/>
|
||||
<locale android:name="sv"/>
|
||||
<locale android:name="kab"/>
|
||||
<locale android:name="vi"/>
|
||||
<locale android:name="tr"/>
|
||||
<locale android:name="bg"/>
|
||||
<locale android:name="ru"/>
|
||||
<locale android:name="uk"/>
|
||||
<locale android:name="ar"/>
|
||||
<locale android:name="ckb"/>
|
||||
<locale android:name="bn-BD"/>
|
||||
<locale android:name="bn-IN"/>
|
||||
<locale android:name="fa"/>
|
||||
<locale android:name="hi"/>
|
||||
<locale android:name="sa"/>
|
||||
<locale android:name="ta"/>
|
||||
<locale android:name="th"/>
|
||||
<locale android:name="ko"/>
|
||||
<locale android:name="zh-TW"/>
|
||||
<locale android:name="zh-SG"/>
|
||||
<locale android:name="zh-MO"/>
|
||||
<locale android:name="zh-CN"/>
|
||||
<locale android:name="zh-HK"/>
|
||||
<locale android:name="ja"/>
|
||||
</locale-config>
|
|
@ -16,9 +16,6 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import com.keylesspalace.tusky.util.LocaleManager
|
||||
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
|
||||
import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
|
||||
|
||||
|
@ -29,19 +26,4 @@ class TuskyApplication : Application() {
|
|||
super.onCreate()
|
||||
EmojiPackHelper.init(this, DefaultEmojiPackList.get(this))
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
localeManager = LocaleManager(base)
|
||||
super.attachBaseContext(localeManager.setLocale(base))
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
localeManager.setLocale(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
lateinit var localeManager: LocaleManager
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[versions]
|
||||
agp = "7.2.2"
|
||||
androidx-activity = "1.6.0"
|
||||
androidx-appcompat = "1.5.1"
|
||||
androidx-appcompat = "1.6.0-rc01"
|
||||
androidx-browser = "1.4.0"
|
||||
androidx-cardview = "1.0.0"
|
||||
androidx-constraintlayout = "2.1.4"
|
||||
|
|
Loading…
Reference in a new issue