fix(android): prevent memory leaks from detached coroutine scopes
Critical fixes to prevent app crashes when screens are kept open for extended periods: - Add repositoryScope with SupervisorJob for structured concurrency in TssRepository - Replace detached CoroutineScope(Dispatchers.IO).launch with repositoryScope.launch: - Session event subscription (line 206) - Session status polling (line 291) - Message routing (line 1508) - Add cleanup() method to properly cancel all jobs and repositoryScope - Update disconnect() to also cancel sessionStatusPollingJob - Update MainViewModel.onCleared() to call repository.cleanup() This ensures all background coroutines are properly cancelled when the ViewModel is cleared, preventing memory accumulation over time. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
fc86af918f
commit
b30017f3a7
|
|
@ -553,7 +553,8 @@
|
|||
"Bash(\"/c/Users/dong/go/bin/go1.22.10.exe\" install golang.org/x/mobile/cmd/gobind@c31d5b91ecc32c0d598b8fe8457d244ca0b4e815)",
|
||||
"Bash(\"/c/Users/dong/go/bin/go1.22.10.exe\" mod tidy)",
|
||||
"Bash(adb devices:*)",
|
||||
"Bash(adb logcat:*)"
|
||||
"Bash(adb logcat:*)",
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(android\\): add 5-minute polling timeout mechanism for keygen/sign\n\nImplements Electron''s checkAndTriggerKeygen\\(\\) polling fallback:\n- Adds polling every 2 seconds with 5-minute timeout\n- Triggers keygen/sign via synthetic session_started event on in_progress status\n- Handles gRPC stream disconnection when app goes to background\n- Shows timeout error in UI via existing error mechanism\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ class TssRepository @Inject constructor(
|
|||
// Called when 5-minute polling timeout is reached without session starting
|
||||
private var keygenTimeoutCallback: ((String) -> Unit)? = null
|
||||
|
||||
// Repository-level CoroutineScope for background tasks
|
||||
// Uses SupervisorJob so individual task failures don't cancel other tasks
|
||||
private val repositoryScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
companion object {
|
||||
// Polling interval for session status check (matching Electron's 2-second interval)
|
||||
private const val POLL_INTERVAL_MS = 2000L
|
||||
|
|
@ -142,6 +146,19 @@ class TssRepository @Inject constructor(
|
|||
fun disconnect() {
|
||||
messageCollectionJob?.cancel()
|
||||
sessionEventJob?.cancel()
|
||||
sessionStatusPollingJob?.cancel()
|
||||
grpcClient.disconnect()
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup all resources when the repository is destroyed.
|
||||
* Call this when the app is being destroyed to prevent memory leaks.
|
||||
*/
|
||||
fun cleanup() {
|
||||
messageCollectionJob?.cancel()
|
||||
sessionEventJob?.cancel()
|
||||
sessionStatusPollingJob?.cancel()
|
||||
repositoryScope.cancel()
|
||||
grpcClient.disconnect()
|
||||
}
|
||||
|
||||
|
|
@ -199,7 +216,7 @@ class TssRepository @Inject constructor(
|
|||
private fun startSessionEventSubscription() {
|
||||
sessionEventJob?.cancel()
|
||||
android.util.Log.d("TssRepository", "Starting session event subscription for partyId: $partyId")
|
||||
sessionEventJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
sessionEventJob = repositoryScope.launch {
|
||||
grpcClient.subscribeSessionEvents(partyId).collect { event ->
|
||||
android.util.Log.d("TssRepository", "=== Session event received ===")
|
||||
android.util.Log.d("TssRepository", " eventType: ${event.eventType}")
|
||||
|
|
@ -284,7 +301,7 @@ class TssRepository @Inject constructor(
|
|||
|
||||
android.util.Log.d("TssRepository", "[POLLING] Starting session status polling for $sessionType session: $sessionId")
|
||||
|
||||
sessionStatusPollingJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
sessionStatusPollingJob = repositoryScope.launch {
|
||||
val startTime = System.currentTimeMillis()
|
||||
var pollCount = 0
|
||||
|
||||
|
|
@ -1501,7 +1518,7 @@ class TssRepository @Inject constructor(
|
|||
currentMessageRoutingPartyIndex = partyIndex
|
||||
|
||||
messageCollectionJob?.cancel()
|
||||
messageCollectionJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
messageCollectionJob = repositoryScope.launch {
|
||||
// Collect outgoing messages from TSS and route via gRPC
|
||||
launch {
|
||||
tssNativeBridge.outgoingMessages.collect { message ->
|
||||
|
|
|
|||
|
|
@ -1204,7 +1204,7 @@ class MainViewModel @Inject constructor(
|
|||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
repository.disconnect()
|
||||
repository.cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue