Update and fix in-app language picker (#3080)

* Manage app language using AndroidX, integrate with Android 13

 - Manage the app language using AndroidX to fix the setting not being applied and to be able to easily integrate with Android 13's system setting

* Move locales_config.xml to common

* Generate locales_config.xml when downloading translations

* Fix multiple variants for languages being collapsed into one

* Fix language codes with region variants

* Don't split languages when using app bundles

* Rename
This commit is contained in:
Joris Pelgröm 2022-11-22 19:59:24 +01:00 committed by GitHub
parent 5a38f95482
commit c50ce30b5b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 118 additions and 158 deletions

View file

@ -35,4 +35,18 @@ runs:
echo "Download Complete, unzipping"
unzip -n strings.zip
echo "Unzipped strings, generating locales_config.xml"
XML_START='<?xml version="1.0" encoding="utf-8"?>\n<locale-config xmlns:android="http://schemas.android.com/apk/res/android">\n'
XML_LOCALES=''
XML_END='</locale-config>'
for i in common/src/main/res/values*/strings.xml; do
FOLDER="$(basename $(dirname $i))"
CODE="${FOLDER#*-}" # remove "values-"
CODE="${CODE/-r/-}" # replace region "-rXX" with "-XX"
if [ "$CODE" == "values" ]; then CODE="en"; fi
XML_LOCALES="$XML_LOCALES <locale android:name=\"$CODE\"/>\n"
done
printf "$XML_START$XML_LOCALES$XML_END" > common/src/main/res/xml/locales_config.xml
echo "Complete"

View file

@ -29,6 +29,12 @@ android {
versionCode = System.getenv("VERSION_CODE")?.toIntOrNull() ?: 1
manifestPlaceholders["sentryRelease"] = "$applicationId@$versionName"
bundle {
language {
enableSplit = false
}
}
}
buildFeatures {
@ -149,7 +155,7 @@ dependencies {
implementation("com.google.dagger:hilt-android:2.44.2")
kapt("com.google.dagger:hilt-android-compiler:2.44.2")
implementation("androidx.appcompat:appcompat:1.5.1")
implementation("androidx.appcompat:appcompat:1.6.0-rc01")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.2.1")

View file

@ -68,7 +68,9 @@
android:theme="@style/Theme.HomeAssistant"
android:networkSecurityConfig="@xml/network_security_config"
android:enableOnBackInvokedCallback="true"
tools:ignore="GoogleAppIndexingWarning">
android:localeConfig="@xml/locales_config"
tools:ignore="GoogleAppIndexingWarning"
tools:targetApi="tiramisu">
<!-- Start things like SensorWorker on device boot -->
<receiver android:name=".websocket.WebsocketBroadcastReceiver"
@ -534,6 +536,15 @@
</intent-filter>
</service>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
<receiver
android:name=".notifications.NotificationActionReceiver"
android:enabled="true"

View file

@ -2,12 +2,4 @@ package io.homeassistant.companion.android
import androidx.appcompat.app.AppCompatActivity
open class BaseActivity : AppCompatActivity() {
//
// @Inject
// lateinit var lm: LanguagesManager
//
// override fun attachBaseContext(newBase: Context) {
// super.attachBaseContext(lm.getContextWrapper(newBase))
// }
}
open class BaseActivity : AppCompatActivity()

View file

@ -17,6 +17,7 @@ import io.homeassistant.companion.android.common.sensors.LastUpdateManager
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.settings.SensorUpdateFrequencySetting
import io.homeassistant.companion.android.sensors.SensorReceiver
import io.homeassistant.companion.android.settings.language.LanguagesManager
import io.homeassistant.companion.android.websocket.WebsocketBroadcastReceiver
import io.homeassistant.companion.android.widgets.button.ButtonWidget
import io.homeassistant.companion.android.widgets.entity.EntityWidget
@ -39,6 +40,9 @@ open class HomeAssistantApplication : Application() {
@Inject
lateinit var keyChainRepository: KeyChainRepository
@Inject
lateinit var languagesManager: LanguagesManager
override fun onCreate() {
super.onCreate()
@ -49,6 +53,8 @@ open class HomeAssistantApplication : Application() {
)
}
languagesManager.applyCurrentLang()
// This will make sure we start/stop when we actually need too.
registerReceiver(
WebsocketBroadcastReceiver(),

View file

@ -436,10 +436,6 @@ class SettingsFragment constructor(
}
}
override fun onLangSettingsChanged() {
requireActivity().recreate()
}
private fun onDisplaySsidScreen() {
val permissionsToCheck: Array<String> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION)

View file

@ -106,10 +106,7 @@ class SettingsPresenterImpl @Inject constructor(
}
}
"themes" -> themesManager.saveTheme(value)
"languages" -> {
langsManager.saveLang(value)
settingsView.onLangSettingsChanged()
}
"languages" -> langsManager.saveLang(value)
else -> throw IllegalArgumentException("No string found by this key: $key")
}
}

View file

@ -9,6 +9,4 @@ interface SettingsView {
fun updateExternalUrl(url: String, useCloud: Boolean)
fun updateSsids(ssids: Set<String>)
fun onLangSettingsChanged()
}

View file

@ -1,42 +1,38 @@
package io.homeassistant.companion.android.settings.language
import android.content.Context
import android.content.ContextWrapper
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.os.LocaleList
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
import kotlinx.coroutines.runBlocking
import java.util.Locale
import org.xmlpull.v1.XmlPullParser
import javax.inject.Inject
import io.homeassistant.companion.android.common.R as commonR
class LanguagesManager @Inject constructor(
private var prefs: PrefsRepository
) {
companion object {
private const val DEF_LOCALE = "default"
}
private const val TAG = "LanguagesManager"
fun getAppVersion(): String? {
return runBlocking {
prefs.getAppVersion()
}
}
fun saveAppVersion(ver: String) {
return runBlocking {
prefs.saveAppVersion(ver)
}
const val DEF_LOCALE = "default"
private const val SYSTEM_MANAGES_LOCALE = "system_managed"
}
fun getCurrentLang(): String {
return runBlocking {
val lang = prefs.getCurrentLang()
if (lang.isNullOrEmpty()) {
prefs.saveLang(DEF_LOCALE)
DEF_LOCALE
} else lang
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
migrateLangSetting()
AppCompatDelegate.getApplicationLocales().toLanguageTags().ifEmpty { DEF_LOCALE }
} else {
if (lang.isNullOrEmpty()) {
prefs.saveLang(DEF_LOCALE)
DEF_LOCALE
} else lang
}
}
}
@ -44,81 +40,62 @@ class LanguagesManager @Inject constructor(
return runBlocking {
if (!lang.isNullOrEmpty()) {
val currentLang = getCurrentLang()
if (currentLang != lang) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val languages =
if (lang == DEF_LOCALE) LocaleListCompat.getEmptyLocaleList()
else LocaleListCompat.forLanguageTags(lang)
AppCompatDelegate.setApplicationLocales(languages) // Applying will also save it
} else if (currentLang != lang) {
prefs.saveLang(lang)
applyCurrentLang()
}
}
}
}
fun getLocales(): String? {
fun applyCurrentLang() = runBlocking {
migrateLangSetting()
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
val lang = getCurrentLang()
val languages =
if (lang == DEF_LOCALE) LocaleListCompat.getEmptyLocaleList()
else LocaleListCompat.forLanguageTags(lang)
AppCompatDelegate.setApplicationLocales(languages)
} // else on Android 13+ the system will manage the app's language
}
private suspend fun migrateLangSetting() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
val lang = prefs.getCurrentLang()
if (lang == SYSTEM_MANAGES_LOCALE) return
// First run on Android 13: save in AndroidX, update app preference
val languages =
if (lang == DEF_LOCALE) LocaleListCompat.getEmptyLocaleList()
else LocaleListCompat.forLanguageTags(lang)
AppCompatDelegate.setApplicationLocales(languages)
prefs.saveLang(SYSTEM_MANAGES_LOCALE)
}
fun getLocaleTags(context: Context): List<String> {
return runBlocking {
prefs.getLocales()
}
}
fun saveLocales(locales: String) {
return runBlocking {
prefs.saveLocales(locales)
}
}
private fun makeLocale(lang: String): Locale {
return if (lang.contains('-')) {
Locale(lang.split('-')[0], lang.split('-')[1])
} else {
Locale(lang)
}
}
private fun getDefaultLocale(config: Configuration): Locale {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
config.locales.get(0)
} else {
config.locale
}
}
private fun getDeviceLocale(): Locale {
return getDefaultLocale(Resources.getSystem().configuration)
}
private fun getApplicationLocale(context: Context): Locale {
return getDefaultLocale(context.resources.configuration)
}
fun getContextWrapper(context: Context): ContextWrapper {
return when {
getCurrentLang() == DEF_LOCALE && getApplicationLocale(context) != getDeviceLocale() -> {
ContextWrapper(updateContext(context, getDeviceLocale()))
val languagesList = mutableListOf<String>()
try {
context.resources.getXml(commonR.xml.locales_config).use {
var tagType = it.eventType
while (tagType != XmlPullParser.END_DOCUMENT) {
if (tagType == XmlPullParser.START_TAG && it.name == "locale") {
languagesList += it.getAttributeValue("http://schemas.android.com/apk/res/android", "name")
}
tagType = it.next()
}
}
} catch (e: Exception) {
Log.e(TAG, "Exception while parsing locale config XML", e)
}
getCurrentLang() != DEF_LOCALE -> {
val locale = makeLocale(getCurrentLang())
ContextWrapper(updateContext(context, locale))
}
else -> {
ContextWrapper(context)
}
}
}
private fun updateContext(context: Context, locale: Locale): Context {
val resources: Resources = context.resources
val configuration: Configuration = resources.configuration
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val localeList = LocaleList(locale)
LocaleList.setDefault(localeList)
configuration.setLocales(localeList)
} else {
configuration.locale = locale
languagesList
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
context.createConfigurationContext(configuration)
} else {
resources.updateConfiguration(configuration, resources.displayMetrics)
}
return context
}
}

