From d360ecfb2bd4a5a1f859254d4d1c71e2cd32f50e Mon Sep 17 00:00:00 2001 From: OneWay Date: Sat, 5 Sep 2026 21:55:01 +0800 Subject: [PATCH] fix: defer import errors until the copy loop finishes Errors and skips are collected while the copy loop runs and reported once it ends. The final import state is exhaustive - success, already added, invalid name, or nothing supported found - so every import run reaches a terminal state and the pipeline notification always resolves instead of lingering as an indeterminate progress bar. --- .../common/util/NotificationHelper.kt | 2 ++ .../service/DictionaryForegroundService.kt | 4 +++ .../feature/import/DictionaryImportManager.kt | 22 +++++++++---- .../research/feature/import/ImportOutcome.kt | 32 +++++++++++++++++++ app/src/main/res/values-ru/strings.xml | 3 +- app/src/main/res/values/strings.xml | 3 +- 6 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/example/research/feature/import/ImportOutcome.kt diff --git a/app/src/main/java/com/example/research/common/util/NotificationHelper.kt b/app/src/main/java/com/example/research/common/util/NotificationHelper.kt index 3df875c..c4a1ecd 100644 --- a/app/src/main/java/com/example/research/common/util/NotificationHelper.kt +++ b/app/src/main/java/com/example/research/common/util/NotificationHelper.kt @@ -93,6 +93,8 @@ class NotificationHelper(private val context: Context) { notificationManager.cancel(NOTIFICATION_ID) } + fun hasActiveProgressNotification(): Boolean = lastNotificationKey != 0 + fun showUnifiedProgressNotification( title: String, contentText: String, diff --git a/app/src/main/java/com/example/research/feature/download/service/DictionaryForegroundService.kt b/app/src/main/java/com/example/research/feature/download/service/DictionaryForegroundService.kt index 2716c1f..f377c90 100644 --- a/app/src/main/java/com/example/research/feature/download/service/DictionaryForegroundService.kt +++ b/app/src/main/java/com/example/research/feature/download/service/DictionaryForegroundService.kt @@ -97,6 +97,10 @@ class DictionaryForegroundService : Service() { contentText = "${snapshot.percent}%", progressPercent = snapshot.percent, ) + } else if (snapshot == null && !isFinalizing && + notificationHelper.hasActiveProgressNotification() + ) { + notificationHelper.cancelNotification() } } } diff --git a/app/src/main/java/com/example/research/feature/import/DictionaryImportManager.kt b/app/src/main/java/com/example/research/feature/import/DictionaryImportManager.kt index 8d744f8..9bc86ea 100644 --- a/app/src/main/java/com/example/research/feature/import/DictionaryImportManager.kt +++ b/app/src/main/java/com/example/research/feature/import/DictionaryImportManager.kt @@ -36,6 +36,8 @@ class DictionaryImportManager( val dictionariesDir = File(context.getExternalFilesDir(null), "dictionaries") val totalFiles = uris.size.coerceAtLeast(1) importedFiles.clear() + val skippedNames = mutableListOf() + val deferredErrors = mutableListOf() if (!dictionariesDir.exists()) { dictionariesDir.mkdirs() @@ -46,9 +48,7 @@ class DictionaryImportManager( currentCoroutineContext().ensureActive() val fileName = getFileName(uri) ?: continue if (SafeFileName.validate(fileName) == null) { - mutableImportState.value = ImportState.Error( - context.getString(R.string.import_invalid_file_name, fileName) - ) + deferredErrors += context.getString(R.string.import_invalid_file_name, fileName) continue } val lowerFileName = fileName.lowercase() @@ -65,9 +65,7 @@ class DictionaryImportManager( val destFile = File(dictionariesDir, fileName) if (destFile.exists()) { - mutableImportState.value = ImportState.Error( - context.getString(R.string.import_file_exists, fileName) - ) + skippedNames += fileName continue } @@ -115,7 +113,17 @@ class DictionaryImportManager( currentCoroutineContext().ensureActive() - mutableImportState.value = ImportState.Success + mutableImportState.value = resolveImportOutcome( + importedCount = importedFiles.size, + skippedNames = skippedNames, + deferredErrors = deferredErrors, + ).toImportState( + existsMessage = { names -> + context.getString(R.string.import_file_exists, names.joinToString(separator = ", ")) + }, + invalidMessage = { message -> message }, + nothingImportedMessage = { context.getString(R.string.import_nothing_imported) }, + ) } catch (e: CancellationException) { cleanupImportedFiles() diff --git a/app/src/main/java/com/example/research/feature/import/ImportOutcome.kt b/app/src/main/java/com/example/research/feature/import/ImportOutcome.kt new file mode 100644 index 0000000..00973f7 --- /dev/null +++ b/app/src/main/java/com/example/research/feature/import/ImportOutcome.kt @@ -0,0 +1,32 @@ +package com.example.research.feature.import + +import com.example.research.ui.settings.ImportState + +internal sealed interface ImportOutcome { + data object Imported : ImportOutcome + data class AlreadyExists(val names: List) : ImportOutcome + data class InvalidFile(val message: String) : ImportOutcome + data object NothingImported : ImportOutcome +} + +internal fun resolveImportOutcome( + importedCount: Int, + skippedNames: List, + deferredErrors: List, +): ImportOutcome = when { + importedCount > 0 -> ImportOutcome.Imported + skippedNames.isNotEmpty() -> ImportOutcome.AlreadyExists(skippedNames) + deferredErrors.isNotEmpty() -> ImportOutcome.InvalidFile(deferredErrors.first()) + else -> ImportOutcome.NothingImported +} + +internal fun ImportOutcome.toImportState( + existsMessage: (List) -> String, + invalidMessage: (String) -> String, + nothingImportedMessage: () -> String, +): ImportState = when (this) { + ImportOutcome.Imported -> ImportState.Success + is ImportOutcome.AlreadyExists -> ImportState.Error(existsMessage(names)) + is ImportOutcome.InvalidFile -> ImportState.Error(invalidMessage(message)) + ImportOutcome.NothingImported -> ImportState.Error(nothingImportedMessage()) +} diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 6082528..26fb402 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -67,8 +67,9 @@ Словари Выбрать файлы Не удалось импортировать словарь: %1$s - Файл уже существует: %1$s + Словарь уже добавлен: %1$s Недопустимое имя файла: %1$s + В выбранном нет поддерживаемых файлов словарей URL Добавить источник Этот URL уже существует diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 32a7df0..fd82745 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -65,8 +65,9 @@ Dictionaries Select files Failed to import dictionary: %1$s - File already exists: %1$s + Dictionary already added: %1$s Invalid file name: %1$s + No supported dictionary files were found in the selection URL Add source This URL already exists