Use platform LocaleManager if possible

This commit is contained in:
topjohnwu
2024-07-10 21:50:54 -07:00
parent 6b81716440
commit 7173693d1b
15 changed files with 294 additions and 162 deletions

View File

@@ -2,6 +2,7 @@ package com.topjohnwu.magisk.core
import android.app.Activity
import android.app.Application
import android.app.LocaleManager
import android.content.ComponentCallbacks2
import android.content.Context
import android.content.ContextWrapper
@@ -13,12 +14,11 @@ import android.system.Os
import androidx.profileinstaller.ProfileInstaller
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.base.UntrackedActivity
import com.topjohnwu.magisk.core.utils.LocaleSetting
import com.topjohnwu.magisk.core.utils.NetworkObserver
import com.topjohnwu.magisk.core.utils.ProcessLifecycle
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.core.utils.ShellInit
import com.topjohnwu.magisk.core.utils.refreshLocale
import com.topjohnwu.magisk.core.utils.setConfig
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler
@@ -31,6 +31,9 @@ import timber.log.Timber
import java.lang.ref.WeakReference
import kotlin.system.exitProcess
lateinit var AppApkPath: String
private set
object AppContext : ContextWrapper(null),
Application.ActivityLifecycleCallbacks, ComponentCallbacks2 {
@@ -52,7 +55,7 @@ object AppContext : ContextWrapper(null),
}
override fun onConfigurationChanged(newConfig: Configuration) {
resources.setConfig(newConfig)
LocaleSetting.instance.updateResource(resources)
}
override fun onActivityResumed(activity: Activity) {
@@ -79,7 +82,6 @@ object AppContext : ContextWrapper(null),
} else {
base.packageResourcePath
}
refreshLocale()
resources.patch()
val shellBuilder = Shell.Builder.create()
@@ -97,6 +99,11 @@ object AppContext : ContextWrapper(null),
// Pre-heat the shell ASAP
Shell.getShell(null) {}
if (SDK_INT >= 34 && isRunningAsStub) {
// Send over the locale config manually
val lm = getSystemService(LocaleManager::class.java)
lm.overrideLocaleConfig = LocaleSetting.localeConfig
}
Notifications.setup()
ProcessLifecycle.init(this)
NetworkObserver.init(this)

View File

@@ -6,7 +6,7 @@ import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.repository.DBConfig
import com.topjohnwu.magisk.core.repository.PreferenceConfig
import com.topjohnwu.magisk.core.utils.refreshLocale
import com.topjohnwu.magisk.core.utils.LocaleSetting
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.runBlocking
@@ -135,7 +135,7 @@ object Config : PreferenceConfig, DBConfig {
get() = localePrefs
set(value) {
localePrefs = value
refreshLocale()
LocaleSetting.instance.setLocale(value)
}
var zygisk by dbSettings(Key.ZYGISK, false)

View File

@@ -6,22 +6,18 @@ import android.content.ComponentName
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.res.AssetManager
import android.content.res.Configuration
import android.content.res.Resources
import android.util.DisplayMetrics
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.ktx.unwrap
import com.topjohnwu.magisk.core.utils.syncLocale
lateinit var AppApkPath: String
import com.topjohnwu.magisk.core.utils.LocaleSetting
fun Resources.addAssetPath(path: String) = StubApk.addAssetPath(this, path)
fun Resources.patch(): Resources {
if (isRunningAsStub)
addAssetPath(AppApkPath)
syncLocale()
LocaleSetting.instance.updateResource(this)
return this
}
@@ -40,16 +36,6 @@ fun Context.wrap(): Context {
}
}
fun createNewResources(): Resources {
val asset = AssetManager::class.java.newInstance()
val config = Configuration(AppContext.resources.configuration)
val metrics = DisplayMetrics()
metrics.setTo(AppContext.resources.displayMetrics)
val res = Resources(asset, metrics, config)
res.addAssetPath(AppApkPath)
return res
}
fun Class<*>.cmp(pkg: String) =
ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)

View File

