From b60b5121aa5836f3e8a7e419a93f81396562aee9 Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Sat, 1 Aug 2026 16:01:08 +0300 Subject: [PATCH] fix: observe cancellation before promoting a verified APK Codex is right about this one. Cancellation in Kotlin is cooperative, and everything from validateDownloadedApk() through saveMetadata() is plain blocking code with no suspension point. Stopping during the signature check was therefore not observed until after the temp file had been renamed and its metadata written, so the worker committed the APK while WorkManager reported the work cancelled. That also raced onCancelDownload(): its checkStatus() could read the cache before the commit and settle on NotDownloaded, after which the cancelled work maps to Idle and the observer ignores it. The row then advertised "Not ready" with a verified universal APK already in the cache, and tapping it downloaded the same bytes again. One checkpoint after validation, which is the slow step and so the most likely moment to press Stop. The verified temp file is left in place, so the next attempt resumes rather than starting over. Co-Authored-By: Claude Opus 5 (1M context) --- .../java/com/bitchat/android/util/UniversalApkManager.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/bitchat/android/util/UniversalApkManager.kt b/app/src/main/java/com/bitchat/android/util/UniversalApkManager.kt index a20f8419..b296d22e 100644 --- a/app/src/main/java/com/bitchat/android/util/UniversalApkManager.kt +++ b/app/src/main/java/com/bitchat/android/util/UniversalApkManager.kt @@ -9,6 +9,7 @@ import com.bitchat.android.net.ArtiTorManager import com.bitchat.android.net.OkHttpProvider import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import okhttp3.Call @@ -217,6 +218,12 @@ class UniversalApkManager( phaseCallback?.invoke(ApkDownloader.DownloadPhase.VerifyingSignature) validateDownloadedApk(tempFile, source) + // Everything from here to the metadata write is plain blocking code, so a + // cancellation arriving during the (slow) signature check would otherwise go + // unobserved and commit the APK anyway. The verified temp file survives for + // resume; only the promotion is abandoned. + ensureActive() + val version = downloadedVersionName(tempFile) val safeVersion = version.replace(Regex("[^A-Za-z0-9._-]"), "_") val finalFileName = "$APK_FILE_PREFIX$safeVersion.apk"