From c96a81571ccc616ceb5c0b7bac241f4c7f799423 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 16 Nov 2022 19:45:18 +0100 Subject: [PATCH] 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 --- app/src/main/AndroidManifest.xml | 3 +- .../com/keylesspalace/tusky/BaseActivity.java | 6 -- .../keylesspalace/tusky/TuskyApplication.kt | 22 +---- .../components/compose/ComposeActivity.kt | 3 +- .../preference/PreferencesActivity.kt | 43 ++++----- .../preference/PreferencesFragment.kt | 7 +- .../keylesspalace/tusky/util/LocaleManager.kt | 92 ++++++++++++++++--- app/src/main/res/values/donottranslate.xml | 8 +- app/src/main/res/xml/locales_config.xml | 50 ++++++++++ .../keylesspalace/tusky/TuskyApplication.kt | 18 ---- gradle/libs.versions.toml | 2 +- 11 files changed, 165 insertions(+), 89 deletions(-) create mode 100644 app/src/main/res/xml/locales_config.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1df0b780..3bfe4c41 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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"> (); } - @Override - protected void attachBaseContext(Context base) { - super.attachBaseContext(TuskyApplication.getLocaleManager().setLocale(base)); - } - protected boolean requiresLogin() { return true; } diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index ded947a8..5401b593 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -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 - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index 70f9d94d..a408e217 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -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 } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt index 878766fa..54bb4a4d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt @@ -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)) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt index ce28a414..6e6d2d43 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt @@ -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 { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt b/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt index 45f3ab37..6795317b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt @@ -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" } } diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 5562b20f..ef0fab09 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -90,7 +90,7 @@ cs cy de - en-gb + en-GB en eo es @@ -103,7 +103,7 @@ it hu nl - nb-no + nb-NO oc pl pt-BR @@ -118,8 +118,8 @@ uk ar ckb - bn-bd - bn-in + bn-BD + bn-IN fa hi sa diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml new file mode 100644 index 00000000..3c2bb007 --- /dev/null +++ b/app/src/main/res/xml/locales_config.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt index 9598f2c1..a9b06631 100644 --- a/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -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 - } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f671e305..3f384f4e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"