Launch system language picker for API 33+ (#3145)

This commit is contained in:
Phil Oliver
2025-09-19 08:16:45 -04:00
committed by GitHub
parent 0d2c1f1516
commit e604942beb
4 changed files with 61 additions and 48 deletions

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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,
)
}

View File

@@ -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()
}
}
}
}
}
}