mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-13 11:29:01 -04:00
Launch system language picker for API 33+ (#3145)
This commit is contained in:
@@ -48,7 +48,6 @@ import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
import com.geeksville.mesh.ui.common.theme.MODE_DYNAMIC
|
||||
import com.geeksville.mesh.ui.intro.AppIntroductionScreen
|
||||
import com.geeksville.mesh.ui.sharing.toSharedContact
|
||||
import com.geeksville.mesh.util.LanguageUtils
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import javax.inject.Inject
|
||||
@@ -78,10 +77,6 @@ class MainActivity :
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val lang = uiPrefs.lang
|
||||
if (lang != LanguageUtils.SYSTEM_MANAGED) LanguageUtils.migrateLanguagePrefs(uiPrefs)
|
||||
info("in-app language is ${LanguageUtils.getLocale()}")
|
||||
|
||||
if (uiPrefs.appIntroCompleted) {
|
||||
(application as GeeksvilleApplication).askToRate(this)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import android.content.SharedPreferences
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
import com.geeksville.mesh.model.NodeSortOption
|
||||
import com.geeksville.mesh.util.LanguageUtils
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -29,7 +28,6 @@ import kotlinx.coroutines.flow.update
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
interface UiPrefs {
|
||||
var lang: String
|
||||
var theme: Int
|
||||
val themeFlow: StateFlow<Int>
|
||||
var appIntroCompleted: Boolean
|
||||
@@ -84,7 +82,6 @@ class UiPrefsImpl(private val prefs: SharedPreferences) : UiPrefs {
|
||||
prefs.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
}
|
||||
|
||||
override var lang: String by PrefDelegate(prefs, "lang", LanguageUtils.SYSTEM_DEFAULT)
|
||||
override var hasShownNotPairedWarning: Boolean by PrefDelegate(prefs, "has_shown_not_paired_warning", false)
|
||||
override var nodeSortOption: Int by PrefDelegate(prefs, "node-sort-option", NodeSortOption.VIA_FAVORITE.ordinal)
|
||||
override var includeUnknown: Boolean by PrefDelegate(prefs, "include-unknown", false)
|
||||
|
||||
@@ -20,6 +20,8 @@ package com.geeksville.mesh.ui.settings
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings.ACTION_APP_LOCALE_SETTINGS
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -30,6 +32,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material.icons.rounded.FormatPaint
|
||||
import androidx.compose.material.icons.rounded.Language
|
||||
@@ -49,6 +52,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.BuildConfig
|
||||
@@ -69,6 +73,7 @@ import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
|
||||
import com.geeksville.mesh.ui.settings.radio.components.EditDeviceProfileDialog
|
||||
import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog
|
||||
import com.geeksville.mesh.util.LanguageUtils
|
||||
import com.geeksville.mesh.util.LanguageUtils.getLanguageMap
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -271,12 +276,27 @@ fun SettingsScreen(
|
||||
settingsViewModel.setProvideLocation(!provideLocation)
|
||||
}
|
||||
|
||||
val settingsLauncher =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {}
|
||||
// On Android 12 and below, system app settings for language are not available. Use the in-app language
|
||||
// picker for these devices.
|
||||
val useInAppLangPicker = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
SettingsItem(
|
||||
text = stringResource(R.string.preferences_language),
|
||||
leadingIcon = Icons.Rounded.Language,
|
||||
trailingIcon = null,
|
||||
trailingIcon = if (useInAppLangPicker) null else Icons.AutoMirrored.Rounded.KeyboardArrowRight,
|
||||
) {
|
||||
showLanguagePickerDialog = true
|
||||
if (useInAppLangPicker) {
|
||||
showLanguagePickerDialog = true
|
||||
} else {
|
||||
val intent = Intent(ACTION_APP_LOCALE_SETTINGS, "package:${context.packageName}".toUri())
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
settingsLauncher.launch(intent)
|
||||
} else {
|
||||
// Fall back to the in-app picker
|
||||
showLanguagePickerDialog = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsItem(
|
||||
@@ -383,14 +403,17 @@ private fun AppVersionButton(excludedModulesUnlocked: Boolean, onUnlockExcludedM
|
||||
@Composable
|
||||
private fun LanguagePickerDialog(onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val languages = remember {
|
||||
LanguageUtils.getLanguageTags(context).mapValues { (_, value) -> { LanguageUtils.setLocale(value) } }
|
||||
val choices = remember {
|
||||
context
|
||||
.getLanguageMap()
|
||||
.map { (languageTag, languageName) -> languageName to { LanguageUtils.setAppLocale(languageTag) } }
|
||||
.toMap()
|
||||
}
|
||||
|
||||
MultipleChoiceAlertDialog(
|
||||
title = stringResource(R.string.preferences_language),
|
||||
message = "",
|
||||
choices = languages,
|
||||
choices = choices,
|
||||
onDismissRequest = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,61 +22,59 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.android.prefs.UiPrefs
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.util.Locale
|
||||
|
||||
object LanguageUtils : Logging {
|
||||
|
||||
const val SYSTEM_DEFAULT = "zz"
|
||||
const val SYSTEM_MANAGED = "appcompat"
|
||||
|
||||
fun getLocale(): String = AppCompatDelegate.getApplicationLocales().toLanguageTags().ifEmpty { SYSTEM_DEFAULT }
|
||||
|
||||
fun setLocale(lang: String) {
|
||||
fun setAppLocale(languageTag: String) {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
if (lang == SYSTEM_DEFAULT) {
|
||||
if (languageTag == SYSTEM_DEFAULT) {
|
||||
LocaleListCompat.getEmptyLocaleList()
|
||||
} else {
|
||||
LocaleListCompat.forLanguageTags(lang)
|
||||
LocaleListCompat.forLanguageTags(languageTag)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun migrateLanguagePrefs(uiPrefs: UiPrefs) {
|
||||
val currentLang = uiPrefs.lang
|
||||
debug("Migrating in-app language prefs: $currentLang")
|
||||
uiPrefs.lang = SYSTEM_MANAGED
|
||||
setLocale(currentLang)
|
||||
}
|
||||
/** Using locales_config.xml, maps language tags to their localized language names (e.g.: "en" -> "English") */
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
fun Context.getLanguageMap(): Map<String, String> {
|
||||
val languageTags = buildList {
|
||||
add(SYSTEM_DEFAULT)
|
||||
|
||||
/** Build a list from locales_config.xml of native language names paired to its Locale tag (ex: "English", "en") */
|
||||
fun getLanguageTags(context: Context): Map<String, String> {
|
||||
val languageTags = mutableListOf(SYSTEM_DEFAULT)
|
||||
try {
|
||||
context.resources.getXml(R.xml.locales_config).use {
|
||||
while (it.eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (it.eventType == XmlPullParser.START_TAG && it.name == "locale") {
|
||||
it.getAttributeValue(0)?.let { tag -> languageTags += tag }
|
||||
try {
|
||||
resources.getXml(R.xml.locales_config).use { parser ->
|
||||
while (parser.eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.eventType == XmlPullParser.START_TAG && parser.name == "locale") {
|
||||
val languageTag =
|
||||
parser.getAttributeValue("http://schemas.android.com/apk/res/android", "name")
|
||||
languageTag?.let { add(it) }
|
||||
}
|
||||
parser.next()
|
||||
}
|
||||
it.next()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errormsg("Error parsing locale_config.xml: ${e.message}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errormsg("Error parsing locale_config.xml ${e.message}")
|
||||
}
|
||||
return languageTags.associateBy { tag ->
|
||||
val loc = Locale.forLanguageTag(tag)
|
||||
when (tag) {
|
||||
SYSTEM_DEFAULT -> context.getString(R.string.preferences_system_default)
|
||||
"fr-HT" -> context.getString(R.string.fr_HT)
|
||||
"pt-BR" -> context.getString(R.string.pt_BR)
|
||||
"zh-CN" -> context.getString(R.string.zh_CN)
|
||||
"zh-TW" -> context.getString(R.string.zh_TW)
|
||||
else ->
|
||||
loc.getDisplayLanguage(loc).replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(loc) else it.toString()
|
||||
|
||||
return languageTags.associateWith { languageTag ->
|
||||
when (languageTag) {
|
||||
SYSTEM_DEFAULT -> getString(R.string.preferences_system_default)
|
||||
"fr-HT" -> getString(R.string.fr_HT)
|
||||
"pt-BR" -> getString(R.string.pt_BR)
|
||||
"zh-CN" -> getString(R.string.zh_CN)
|
||||
"zh-TW" -> getString(R.string.zh_TW)
|
||||
else -> {
|
||||
Locale.forLanguageTag(languageTag).let { locale ->
|
||||
locale.getDisplayLanguage(locale).replaceFirstChar { char ->
|
||||
if (char.isLowerCase()) char.titlecase(locale) else char.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user