diff --git a/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt b/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt index 8ad5f32..da5d181 100644 --- a/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt @@ -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 diff --git a/app/src/main/java/com/rainy/token/ui/widget/OpenCodeGoWidgetProvider.kt b/app/src/main/java/com/rainy/token/ui/widget/OpenCodeGoWidgetProvider.kt index 99d974d..83932a5 100644 --- a/app/src/main/java/com/rainy/token/ui/widget/OpenCodeGoWidgetProvider.kt +++ b/app/src/main/java/com/rainy/token/ui/widget/OpenCodeGoWidgetProvider.kt @@ -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) } } diff --git a/app/src/main/java/com/rainy/token/util/LocaleManager.kt b/app/src/main/java/com/rainy/token/util/LocaleManager.kt index 1152bc3..1d6258b 100644 --- a/app/src/main/java/com/rainy/token/util/LocaleManager.kt +++ b/app/src/main/java/com/rainy/token/util/LocaleManager.kt @@ -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)