fix: 与系统应用语言页同步(framework 唯一权威源)+ widget 文本跟随最新偏好(Codex review + 审计)
This commit is contained in:
parent
f463751fb1
commit
2e4e5ce449
@ -1,6 +1,7 @@
|
||||
package com.rainy.token.ui.settings
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@ -230,8 +231,11 @@ private fun LanguageDialog(onDismiss: () -> Unit) {
|
||||
.clickable {
|
||||
LocaleManager.saveLocale(context, code)
|
||||
onDismiss()
|
||||
// 重建 Activity 让资源按新语言重新解析
|
||||
(context as? Activity)?.recreate()
|
||||
// Android 13+:framework 设置语言后系统自动重建 Activity;
|
||||
// 旧系统才需要手动 recreate 让资源按新语言重新解析
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
(context as? Activity)?.recreate()
|
||||
}
|
||||
}
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
||||
@ -15,6 +15,7 @@ import com.rainy.token.data.cache.BalanceCache
|
||||
import com.rainy.token.data.cache.balanceCacheDataStore
|
||||
import com.rainy.token.domain.service.ServiceType
|
||||
import com.rainy.token.ui.components.normalizeWindowLabel
|
||||
import com.rainy.token.util.LocaleManager
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
@ -57,15 +58,19 @@ class OpenCodeGoWidgetProvider : AppWidgetProvider() {
|
||||
) {
|
||||
var selectedHasCachedData = false
|
||||
|
||||
// 每次渲染都用最新语言偏好重新包装 context:Application 层的 wrap 在进程启动时固定,
|
||||
// 应用内切换语言后不会自动更新,必须在这里按当前偏好重建,否则 widget 文本停留在旧语言
|
||||
val localized = LocaleManager.wrapContext(context)
|
||||
|
||||
for (widgetId in appWidgetIds) {
|
||||
val views = RemoteViews(context.packageName, R.layout.widget_opencode_go)
|
||||
|
||||
// 署名动态设置:与行标签同源(跟随应用内语言),
|
||||
// 避免 XML 静态文本由宿主进程按系统语言渲染造成中英混搭
|
||||
views.setTextViewText(R.id.widget_brand, context.getString(R.string.widget_brand))
|
||||
views.setTextViewText(R.id.widget_brand, localized.getString(R.string.widget_brand))
|
||||
|
||||
// DeepSeek 余额标签同样动态设置(静态 XML 文本由 Launcher 按系统语言解析)
|
||||
views.setTextViewText(R.id.widget_ds_label, context.getString(R.string.widget_deepseek_balance))
|
||||
views.setTextViewText(R.id.widget_ds_label, localized.getString(R.string.widget_deepseek_balance))
|
||||
|
||||
// 点击左上角品牌 → 打开 APP;点击其它内容区域 → 切换服务;刷新按钮单独刷新。
|
||||
val openAppIntent = Intent(context, MainActivity::class.java).apply {
|
||||
@ -102,7 +107,7 @@ class OpenCodeGoWidgetProvider : AppWidgetProvider() {
|
||||
views.setTextViewText(R.id.widget_switch, shortName(selectedService))
|
||||
views.setTextViewText(
|
||||
R.id.widget_service_title,
|
||||
context.getString(R.string.widget_service_quota, selectedService.displayName)
|
||||
localized.getString(R.string.widget_service_quota, selectedService.displayName)
|
||||
)
|
||||
views.setImageViewResource(R.id.widget_logo, widgetLogo(selectedService))
|
||||
// Ollama logo 是正方形,XML 默认 22x12 是给宽扁 logo 的
|
||||
@ -116,20 +121,20 @@ class OpenCodeGoWidgetProvider : AppWidgetProvider() {
|
||||
val cached = cache.get(selectedService)
|
||||
if (cached != null) {
|
||||
selectedHasCachedData = true
|
||||
populateServiceRows(views, context, selectedService, cached.balance)
|
||||
populateServiceRows(views, localized, selectedService, cached.balance)
|
||||
|
||||
val sdf = SimpleDateFormat("MM-dd HH:mm", Locale.getDefault())
|
||||
val timeText = sdf.format(Date(cached.fetchedAt))
|
||||
views.setTextViewText(
|
||||
R.id.widget_updated,
|
||||
context.getString(R.string.widget_updated_at, timeText)
|
||||
localized.getString(R.string.widget_updated_at, timeText)
|
||||
)
|
||||
} else {
|
||||
setEmptyState(views, context)
|
||||
setEmptyState(views, localized)
|
||||
views.setTextViewText(R.id.widget_switch, shortName(selectedService))
|
||||
views.setTextViewText(
|
||||
R.id.widget_service_title,
|
||||
context.getString(R.string.widget_service_quota, selectedService.displayName)
|
||||
localized.getString(R.string.widget_service_quota, selectedService.displayName)
|
||||
)
|
||||
}
|
||||
|
||||
@ -143,7 +148,7 @@ class OpenCodeGoWidgetProvider : AppWidgetProvider() {
|
||||
views.setTextViewText(R.id.widget_ds_amount, "—")
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
setEmptyState(views, context)
|
||||
setEmptyState(views, localized)
|
||||
}
|
||||
}
|
||||
|
||||
@ -378,12 +383,14 @@ class OpenCodeGoWidgetProvider : AppWidgetProvider() {
|
||||
}
|
||||
|
||||
fun showRefreshing(context: Context) {
|
||||
// 与 onUpdate 同理:用最新语言偏好包装,避免进程启动后切换语言不生效
|
||||
val localized = LocaleManager.wrapContext(context)
|
||||
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||
val component = ComponentName(context, OpenCodeGoWidgetProvider::class.java)
|
||||
val ids = appWidgetManager.getAppWidgetIds(component)
|
||||
ids.forEach { id ->
|
||||
val views = RemoteViews(context.packageName, R.layout.widget_opencode_go)
|
||||
views.setTextViewText(R.id.widget_updated, context.getString(R.string.widget_refreshing))
|
||||
views.setTextViewText(R.id.widget_updated, localized.getString(R.string.widget_refreshing))
|
||||
appWidgetManager.partiallyUpdateAppWidget(id, views)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ package com.rainy.token.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.LocaleList
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
@ -19,21 +21,62 @@ import java.util.Locale
|
||||
*
|
||||
* 在 [Application] 与 [MainActivity] 的 attachBaseContext 中调用 [wrapContext]
|
||||
* 应用偏好;切换语言后由设置页保存偏好并 recreate Activity 触发重新解析。
|
||||
*
|
||||
* Android 13+(TIRAMISU)与系统"应用语言"页同步:读写统一走 framework 的
|
||||
* [android.app.LocaleManager],私有 SharedPreferences 仅在 API < 33 或
|
||||
* framework 未设置时作为兜底——避免应用内偏好与系统级设置互相覆盖造成双源冲突。
|
||||
*/
|
||||
object LocaleManager {
|
||||
|
||||
private const val PREFS_NAME = "locale_prefs"
|
||||
private const val KEY_LOCALE = "locale"
|
||||
|
||||
/** 当前保存的语言代码;null = 跟随系统 */
|
||||
/** 当前生效的语言代码;null = 跟随系统 */
|
||||
fun getLocaleCode(context: Context): String? {
|
||||
// Android 13+:framework 是唯一权威源(系统"应用语言"页与应用内选择共用)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val locales = context.getSystemService(android.app.LocaleManager::class.java)?.applicationLocales
|
||||
if (locales != null && !locales.isEmpty) {
|
||||
for (i in 0 until locales.size()) {
|
||||
val l = locales.get(i)
|
||||
when {
|
||||
l.language == "zh" && l.script != "Hant" -> return "zh"
|
||||
l.language == "zh" && l.script == "Hant" -> return "zh-Hant"
|
||||
l.language == "en" -> return "en"
|
||||
}
|
||||
}
|
||||
return null // 系统选了不支持的第三方语言 → 回退默认英文资源
|
||||
}
|
||||
// framework 未设置(跟随系统):一次性迁移旧 SharedPreferences 偏好到
|
||||
// framework 后清除,避免升级设备上过期偏好复活(framework 才是唯一权威源)
|
||||
migrateLegacyPrefs(context)
|
||||
return null
|
||||
}
|
||||
// API < 33:回退私有偏好
|
||||
val code = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.getString(KEY_LOCALE, null) ?: return null
|
||||
return code.takeIf { it == "zh" || it == "zh-Hant" || it == "en" }
|
||||
}
|
||||
|
||||
/** 把 API < 33 时代写入的私有偏好一次性迁移到 framework 并清除 */
|
||||
private fun migrateLegacyPrefs(context: Context) {
|
||||
// 方法体内守卫:lint 据此识别 API33 调用(@RequiresApi 注解在本环境解析异常)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val code = prefs.getString(KEY_LOCALE, null) ?: return
|
||||
context.getSystemService(android.app.LocaleManager::class.java)
|
||||
?.setApplicationLocales(LocaleList.forLanguageTags(code))
|
||||
prefs.edit().remove(KEY_LOCALE).apply()
|
||||
}
|
||||
|
||||
/** 保存语言偏好;传 null 表示跟随系统 */
|
||||
fun saveLocale(context: Context, code: String?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// Android 13+:写入系统"应用语言",framework 生效并自动重建 Activity
|
||||
context.getSystemService(android.app.LocaleManager::class.java)
|
||||
?.setApplicationLocales(LocaleList.forLanguageTags(code ?: ""))
|
||||
return
|
||||
}
|
||||
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString(KEY_LOCALE, code)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user