diff --git a/app/src/main/java/com/example/research/ReSearchApplication.kt b/app/src/main/java/com/example/research/ReSearchApplication.kt index ce33077..cd8df1c 100644 --- a/app/src/main/java/com/example/research/ReSearchApplication.kt +++ b/app/src/main/java/com/example/research/ReSearchApplication.kt @@ -62,7 +62,8 @@ class ReSearchApplication : Application() { downloadManager = DownloadManager( dictionaryRepository = downloadDictionaryRepository, - unknownErrorMessage = getString(R.string.unknown_error) + unknownErrorMessage = getString(R.string.unknown_error), + sourceUnavailableMessage = getString(R.string.download_file_not_found) ) dictionaryImportManager = DictionaryImportManager( 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 4b3ea83..58d86fd 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 @@ -12,6 +12,7 @@ import kotlin.time.Duration.Companion.seconds class DownloadManager( private val dictionaryRepository: DictionaryRepository, private val unknownErrorMessage: String, + private val sourceUnavailableMessage: String, dispatcher: CoroutineDispatcher = Dispatchers.IO, externalScope: CoroutineScope? = null ) { @@ -49,22 +50,32 @@ class DownloadManager( downloadJob = downloadScope.launch { mutex.withLock { try { - updateDownloadProgressState(DownloadState.Loading, 0f) - - val hasEnabledSources = try { - dictionaryRepository.hasEnabledSources() + val enabledSources = try { + dictionaryRepository.getEnabledSources() } catch (_: Exception) { - false + emptyList() } - if (!hasEnabledSources) { + if (enabledSources.isEmpty()) { dictionaryRepository.performAllCleanup() updateDownloadProgressState(DownloadState.Success, 1f) return@withLock } + + val downloadableSources = + dictionaryRepository.filterServerAvailableSources(enabledSources) + if (downloadableSources.isEmpty()) { + updateDownloadProgressState( + DownloadState.Error(sourceUnavailableMessage), 0f + ) + return@withLock + } + + updateDownloadProgressState(DownloadState.Loading, 0f) + var lastReportedProgressBucket = -1 val result = ReSearchTrace.asyncSection(ReSearchTrace.DICTIONARY_DOWNLOAD) { - dictionaryRepository.downloadDictionaries { progress -> + dictionaryRepository.downloadSources(downloadableSources) { progress -> val progressBucket = progressBucket(progress, DOWNLOAD_PROGRESS_STEP_PERCENT) if (progressBucket != lastReportedProgressBucket) { lastReportedProgressBucket = progressBucket @@ -130,16 +141,36 @@ class DownloadManager( downloadJob = downloadScope.launch { mutex.withLock { try { - updateDownloadProgressState(DownloadState.Loading, 0f) - if (sourceUrls.isEmpty()) { updateDownloadProgressState(DownloadState.Success, 1f) return@withLock } + val sources = try { + dictionaryRepository.getEnabledSources(sourceUrls) + } catch (_: Exception) { + emptyList() + } + + if (sources.isEmpty()) { + updateDownloadProgressState(DownloadState.Success, 1f) + return@withLock + } + + val downloadableSources = + dictionaryRepository.filterServerAvailableSources(sources) + if (downloadableSources.isEmpty()) { + updateDownloadProgressState( + DownloadState.Error(sourceUnavailableMessage), 0f + ) + return@withLock + } + + updateDownloadProgressState(DownloadState.Loading, 0f) + var lastReportedProgressBucket = -1 val result = ReSearchTrace.asyncSection(ReSearchTrace.DICTIONARY_DOWNLOAD) { - dictionaryRepository.downloadSpecificSources(sourceUrls) { progress -> + dictionaryRepository.downloadSources(downloadableSources) { progress -> val progressBucket = progressBucket(progress, DOWNLOAD_PROGRESS_STEP_PERCENT) if (progressBucket != lastReportedProgressBucket) { lastReportedProgressBucket = progressBucket diff --git a/app/src/main/java/com/example/research/feature/download/repository/DictionaryChecker.kt b/app/src/main/java/com/example/research/feature/download/repository/DictionaryChecker.kt index a5c8910..cf1b131 100644 --- a/app/src/main/java/com/example/research/feature/download/repository/DictionaryChecker.kt +++ b/app/src/main/java/com/example/research/feature/download/repository/DictionaryChecker.kt @@ -12,6 +12,12 @@ import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request +enum class SourceAvailability { + AVAILABLE, + NOT_FOUND, + UNKNOWN +} + class DictionaryChecker( private val fileStorageManager: FileStorageManager, private val client: OkHttpClient @@ -85,7 +91,26 @@ class DictionaryChecker( } } - private fun isSourceAvailableOnServer(source: DictionarySource, date: String): Boolean { + suspend fun checkSourceAvailability(source: DictionarySource): SourceAvailability = + withContext(Dispatchers.IO) { + val currentDate = DateUtils.getCurrentDateString() + when (probeSource(source, currentDate)) { + SourceAvailability.AVAILABLE -> SourceAvailability.AVAILABLE + SourceAvailability.UNKNOWN -> SourceAvailability.UNKNOWN + SourceAvailability.NOT_FOUND -> { + if (DictionarySource.hasDatePlaceholder(source.urlTemplate)) { + probeSource(source, DateUtils.getPreviousDateString()) + } else { + SourceAvailability.NOT_FOUND + } + } + } + } + + private fun isSourceAvailableOnServer(source: DictionarySource, date: String): Boolean = + probeSource(source, date) == SourceAvailability.AVAILABLE + + private fun probeSource(source: DictionarySource, date: String): SourceAvailability { return try { val url = DictionarySource.buildUrl(source.urlTemplate, date) val request = Request.Builder() @@ -94,12 +119,17 @@ class DictionaryChecker( .build() client.newCall(request).execute().use { response -> - response.isSuccessful && + if (response.isSuccessful && response.header("Content-Type")?.startsWith("text/html") != true + ) { + SourceAvailability.AVAILABLE + } else { + SourceAvailability.NOT_FOUND + } } } catch (e: Exception) { Log.w(TAG, "Failed to check source availability for ${source.urlTemplate}: ${e.message}") - false + SourceAvailability.UNKNOWN } } 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 7ab9f2b..995c737 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 @@ -8,6 +8,8 @@ import com.example.research.core.domain.model.DictionarySource import com.example.research.data.local.preferences.PreferencesManager import com.example.research.common.util.FileStorageManager import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -40,74 +42,58 @@ class DictionaryRepository( checker.areDictionariesUpToDate(uri, sourcesToCheck, files) } - suspend fun hasEnabledSources(): Boolean = withContext(Dispatchers.IO) { - preferencesManager.dictionarySources.first().any { it.isEnabled } - } - - suspend fun downloadDictionaries( - onProgress: (Float) -> Unit - ): Result = withContext(Dispatchers.IO) { - val folderUri = preferencesManager.dictionaryPath - val uri = Uri.fromFile(File(folderUri)) - val files = fileStorageManager.listFilesInFolder(uri) - cleaner.deleteCorruptedArchives(uri, files) - - synchronized(filesBeforeDownload) { - filesBeforeDownload = files.map { it.name }.toMutableSet() - } - - val sources = preferencesManager.dictionarySources.first() - val enabledSources = sources.filter { it.isEnabled } - - synchronized(downloadedPrefixes) { - downloadedPrefixes = enabledSources.mapNotNull { - DictionarySource.extractPrefix(it.urlTemplate) - }.toMutableSet() - } - - downloader.downloadDictionaries( - folderUri = uri, - sources = enabledSources, - onProgress = onProgress - ) - } - - suspend fun downloadSpecificSources( - sourceUrls: List, - onProgress: (Float) -> Unit - ): Result = withContext(Dispatchers.IO) { - val folderUri = preferencesManager.dictionaryPath - val uri = Uri.fromFile(File(folderUri)) - val files = fileStorageManager.listFilesInFolder(uri) - cleaner.deleteCorruptedArchives(uri, files) - - synchronized(filesBeforeDownload) { - filesBeforeDownload = files.map { it.name }.toMutableSet() - } - - val allSources = preferencesManager.dictionarySources.first() - val sourceKeys = sourceUrls.mapTo(mutableSetOf(), DictionarySource::identityKey) - val sourcesToDownload = allSources.mapNotNull { source -> - if (!source.isEnabled || DictionarySource.identityKey(source.urlTemplate) !in sourceKeys) { - null - } else { - source.copy(urlTemplate = DictionarySource.normalizeTemplate(source.urlTemplate)) + suspend fun getEnabledSources(sourceUrls: List? = null): List = + withContext(Dispatchers.IO) { + val enabledSources = preferencesManager.dictionarySources.first() + .filter { it.isEnabled } + if (sourceUrls == null) { + return@withContext enabledSources + } + val sourceKeys = sourceUrls.mapTo(mutableSetOf(), DictionarySource::identityKey) + enabledSources.mapNotNull { source -> + if (DictionarySource.identityKey(source.urlTemplate) !in sourceKeys) { + null + } else { + source.copy(urlTemplate = DictionarySource.normalizeTemplate(source.urlTemplate)) + } } } - if (sourcesToDownload.isEmpty()) { - return@withContext Result.success(Unit) + suspend fun filterServerAvailableSources( + sources: List + ): List = withContext(Dispatchers.IO) { + if (sources.isEmpty()) { + return@withContext emptyList() + } + sources + .map { source -> async { source to checker.checkSourceAvailability(source) } } + .awaitAll() + .filter { (_, availability) -> availability != SourceAvailability.NOT_FOUND } + .map { (source, _) -> source } + } + + suspend fun downloadSources( + sources: List, + onProgress: (Float) -> Unit + ): Result = withContext(Dispatchers.IO) { + val folderUri = preferencesManager.dictionaryPath + val uri = Uri.fromFile(File(folderUri)) + val files = fileStorageManager.listFilesInFolder(uri) + cleaner.deleteCorruptedArchives(uri, files) + + synchronized(filesBeforeDownload) { + filesBeforeDownload = files.map { it.name }.toMutableSet() } synchronized(downloadedPrefixes) { - downloadedPrefixes = sourcesToDownload.mapNotNull { + downloadedPrefixes = sources.mapNotNull { DictionarySource.extractPrefix(it.urlTemplate) }.toMutableSet() } downloader.downloadDictionaries( folderUri = uri, - sources = sourcesToDownload, + sources = sources, onProgress = onProgress ) }