View file

@ -1,11 +1,6 @@
package io.homeassistant.companion.android.settings.language
import android.content.Context
import android.content.res.Resources
import android.os.Build
import android.os.LocaleList
import android.util.DisplayMetrics
import io.homeassistant.companion.android.BuildConfig
import java.util.Locale
import javax.inject.Inject
import io.homeassistant.companion.android.common.R as commonR
@ -18,52 +13,21 @@ class LanguagesProvider @Inject constructor(
val listAppLocales = sortedMapOf<String, String>()
val resources = context.resources
if (langManager.getAppVersion() != BuildConfig.VERSION_NAME || langManager.getLocales().isNullOrEmpty()) {
val listLocales = resources.assets.locales
val defString = getStringResource(context, "")
var supportedLocales = ""
listLocales.forEach {
if (getStringResource(context, it) != defString || it == Locale.ENGLISH.language) {
val name = makeLocale(it).displayLanguage.capitalize()
listAppLocales["$name ($it)"] = it
supportedLocales += "$it,"
}
}
langManager.saveAppVersion(BuildConfig.VERSION_NAME)
langManager.saveLocales(supportedLocales)
} else {
val listLocales = langManager.getLocales()!!.split(',')
listLocales.forEach {
if (it.isNotEmpty()) {
val name = makeLocale(it).displayLanguage.capitalize()
listAppLocales["$name ($it)"] = it
}
val locales = langManager.getLocaleTags(context)
locales.forEach {
val locale = makeLocale(it)
var display = locale.getDisplayLanguage(locale).replaceFirstChar { char ->
if (char.isLowerCase()) char.titlecase(locale) else char.toString()
}
if (locale.country.isNotBlank()) display += " (${locale.getDisplayCountry(locale)})"
listAppLocales[display] = it
}
val languages = mutableMapOf(resources.getString(commonR.string.lang_option_label_default) to resources.getString(commonR.string.lang_option_value_default))
val languages = mutableMapOf(resources.getString(commonR.string.lang_option_label_default) to LanguagesManager.DEF_LOCALE)
languages.putAll(listAppLocales)
return languages
}
private fun getStringResource(context: Context, lang: String): String {
val resource = commonR.string.application_version
val resources = context.resources
val configuration = resources.configuration
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
configuration.setLocales(LocaleList(makeLocale(lang)))
val c = context.createConfigurationContext(configuration)
c.getString(resource)
} else {
val metrics = DisplayMetrics()
configuration.locale = makeLocale(lang)
val res = Resources(context.assets, metrics, configuration)
res.getString(resource)
}
}
private fun makeLocale(lang: String): Locale {
return if (lang.contains('-')) {
Locale(lang.split('-')[0], lang.split('-')[1])

View file

@ -85,7 +85,6 @@ import io.homeassistant.companion.android.nfc.WriteNfcTag
import io.homeassistant.companion.android.sensors.SensorReceiver
import io.homeassistant.companion.android.sensors.SensorWorker
import io.homeassistant.companion.android.settings.SettingsActivity
import io.homeassistant.companion.android.settings.language.LanguagesManager
import io.homeassistant.companion.android.themes.ThemesManager
import io.homeassistant.companion.android.util.ChangeLog
import io.homeassistant.companion.android.util.DataUriDownloadManager
@ -164,9 +163,6 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
@Inject
lateinit var changeLog: ChangeLog
@Inject
lateinit var languagesManager: LanguagesManager
@Inject
lateinit var urlRepository: UrlRepository
@ -651,7 +647,6 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
if (presenter.isKeepScreenOnEnabled())
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
currentLang = languagesManager.getCurrentLang()
currentAutoplay = presenter.isAutoPlayVideoEnabled()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -693,7 +688,7 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
override fun onResume() {
super.onResume()
if ((currentLang != languagesManager.getCurrentLang()) || currentAutoplay != presenter.isAutoPlayVideoEnabled())
if (currentAutoplay != presenter.isAutoPlayVideoEnabled())
recreate()
appLocked = presenter.isAppLocked()

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en"/>
</locale-config>