diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 1ec1fa563..63d7cb436 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -5,12 +5,12 @@ plugins { } android { - compileSdk 32 + compileSdk 33 defaultConfig { applicationId "chat.simplex.app" minSdk 29 - targetSdk 32 + targetSdk 33 versionCode 103 versionName "4.5.4" @@ -79,7 +79,7 @@ android { def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null // if (isRelease) { // Comma separated list of languages that will be included in the apk - android.defaultConfig.resConfigs("en", "ru", "de", "fr", "it", "nl", "cs", "zh-rCN") + android.defaultConfig.resConfigs("en", "cs", "de", "fr", "it", "nl", "ru", "zh-rCN") // } } diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml index eb5f13055..3476fe605 100644 --- a/apps/android/app/src/main/AndroidManifest.xml +++ b/apps/android/app/src/main/AndroidManifest.xml @@ -30,6 +30,7 @@ android:label="${app_name}" android:extractNativeLibs="${extract_native_libs}" android:supportsRtl="true" + android:localeConfig="@xml/locales_config" android:theme="@style/Theme.SimpleX"> diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index e26fa02eb..c868a9731 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -3,9 +3,7 @@ package chat.simplex.app import android.app.Application import android.content.Intent import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.os.Parcelable +import android.os.* import android.os.SystemClock.elapsedRealtime import android.util.Log import android.view.WindowManager @@ -68,6 +66,7 @@ class MainActivity: FragmentActivity() { super.onCreate(savedInstanceState) // testJson() val m = vm.chatModel + applyAppLocale(m.controller.appPrefs.appLanguage) // When call ended and orientation changes, it re-process old intent, it's unneeded. // Only needed to be processed on first creation of activity if (savedInstanceState == null) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index 0af27cc12..923f71b97 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -38,6 +38,8 @@ class SimplexApp: Application(), LifecycleEventObserver { var isAppOnForeground: Boolean = false + val defaultLocale: Locale = Locale.getDefault() + fun initChatController(useKey: String? = null, startChat: Boolean = true) { val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 5b2872724..35b918776 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -133,6 +133,7 @@ class AppPreferences(val context: Context) { val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false) val connectViaLinkTab = mkStrPreference(SHARED_PREFS_CONNECT_VIA_LINK_TAB, ConnectViaLinkTab.SCAN.name) val liveMessageAlertShown = mkBoolPreference(SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN, false) + val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null) val storeDBPassphrase = mkBoolPreference(SHARED_PREFS_STORE_DB_PASSPHRASE, true) val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false) @@ -214,6 +215,7 @@ class AppPreferences(val context: Context) { private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName" private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime" + private const val SHARED_PREFS_APP_LANGUAGE = "AppLanguage" private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index 777f7d110..9f089425a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -1,12 +1,15 @@ package chat.simplex.app.views.helpers +import android.app.Activity import android.app.Application +import android.app.LocaleManager import android.content.Context +import android.content.res.Configuration import android.content.res.Resources import android.graphics.* import android.graphics.Typeface import android.net.Uri -import android.os.FileUtils +import android.os.* import android.provider.OpenableColumns import android.text.Spanned import android.text.SpannedString @@ -29,8 +32,7 @@ import androidx.compose.ui.unit.* import androidx.core.content.FileProvider import androidx.core.text.HtmlCompat import chat.simplex.app.* -import chat.simplex.app.model.CIFile -import chat.simplex.app.model.json +import chat.simplex.app.model.* import kotlinx.coroutines.* import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString @@ -501,3 +503,34 @@ inline fun serializableSaver(): Saver = Saver( save = { json.encodeToString(it) }, restore = { json.decodeFromString(it) } ) + +fun saveAppLocale(pref: SharedPreference, activity: Activity, languageCode: String? = null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val localeManager = SimplexApp.context.getSystemService(LocaleManager::class.java) + localeManager.applicationLocales = LocaleList(Locale.forLanguageTag(languageCode ?: return)) + } else { + pref.set(languageCode) + if (languageCode == null) { + activity.applyLocale(SimplexApp.context.defaultLocale) + } + activity.recreate() + } +} + +fun Activity.applyAppLocale(pref: SharedPreference) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + val lang = pref.get() + if (lang == null || lang == Locale.getDefault().language) return + applyLocale(Locale.forLanguageTag(lang)) + } +} + +private fun Activity.applyLocale(locale: Locale) { + Locale.setDefault(locale) + val appConf = Configuration(SimplexApp.context.resources.configuration).apply { setLocale(locale) } + val activityConf = Configuration(resources.configuration).apply { setLocale(locale) } + @Suppress("DEPRECATION") + SimplexApp.context.resources.updateConfiguration(appConf, resources.displayMetrics) + @Suppress("DEPRECATION") + resources.updateConfiguration(activityConf, resources.displayMetrics) +} \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt index d63989438..67f92e53a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt @@ -2,13 +2,20 @@ package chat.simplex.app.views.usersettings import SectionCustomFooter import SectionDivider +import SectionItemView import SectionItemViewSpaceBetween +import SectionItemWithValue import SectionSpacer import SectionView +import android.app.Activity import android.content.ComponentName +import android.content.Intent import android.content.pm.PackageManager import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED +import android.net.Uri +import android.os.Build +import android.provider.Settings import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow @@ -17,6 +24,7 @@ import androidx.compose.material.MaterialTheme.colors import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Circle import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow @@ -30,9 +38,13 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import chat.simplex.app.* import chat.simplex.app.R +import chat.simplex.app.model.ChatModel +import chat.simplex.app.model.SharedPreference import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* import com.godaddy.android.colorpicker.* +import kotlinx.coroutines.delay +import java.util.* enum class AppIcon(val resId: Int) { DEFAULT(R.mipmap.icon), @@ -40,7 +52,7 @@ enum class AppIcon(val resId: Int) { } @Composable -fun AppearanceView() { +fun AppearanceView(m: ChatModel) { val appIcon = remember { mutableStateOf(findEnabledIcon()) } fun setAppIcon(newIcon: AppIcon) { @@ -62,6 +74,7 @@ fun AppearanceView() { AppearanceLayout( appIcon, + m.controller.appPrefs.appLanguage, changeIcon = ::setAppIcon, editPrimaryColor = { primary -> ModalManager.shared.showModalCloseable { close -> @@ -73,6 +86,7 @@ fun AppearanceView() { @Composable fun AppearanceLayout( icon: MutableState, + languagePref: SharedPreference, changeIcon: (AppIcon) -> Unit, editPrimaryColor: (Color) -> Unit, ) { @@ -81,6 +95,37 @@ fun AppearanceView() { horizontalAlignment = Alignment.Start, ) { AppBarTitle(stringResource(R.string.appearance_settings)) + SectionView(stringResource(R.string.settings_section_title_language), padding = PaddingValues()) { + val context = LocalContext.current + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + SectionItemWithValue( + generalGetString(R.string.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() }, + remember { mutableStateOf("system") }, + listOf(ValueTitleDesc("system", generalGetString(R.string.change_verb), "")), + onSelected = { openSystemLangPicker(context as? Activity ?: return@SectionItemWithValue) } + ) + } else { + val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") } + SectionItemView { + LangSelector(state) { + state.value = it + withApi { + delay(200) + val activity = context as? Activity + if (activity != null) { + if (it == "system") { + saveAppLocale(languagePref, activity) + } else { + saveAppLocale(languagePref, activity, it) + } + } + } + } + } + } + } + SectionSpacer() + SectionView(stringResource(R.string.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) { LazyRow { items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index -> @@ -179,6 +224,31 @@ fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) { ) } +@Composable +private fun LangSelector(state: State, onSelected: (String) -> Unit) { + // Should be the same as in app/build.gradle's `android.defaultConfig.resConfigs` + val supportedLanguages = mapOf( + "system" to generalGetString(R.string.language_system), + "en" to "English", + "cs" to "Čeština", + "de" to "Deutsch", + "fr" to "Français", + "it" to "Italiano", + "nl" to "Nederlands", + "ru" to "Русский", + "zh-CN" to "简体中文" + ) + val values by remember { mutableStateOf(supportedLanguages.map { it.key to it.value }) } + ExposedDropDownSettingRow( + generalGetString(R.string.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() }, + values, + state, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = onSelected + ) +} + @Composable private fun ThemeSelector(state: State, onSelected: (DefaultTheme) -> Unit) { val darkTheme = isSystemInDarkTheme() @@ -193,6 +263,9 @@ private fun ThemeSelector(state: State, onSelected: (DefaultTheme) ) } +private fun openSystemLangPicker(activity: Activity) { + activity.startActivity(Intent(Settings.ACTION_APP_LOCALE_SETTINGS, Uri.parse("package:" + SimplexApp.context.packageName))) +} private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon -> SimplexApp.context.packageManager.getComponentEnabledSetting( @@ -206,6 +279,7 @@ fun PreviewAppearanceSettings() { SimpleXTheme { AppearanceLayout( icon = remember { mutableStateOf(AppIcon.DARK_BLUE) }, + languagePref = SharedPreference({ null }, {}), changeIcon = {}, editPrimaryColor = {}, ) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index f6d4e6fc9..a43524daa 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -160,7 +160,7 @@ fun SettingsLayout( SectionDivider() SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped) SectionDivider() - SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView() }, disabled = stopped) + SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView(it) }, disabled = stopped) SectionDivider() DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) } diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 17b37d9cd..158dbc0a1 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -696,6 +696,7 @@ Developer tools Experimental features SOCKS PROXY + LANGUAGE APP ICON THEMES MESSAGES @@ -1018,6 +1019,9 @@ Light Dark + + System + Theme Save color diff --git a/apps/android/app/src/main/res/xml/locales_config.xml b/apps/android/app/src/main/res/xml/locales_config.xml new file mode 100644 index 000000000..22fe04d1e --- /dev/null +++ b/apps/android/app/src/main/res/xml/locales_config.xml @@ -0,0 +1,11 @@ + + + + + + + + + + +