From 3dfedc8822d69201f5b40ae50953228bef3d783b Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 28 Oct 2026 13:34:32 +0100 Subject: [PATCH] Read back locale requests after partial framework failures --- .../com/utun/chat/ui/components/LanguageSetting.kt | 7 ++++--- .../androidx/appcompat/app/AppCompatDelegate.kt | 4 +++- .../kotlin/com/utun/chat/data/AppLanguageTest.kt | 13 ++++++++++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/LanguageSetting.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/LanguageSetting.kt index 7fa4052b..246d37af 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/LanguageSetting.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/LanguageSetting.kt @@ -16,7 +16,7 @@ import com.utun.chat.data.LogManager /** Выбор одного языка каталога либо «Как в системе»; название текущего запроса учитывает приоритетный список Android. * Чтение повторяется при новой Configuration/явной попытке; ошибка не выдаётся за системный выбор. - * Неудачный select оставляет прежний выбор и показывает ошибку, повтор открывает меню без перезапуска служб. + * После select перечитывает фактический запрос Android, включая отказ после частичного применения; ошибка показывается отдельно. * Успех означает принятие запроса AppCompat; фактические ресурсы/пересоздание Activity применяет Android. */ @Composable fun LanguageSetting() { @@ -25,7 +25,8 @@ fun LanguageSetting() { var selectionFailed by remember { mutableStateOf(false) } val configuration = LocalConfiguration.current val state = remember(configuration, retry) { - runCatching { AppLanguage.selectedTag() to AppLanguage.supportedTags } + try { Result.success(AppLanguage.selectedTag() to AppLanguage.supportedTags) + } catch (error: Exception) { Result.failure(error) } } val selection = state.getOrNull() val selected = selection?.first @@ -58,7 +59,7 @@ fun LanguageSetting() { DropdownMenuItem(text = { Text(name) }, onClick = { expanded = false selectionFailed = !AppLanguage.select(tag) - if (!selectionFailed) retry++ + retry++ }) } } diff --git a/tools/chatgui-android/language-tests/src/main/kotlin/androidx/appcompat/app/AppCompatDelegate.kt b/tools/chatgui-android/language-tests/src/main/kotlin/androidx/appcompat/app/AppCompatDelegate.kt index 340be23e..1b162ed8 100644 --- a/tools/chatgui-android/language-tests/src/main/kotlin/androidx/appcompat/app/AppCompatDelegate.kt +++ b/tools/chatgui-android/language-tests/src/main/kotlin/androidx/appcompat/app/AppCompatDelegate.kt @@ -2,11 +2,12 @@ package androidx.appcompat.app import androidx.core.os.LocaleListCompat -/** Host AppCompat: при отказе setApplicationLocales сохранённый выбор не меняется. */ +/** Host AppCompat: отказ до/после публикации requested моделируется отдельно; завершение конфигурации не имитируется. */ object AppCompatDelegate { var requested = LocaleListCompat.forLanguageTags("") var failRead = false var failWrite = false + var failWriteAfter = false val selections = mutableListOf() fun getApplicationLocales(): LocaleListCompat { check(!failRead) { "application locales read failed" } @@ -15,5 +16,6 @@ object AppCompatDelegate { fun setApplicationLocales(locales: LocaleListCompat) { check(!failWrite) { "application locales write failed" } selections.add(locales.toLanguageTags()); requested = locales + check(!failWriteAfter) { "application locales write failed after effect" } } } diff --git a/tools/chatgui-android/language-tests/src/test/kotlin/com/utun/chat/data/AppLanguageTest.kt b/tools/chatgui-android/language-tests/src/test/kotlin/com/utun/chat/data/AppLanguageTest.kt index 52d955d7..de07c7eb 100644 --- a/tools/chatgui-android/language-tests/src/test/kotlin/com/utun/chat/data/AppLanguageTest.kt +++ b/tools/chatgui-android/language-tests/src/test/kotlin/com/utun/chat/data/AppLanguageTest.kt @@ -25,6 +25,7 @@ class AppLanguageTest { ContextCompat.failContext = false AppCompatDelegate.requested = LocaleListCompat.forLanguageTags("") AppCompatDelegate.failRead = false; AppCompatDelegate.failWrite = false; AppCompatDelegate.selections.clear() + AppCompatDelegate.failWriteAfter = false HostLanguageNotifications.attempts.clear(); HostLanguageNotifications.failures.clear() LogManager.entries.clear() } @@ -86,7 +87,7 @@ class AppLanguageTest { error("Unsupported language") } - @Test fun failedFrameworkWritePreservesSelectionAndIsReportedBeforeSuccessLogging() { + @Test fun failedFrameworkWriteBeforeEffectPreservesSelectionAndIsReportedBeforeSuccessLogging() { AppCompatDelegate.requested = LocaleListCompat.forLanguageTags("en") AppCompatDelegate.failWrite = true assertFalse(AppLanguage.select("ru")) @@ -97,6 +98,16 @@ class AppLanguageTest { assertTrue(AppLanguage.select("ru")); assertEquals("ru", AppLanguage.selectedTag()) } + @Test fun partialFrameworkFailureIsReportedWithoutInventingARollback() { + AppCompatDelegate.requested = LocaleListCompat.forLanguageTags("en") + AppCompatDelegate.failWriteAfter = true + assertFalse(AppLanguage.select("ru")) + assertEquals("ru", AppLanguage.selectedTag()) + assertEquals(listOf("ru"), AppCompatDelegate.selections) + error("after effect") + assertTrue(LogManager.entries.none { it.level == "INFO" }) + } + @Test fun failedSelectionReadIsNotReplacedWithFollowSystem() { AppCompatDelegate.failRead = true assertThrows(IllegalStateException::class.java) { AppLanguage.selectedTag() }