diff --git a/app/src/main/java/com/example/research/common/progress/DictionaryProgressModel.kt b/app/src/main/java/com/example/research/common/progress/DictionaryProgressModel.kt index 9073975..b44b2c9 100644 --- a/app/src/main/java/com/example/research/common/progress/DictionaryProgressModel.kt +++ b/app/src/main/java/com/example/research/common/progress/DictionaryProgressModel.kt @@ -21,14 +21,16 @@ object DictionaryProgressModel { val progress: Float, val percent: Int, @param:StringRes val titleRes: Int, + val indeterminate: Boolean = false, ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Snapshot) return false - return percent == other.percent && titleRes == other.titleRes + return percent == other.percent && titleRes == other.titleRes && + indeterminate == other.indeterminate } override fun hashCode(): Int { - return 31 * percent + titleRes + return 31 * (31 * percent + titleRes) + indeterminate.hashCode() } } fun computeProgressSnapshot( @@ -92,6 +94,7 @@ object DictionaryProgressModel { progress = p, percent = pct, titleRes = R.string.notification_download_title, + indeterminate = dl <= 0f, ) } diff --git a/app/src/main/java/com/example/research/feature/download/DownloadManager.kt b/app/src/main/java/com/example/research/feature/download/DownloadManager.kt index e956a23..fcde452 100644 --- a/app/src/main/java/com/example/research/feature/download/DownloadManager.kt +++ b/app/src/main/java/com/example/research/feature/download/DownloadManager.kt @@ -57,6 +57,7 @@ class DownloadManager( if (!downloadScope.isActive) return onFlowStarted?.invoke() cancelCleanupJob = null + updateDownloadProgressState(DownloadState.Loading, 0f) downloadJob = downloadScope.launch { mutex.withLock { try { @@ -81,8 +82,6 @@ class DownloadManager( return@withLock } - updateDownloadProgressState(DownloadState.Loading, 0f) - var lastReportedProgressBucket = -1 val result = ReSearchTrace.asyncSection(ReSearchTrace.DICTIONARY_DOWNLOAD) { dictionaryRepository.downloadSources(downloadableSources) { progress -> @@ -149,6 +148,7 @@ class DownloadManager( if (!downloadScope.isActive) return onFlowStarted?.invoke() cancelCleanupJob = null + updateDownloadProgressState(DownloadState.Loading, 0f) downloadJob = downloadScope.launch { mutex.withLock { try { @@ -177,8 +177,6 @@ class DownloadManager( return@withLock } - updateDownloadProgressState(DownloadState.Loading, 0f) - var lastReportedProgressBucket = -1 val result = ReSearchTrace.asyncSection(ReSearchTrace.DICTIONARY_DOWNLOAD) { dictionaryRepository.downloadSources(downloadableSources) { progress -> diff --git a/app/src/main/java/com/example/research/feature/download/repository/DictionaryRepository.kt b/app/src/main/java/com/example/research/feature/download/repository/DictionaryRepository.kt index 0b4830a..2e63450 100644 --- a/app/src/main/java/com/example/research/feature/download/repository/DictionaryRepository.kt +++ b/app/src/main/java/com/example/research/feature/download/repository/DictionaryRepository.kt @@ -25,8 +25,10 @@ class DictionaryRepository( private val extractor = DictionaryExtractor(context, fileStorageManager) private val cleaner = DictionaryCleaner(context, fileStorageManager) - private var filesBeforeDownload = mutableSetOf() - private var downloadedPrefixes = mutableSetOf() + private class DownloadSession(val filesBefore: Set, val prefixes: Set) + + @Volatile + private var session: DownloadSession? = null companion object { private const val TAG = "DictionaryRepository" @@ -81,15 +83,10 @@ class DictionaryRepository( val files = fileStorageManager.listFilesInFolder(uri) cleaner.deleteCorruptedArchives(uri, files) - synchronized(filesBeforeDownload) { - filesBeforeDownload = files.map { it.name }.toMutableSet() - } - - synchronized(downloadedPrefixes) { - downloadedPrefixes = sources.mapNotNull { - DictionarySource.extractPrefix(it.urlTemplate) - }.toMutableSet() - } + session = DownloadSession( + filesBefore = files.mapTo(mutableSetOf()) { it.name }, + prefixes = sources.mapNotNullTo(mutableSetOf()) { DictionarySource.extractPrefix(it.urlTemplate) }, + ) downloader.downloadDictionaries( folderUri = uri, @@ -114,63 +111,22 @@ class DictionaryRepository( cleaner.deleteUnprocessedFiles(uri, files) - val filesAfterStandardCleanup = fileStorageManager.listFilesInFolder(uri) + val current = session ?: return@withContext Result.success(Unit) + session = null - val existingFiles = synchronized(filesBeforeDownload) { - filesBeforeDownload.toSet() - } - - val prefixes = synchronized(downloadedPrefixes) { - downloadedPrefixes.toSet() - } - - val finalPrefixes = prefixes.ifEmpty { - val sources = preferencesManager.dictionarySources.first() - sources.mapNotNull { DictionarySource.extractPrefix(it.urlTemplate) }.toSet() - } - - val filesToDelete = if (existingFiles.isEmpty()) { - filesAfterStandardCleanup.filter { file -> - val fileName = file.name - finalPrefixes.any { prefix -> fileName.startsWith("${prefix}_") } + fileStorageManager.listFilesInFolder(uri) + .filter { file -> + file.name !in current.filesBefore && + current.prefixes.any { prefix -> file.name.startsWith("${prefix}_") } } - } else { - filesAfterStandardCleanup.filter { file -> - file.name !in existingFiles && - finalPrefixes.any { prefix -> file.name.startsWith("${prefix}_") } - } - } - - if (filesToDelete.isNotEmpty()) { - var deletedCount = 0 - - for ((fileName, uri) in filesToDelete) { - val docFile = fileStorageManager.getDocumentFile(uri) - if (docFile != null && docFile.exists()) { - val deleted = docFile.delete() - if (deleted) { - deletedCount++ - } else { - val forceDeleted = fileStorageManager.forceDeleteFile(docFile, fileName) - if (!forceDeleted) { - Log.e(TAG, "deleteUnprocessedFiles() - force delete failed for: $fileName") - } else { - deletedCount++ - } - } + .forEach { (fileName, fileUri) -> + val docFile = fileStorageManager.getDocumentFile(fileUri) + if (docFile != null && docFile.exists() && !docFile.delete() && + !fileStorageManager.forceDeleteFile(docFile, fileName) + ) { + Log.e(TAG, "deleteUnprocessedFiles() - force delete failed for: $fileName") } } - } - - synchronized(filesBeforeDownload) { - if (filesBeforeDownload.isNotEmpty()) { - filesBeforeDownload.clear() - } - } - - synchronized(downloadedPrefixes) { - downloadedPrefixes.clear() - } Result.success(Unit) } @@ -183,15 +139,7 @@ class DictionaryRepository( val prefixes = sources.mapNotNull { DictionarySource.extractPrefix(it.urlTemplate) }.toSet() val result = cleaner.performAllCleanup(uri, prefixes, files) - - synchronized(filesBeforeDownload) { - filesBeforeDownload.clear() - } - - synchronized(downloadedPrefixes) { - downloadedPrefixes.clear() - } - + session = null result } } diff --git a/app/src/main/java/com/example/research/ui/settings/components/DictionaryManagement.kt b/app/src/main/java/com/example/research/ui/settings/components/DictionaryManagement.kt index a094b87..c41838b 100644 --- a/app/src/main/java/com/example/research/ui/settings/components/DictionaryManagement.kt +++ b/app/src/main/java/com/example/research/ui/settings/components/DictionaryManagement.kt @@ -268,6 +268,7 @@ private fun DictionaryProgressSectionHost( label = label, onCancel = onCancel, percent = progressSnapshot?.percent ?: 0, + indeterminate = progressSnapshot?.indeterminate ?: false, testTag = progressTestTag, ) } diff --git a/app/src/main/java/com/example/research/ui/settings/components/DictionaryProgressSection.kt b/app/src/main/java/com/example/research/ui/settings/components/DictionaryProgressSection.kt index 0dfccec..191a27e 100644 --- a/app/src/main/java/com/example/research/ui/settings/components/DictionaryProgressSection.kt +++ b/app/src/main/java/com/example/research/ui/settings/components/DictionaryProgressSection.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.ui.res.painterResource import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.testTag @@ -31,6 +32,7 @@ fun DictionaryProgressSection( onCancel: () -> Unit, modifier: Modifier = Modifier, percent: Int, + indeterminate: Boolean, testTag: String, ) { AnimatedVisibility( @@ -44,6 +46,7 @@ fun DictionaryProgressSection( var displayedLabel by remember { mutableStateOf(label) } var displayedPercent by remember { mutableIntStateOf(percent) } var displayedTestTag by remember { mutableStateOf(testTag) } + var displayedIndeterminate by remember { mutableStateOf(indeterminate) } LaunchedEffect(visible) { if (visible) { @@ -60,6 +63,7 @@ fun DictionaryProgressSection( displayedLabel = label displayedPercent = percent displayedTestTag = testTag + displayedIndeterminate = indeterminate } } val animatedState = remember { Animatable(0f) } @@ -88,21 +92,33 @@ fun DictionaryProgressSection( style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary ) - Text( - text = "$displayedPercent%", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.primary - ) + if (!displayedIndeterminate) { + Text( + text = "$displayedPercent%", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary + ) + } } - Spacer(Modifier.height(8.dp)) - LinearProgressIndicator( - progress = { animatedProgress }, + Box( modifier = Modifier .fillMaxWidth() - .height(4.dp) - .clip(MaterialTheme.shapes.small), - ) - Spacer(Modifier.height(8.dp)) + .height(WavyProgressIndicatorDefaults.LinearContainerHeight), + contentAlignment = Alignment.Center, + ) { + if (displayedIndeterminate) { + LinearWavyProgressIndicator(modifier = Modifier.fillMaxWidth()) + } else { + LinearProgressIndicator( + progress = { animatedProgress }, + modifier = Modifier + .fillMaxWidth() + .height(4.dp) + .clip(MaterialTheme.shapes.small), + ) + } + } + Spacer(Modifier.height(4.dp)) OutlinedButton( onClick = onCancel, modifier = Modifier.fillMaxWidth(),