fix: probe source availability before showing download progress
Instant server failures (404/HTML error page) no longer flash a 0% progress bar; the check mirrors the downloader's previous-day fallback and tolerates transient network errors, which still go through the downloader's retry path.
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
+33
-3
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+42
-56
@@ -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<Unit> = 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<String>,
|
||||
onProgress: (Float) -> Unit
|
||||
): Result<Unit> = 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<String>? = null): List<DictionarySource> =
|
||||
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<DictionarySource>
|
||||
): List<DictionarySource> = 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<DictionarySource>,
|
||||
onProgress: (Float) -> Unit
|
||||
): Result<Unit> = 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
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user