fix(cw): stop swallowing coroutine cancellation on pause/resume

用户反馈: 暂停再恢复时经常弹出 "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 草稿, 已全部清除。
This commit is contained in:
mckero committed 2026-08-13 09:55:15 +00:00
1 parent 2e1d8b9c00
commit 26c714fc24
2 files changed
+14 -3

No files matched your search

@@ -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 {
+2 -2
View File
@@ -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