@@ -6,7 +6,7 @@ import com.topjohnwu.magisk.ProviderInstaller
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.core.utils.LocaleSetting
import okhttp3.Cache
import okhttp3.ConnectionSpec
import okhttp3.Dns
@@ -68,7 +68,7 @@ fun createOkHttpClient(context: Context): OkHttpClient {
builder.addInterceptor { chain ->
val request = chain.request().newBuilder()
request.header("User-Agent", "Magisk/${BuildConfig.APP_VERSION_CODE}")
request.header("Accept-Language", currentLocale.toLanguageTag())
request.header("Accept-Language", LocaleSetting.instance.currentLocale.toLanguageTag())
chain.proceed(request.build())
}

View File

@@ -6,7 +6,6 @@ import android.content.*
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.AdaptiveIconDrawable
@@ -19,8 +18,8 @@ import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.core.content.getSystemService
import com.topjohnwu.magisk.core.utils.LocaleSetting
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.superuser.internal.UiThreadHandler
import java.io.File
@@ -54,9 +53,7 @@ fun ApplicationInfo.getLabel(pm: PackageManager): String {
runCatching {
if (labelRes > 0) {
val res = pm.getResourcesForApplication(this)
val config = Configuration()
config.setLocale(currentLocale)
res.updateConfiguration(config, res.displayMetrics)
LocaleSetting.instance.updateResource(res)
return res.getString(labelRes)
}
}

View File

@@ -1,7 +1,6 @@
package com.topjohnwu.magisk.core.ktx
import androidx.collection.SparseArrayCompat
import com.topjohnwu.magisk.core.utils.currentLocale
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@@ -17,6 +16,7 @@ import java.lang.reflect.Field
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Collections
import java.util.Locale
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
@@ -98,13 +98,13 @@ fun Long.toTime(format: DateFormat) = format.format(this).orEmpty()
val timeFormatStandard by lazy {
SimpleDateFormat(
"yyyy-MM-dd'T'HH.mm.ss",
currentLocale
Locale.ROOT
)
}
val timeDateFormat: DateFormat by lazy {
DateFormat.getDateTimeInstance(
DateFormat.DEFAULT,
DateFormat.DEFAULT,
currentLocale
Locale.ROOT
)
}

View File

@@ -0,0 +1,183 @@
package com.topjohnwu.magisk.core.utils
import android.annotation.SuppressLint
import android.app.LocaleConfig
import android.app.LocaleManager
import android.content.ContextWrapper
import android.content.res.Resources
import android.os.Build
import android.os.LocaleList
import androidx.annotation.RequiresApi
import com.topjohnwu.magisk.core.AppApkPath
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.base.relaunch
import com.topjohnwu.magisk.core.isRunningAsStub
import org.xmlpull.v1.XmlPullParser
import java.util.Locale
interface LocaleSetting {
// The locale that is manually overridden, null if system default
val appLocale: Locale?
// The current active locale used in the application
val currentLocale: Locale
fun setLocale(tag: String)
fun updateResource(res: Resources)
private class Api23Impl : LocaleSetting {
private val systemLocale: Locale = Locale.getDefault()
override var currentLocale: Locale = systemLocale
override var appLocale: Locale? = null
init {
setLocale(Config.locale)
}
override fun setLocale(tag: String) {
val locale = when {
tag.isEmpty() -> null
else -> Locale.forLanguageTag(tag)
}
currentLocale = locale ?: systemLocale
appLocale = locale
Locale.setDefault(currentLocale)
updateResource(AppContext.resources)
AppContext.foregroundActivity?.relaunch()
}
@Suppress("DEPRECATION")
override fun updateResource(res: Resources) {
val config = res.configuration
config.setLocale(currentLocale)
res.updateConfiguration(config, null)
}
}
@RequiresApi(24)
private class Api24Impl : LocaleSetting {
private val systemLocaleList = LocaleList.getDefault()
private var currentLocaleList: LocaleList = systemLocaleList
override var appLocale: Locale? = null
override val currentLocale: Locale get() = currentLocaleList[0]
init {
setLocale(Config.locale)
}
override fun setLocale(tag: String) {
val localeList = when {
tag.isEmpty() -> null
else -> LocaleList.forLanguageTags(tag)
}
currentLocaleList = localeList ?: systemLocaleList
appLocale = localeList?.get(0)
LocaleList.setDefault(currentLocaleList)
updateResource(AppContext.resources)
AppContext.foregroundActivity?.relaunch()
}
@Suppress("DEPRECATION")
override fun updateResource(res: Resources) {
val config = res.configuration
config.setLocales(currentLocaleList)
res.updateConfiguration(config, null)
}
}
@RequiresApi(33)
private class Api33Impl : LocaleSetting {
private val lm: LocaleManager = AppContext.getSystemService(LocaleManager::class.java)
override val appLocale: Locale?
get() = lm.applicationLocales.let { if (it.isEmpty) null else it[0] }
override val currentLocale: Locale
get() = appLocale ?: lm.systemLocales[0]
// These following methods should not be used
override fun setLocale(tag: String) {}
override fun updateResource(res: Resources) {}
}
class AppLocaleList(
val names: Array<String>,
val tags: Array<String>
)
@SuppressLint("NewApi")
companion object {
val available: AppLocaleList by lazy {
val names = ArrayList<String>()
val tags = ArrayList<String>()
names.add(AppContext.getString(R.string.system_default))
tags.add("")
if (Build.VERSION.SDK_INT >= 34) {
// Use platform LocaleConfig parser
val config = localeConfig
val list = config.supportedLocales ?: LocaleList.getEmptyLocaleList()
names.ensureCapacity(list.size() + 1)
tags.ensureCapacity(list.size() + 1)
for (i in 0 until list.size()) {
val locale = list[i]
names.add(locale.getDisplayName(locale))
tags.add(locale.toLanguageTag())
}
} else {
// Manually parse locale_config.xml
val parser = AppContext.resources.getXml(R.xml.locale_config)
while (true) {
when (parser.next()) {
XmlPullParser.START_TAG -> {
if (parser.name == "locale") {
val tag = parser.getAttributeValue(0)
val locale = Locale.forLanguageTag(tag)
names.add(locale.getDisplayName(locale))
tags.add(tag)
}
}
XmlPullParser.END_DOCUMENT -> break
}
}
}
AppLocaleList(names.toTypedArray(), tags.toTypedArray())
}
@get:RequiresApi(34)
val localeConfig: LocaleConfig by lazy {
val context = if (isRunningAsStub) {
val pkgInfo = AppContext.packageManager.getPackageArchiveInfo(AppApkPath, 0)!!
object : ContextWrapper(AppContext) {
override fun getApplicationInfo() = pkgInfo.applicationInfo
}
} else {
AppContext
}
LocaleConfig.fromContextIgnoringOverride(context)
}
val useLocaleManager get() =
if (isRunningAsStub) Build.VERSION.SDK_INT >= 34
else Build.VERSION.SDK_INT >= 33
val instance: LocaleSetting by lazy {
// Initialize available locale list
available
if (useLocaleManager) {
Api33Impl()
} else if (Build.VERSION.SDK_INT <= 23) {
Api23Impl()
} else {
Api24Impl()
}
}
}
}

View File

@@ -1,88 +0,0 @@
@file:Suppress("DEPRECATION")
package com.topjohnwu.magisk.core.utils
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.content.res.Resources
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.base.relaunch
import com.topjohnwu.magisk.core.createNewResources
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.Locale
var currentLocale: Locale = Locale.getDefault()
@SuppressLint("ConstantLocale")
val defaultLocale: Locale = Locale.getDefault()
private var cachedLocales: Pair<Array<String>, Array<String>>? = null
suspend fun availableLocales() = cachedLocales ?:
withContext(Dispatchers.Default) {
val compareId = R.string.app_changelog
// Create a completely new resource to prevent cross talk over active configs
val res = createNewResources()
fun changeLocale(locale: Locale) {
res.configuration.setLocale(locale)
res.updateConfiguration(res.configuration, res.displayMetrics)
}
val locales = ArrayList<String>().apply {
// Add default locale
add("en")
// Add some special locales
add("zh-TW")
add("pt-BR")
// Then add all supported locales
addAll(Resources.getSystem().assets.locales)
}.map {
Locale.forLanguageTag(it)
}.distinctBy {
changeLocale(it)
res.getString(compareId)
}.sortedWith { a, b ->
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
}
changeLocale(defaultLocale)
val defName = res.getString(R.string.system_default)
val names = ArrayList<String>(locales.size + 1)
val values = ArrayList<String>(locales.size + 1)
names.add(defName)
values.add("")
locales.forEach { locale ->
names.add(locale.getDisplayName(locale))
values.add(locale.toLanguageTag())
}
(names.toTypedArray() to values.toTypedArray()).also { cachedLocales = it }
}
fun Resources.setConfig(config: Configuration) {
config.setLocale(currentLocale)
updateConfiguration(config, null)
}
fun Resources.syncLocale() = setConfig(configuration)
fun refreshLocale() {
val localeConfig = Config.locale
currentLocale = when {
localeConfig.isEmpty() -> defaultLocale
else -> Locale.forLanguageTag(localeConfig)
}
Locale.setDefault(currentLocale)
AppContext.resources.syncLocale()
AppContext.foregroundActivity?.relaunch()
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="ar" />
<locale android:name="ast" />
<locale android:name="az" />
<locale android:name="be" />
<locale android:name="bg" />
<locale android:name="bn" />
<locale android:name="ca" />
<locale android:name="cs" />
<locale android:name="de" />
<locale android:name="el" />
<locale android:name="en" />
<locale android:name="es" />
<locale android:name="et" />
<locale android:name="fa" />
<locale android:name="fr" />
<locale android:name="hi" />
<locale android:name="hr" />
<locale android:name="hu" />
<locale android:name="in" />
<locale android:name="it" />
<locale android:name="iw" />
<locale android:name="ja" />
<locale android:name="ka" />
<locale android:name="kk" />
<locale android:name="ko" />
<locale android:name="lt" />
<locale android:name="mk" />
<locale android:name="ml" />
<locale android:name="nb" />
<locale android:name="nl" />
<locale android:name="pa" />
<locale android:name="pl" />
<locale android:name="pt-BR" />
<locale android:name="pt-PT" />
<locale android:name="ro" />
<locale android:name="ru" />
<locale android:name="sk" />
<locale android:name="sq" />
<locale android:name="sr" />
<locale android:name="sv" />
<locale android:name="sw" />
<locale android:name="ta" />
<locale android:name="th" />
<locale android:name="tr" />
<locale android:name="uk" />
<locale android:name="vi" />
<locale android:name="zh-CN" />
<locale android:name="zh-TW" />
</locale-config>