From 8e5bb2ea9a9ee125da1c076dfd9c0fd048f6367f Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:20:50 +0300 Subject: [PATCH] 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) --- .../android/ui/ApkDownloadViewModel.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt index d553dcbf..d027d82f 100644 --- a/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt @@ -196,7 +196,12 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat private fun onCancelDownload() { 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() { @@ -210,21 +215,26 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat 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 { // WorkManager is the source of truth for active work. A queued or // newly started job legitimately has no partial file yet, so never // 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 } val resolvedStatus = resolveApkStatus() _state.update { current -> - if (current.apkStatus is ApkPreparationStatus.Downloading) { + if (!force && current.apkStatus is ApkPreparationStatus.Downloading) { current } else { - current.copy(apkStatus = resolvedStatus) + current.copy(apkStatus = resolvedStatus, downloadProgress = 0) } } }