fix: clear the downloading state when a download is cancelled

Addresses review on #812.

Pressing the new stop button before a partial file exists -- while
resolving the release, or waiting for Tor -- left the row disabled and
spinning for the lifetime of the ViewModel. Two guards conspired:
checkStatus() returns early while the state is Downloading, and the
downloader observer deliberately ignores the Idle that WorkManager
reports for a cancelled job. Both exist to stop a running job being
second-guessed from cache contents, and neither anticipated a job that
is no longer running.

checkStatus() takes a force flag, used only by cancellation, and clears
the stale progress along with the status.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Moe Hamade 2026-07-29 15:20:50 +03:00
parent b79ae8e7f1
commit 8e5bb2ea9a

View File

@ -196,7 +196,12 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
private fun onCancelDownload() { private fun onCancelDownload() {
downloader.cancelDownload() downloader.cancelDownload()
checkStatus()
// Nothing else will move the UI off the spinner. checkStatus() refuses to
// overwrite a Downloading state, and the Idle that WorkManager reports for a
// cancelled job is ignored for the same reason -- both guards protect a job
// that is still running, which this one is not.
checkStatus(force = true)
} }
private fun startDownload() { private fun startDownload() {
@ -210,21 +215,26 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
downloader.startDownload() downloader.startDownload()
} }
private fun checkStatus() { /**
* @param force resolve even while the state says Downloading. Only cancellation
* should pass true: the guard below exists so a running job is never second-guessed
* from cache contents, but a cancelled one has no other route out of that state.
*/
private fun checkStatus(force: Boolean = false) {
viewModelScope.launch { viewModelScope.launch {
// WorkManager is the source of truth for active work. A queued or // WorkManager is the source of truth for active work. A queued or
// newly started job legitimately has no partial file yet, so never // newly started job legitimately has no partial file yet, so never
// infer that it is orphaned from cache contents. // infer that it is orphaned from cache contents.
if (_state.value.apkStatus is ApkPreparationStatus.Downloading) { if (!force && _state.value.apkStatus is ApkPreparationStatus.Downloading) {
return@launch return@launch
} }
val resolvedStatus = resolveApkStatus() val resolvedStatus = resolveApkStatus()
_state.update { current -> _state.update { current ->
if (current.apkStatus is ApkPreparationStatus.Downloading) { if (!force && current.apkStatus is ApkPreparationStatus.Downloading) {
current current
} else { } else {
current.copy(apkStatus = resolvedStatus) current.copy(apkStatus = resolvedStatus, downloadProgress = 0)
} }
} }
} }