From 26c714fc2475a5aba7fc05ce594bf7f43fe4e47a Mon Sep 17 00:00:00 2001 From: QIU Date: Thu, 13 Aug 2026 09:55:15 +0000 Subject: [PATCH] fix(cw): stop swallowing coroutine cancellation on pause/resume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户反馈: 暂停再恢复时经常弹出 "CW decode failed: The coroutine scope left the composition"。 根因: 暂停 (isListening=false) 使 LaunchedEffect 重启、旧的采集协程被 取消。若此刻 decodeWindow 正在 withContext(Dispatchers.Default) 里做 ONNX 推理, 取消传播时会抛出 LeftCompositionCancellationException (message 即 "The coroutine scope left the composition", 是 CancellationException 的子类)。processBuffer 的 catch (Throwable) 把它当成解码失败吞掉, 既误报了 错误横幅, 又破坏了协程取消的正常传播。 修复: - processBuffer 的 decodeWindow catch 里, CancellationException 直接 rethrow (暂停导致的中断是正常流程, 不是错误)。 - archiveDecode 调用同样包 try-catch, CancellationException rethrow, 其余异常只记日志不崩溃。 这是协程的标准纪律: 永远不要把 CancellationException 当业务异常吞掉。 版本: 4.5.5 -> 4.5.7 (versionCode 460)。此前多次删 v4.5.5 tag 重打导致 release 页面累积 12 个 draft 草稿, 已全部清除。 --- .../rtbishop/look4sat/core/data/cw/CwDeepDecoder.kt | 13 ++++++++++++- gradle/libs.versions.toml | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/cw/CwDeepDecoder.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/cw/CwDeepDecoder.kt index 5b7ed644..e614b6d0 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/cw/CwDeepDecoder.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/cw/CwDeepDecoder.kt @@ -26,6 +26,7 @@ import com.rtbishop.look4sat.core.domain.cw.CwCtcDecoder import com.rtbishop.look4sat.core.domain.cw.CwDeepBuffer import com.rtbishop.look4sat.core.domain.cw.CwDeepSpectrogram import com.rtbishop.look4sat.core.domain.cw.ICwDecoder +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -185,7 +186,12 @@ class CwDeepDecoder(context: Context) : ICwDecoder { if (archiveSize >= ARCHIVE_THRESHOLD) { val audio = archiveBuffer.copyOf(archiveSize) archiveSize = 0 - archiveDecode(audio) + try { + archiveDecode(audio) + } catch (t: Throwable) { + if (t is CancellationException) throw t + Log.e(TAG, "archive decode failed", t) + } } } @@ -199,6 +205,11 @@ class CwDeepDecoder(context: Context) : ICwDecoder { try { decodeWindow(buffer.snapshot()) } catch (t: Throwable) { + // Cancellation is normal when the user pauses: the capture coroutine + // is cancelled while an inference is in flight. Never swallow it as + // a decode error — rethrow so the coroutine machinery works, and do + // not flash a spurious "decode failed" banner. + if (t is CancellationException) throw t Log.e(TAG, "inference failed", t) _errorMessage.value = "CW decode failed: ${t.message ?: t.javaClass.simpleName}" runCatching { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 82beb8ef..ccbaa9aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] #noinspection UnusedVersionCatalogEntry -appVersionCode = "458" +appVersionCode = "460" #noinspection UnusedVersionCatalogEntry -appVersionName = "4.5.5" +appVersionName = "4.5.7" #noinspection UnusedVersionCatalogEntry compileSdk = "37" #noinspection UnusedVersionCatalogEntry