From 7319cf8f5bcd1d711cc8abaeb752dc9b08d15fa1 Mon Sep 17 00:00:00 2001 From: QIU Date: Mon, 17 Aug 2026 16:46:09 +0000 Subject: [PATCH] fix(coroutines): propagate cancellation in remote source and status view model RemoteSource's five suspend functions and SatStatusViewModel's two fetch paths caught bare Exception, which also swallows CancellationException. When the owning scope is cancelled (screen leaves, app closes) a cancelled network call was reported as a null/error result instead of stopping: the caller kept running until the next suspension point, and SatStatusViewModel wrote state updates into an already-cancelled scope. Correct coroutine hygiene is to let cancellation propagate - rethrow CancellationException before the generic catch. Verified semantically with an asyncio probe: a swallowed cancel returns a normal-looking null and the caller continues; a propagated cancel stops the coroutine immediately. No behaviour change for real errors; :core:data and :feature:status compile. --- .../look4sat/core/data/source/RemoteSource.kt | 11 +++++++++++ .../look4sat/feature/status/SatStatusViewModel.kt | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/RemoteSource.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/RemoteSource.kt index 985d0731..a24191c0 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/RemoteSource.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/RemoteSource.kt @@ -20,6 +20,7 @@ package com.rtbishop.look4sat.core.data.source import android.content.ContentResolver import androidx.core.net.toUri import com.rtbishop.look4sat.core.domain.source.IRemoteSource +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -37,6 +38,8 @@ class RemoteSource( try { val fileUri = uri.toUri() contentResolver.openInputStream(fileUri)?.buffered() + } catch (exception: CancellationException) { + throw exception } catch (exception: Exception) { println("RemoteSource file stream exception: $exception") null @@ -53,6 +56,8 @@ class RemoteSource( if (!response.isSuccessful) return@use null response.body?.string() } + } catch (exception: CancellationException) { + throw exception } catch (exception: Exception) { println("RemoteSource amsat status exception: $exception") null @@ -66,6 +71,8 @@ class RemoteSource( if (!response.isSuccessful) return@withContext null ByteArrayInputStream(response.body.bytes()) } + } catch (exception: CancellationException) { + throw exception } catch (exception: Exception) { println("RemoteSource network stream exception: $exception") null @@ -82,6 +89,8 @@ class RemoteSource( if (!response.isSuccessful) return@use null response.body?.string() } + } catch (exception: CancellationException) { + throw exception } catch (exception: Exception) { println("RemoteSource amsat catalog exception: $exception") null @@ -98,6 +107,8 @@ class RemoteSource( if (!response.isSuccessful) return@use null response.body?.string() } + } catch (exception: CancellationException) { + throw exception } catch (exception: Exception) { println("RemoteSource amsat reports exception: $exception") null diff --git a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt index a6997781..dd0c4a57 100644 --- a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt +++ b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.rtbishop.look4sat.core.domain.model.SatReport import com.rtbishop.look4sat.core.domain.model.SatStatus import com.rtbishop.look4sat.core.domain.repository.IMainContainer +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -50,6 +51,8 @@ class SatStatusViewModel( it.copy(isLoading = false, isRefreshing = false, error = "fetch_failed") } } + } catch (exception: CancellationException) { + throw exception } catch (exception: Exception) { _uiState.update { it.copy(isLoading = false, isRefreshing = false, error = exception.message ?: "fetch_failed") @@ -76,6 +79,8 @@ class SatStatusViewModel( } else { _uiState.update { it.copy(isRefreshing = false, error = "fetch_failed") } } + } catch (exception: CancellationException) { + throw exception } catch (exception: Exception) { _uiState.update { it.copy(isRefreshing = false, error = exception.message ?: "fetch_failed") } }