feat(android): add 5-minute polling timeout mechanism for keygen/sign
Implements Electron's checkAndTriggerKeygen() polling fallback: - Adds polling every 2 seconds with 5-minute timeout - Triggers keygen/sign via synthetic session_started event on in_progress status - Handles gRPC stream disconnection when app goes to background - Shows timeout error in UI via existing error mechanism 🤖 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
ad79679ee2
commit
fc86af918f
|
|
@ -59,6 +59,17 @@ class TssRepository @Inject constructor(
|
|||
// Android gRPC streams can disconnect when app goes to background, so we poll as backup
|
||||
private var sessionStatusPollingJob: Job? = null
|
||||
|
||||
// Keygen timeout callback (set by ViewModel)
|
||||
// Called when 5-minute polling timeout is reached without session starting
|
||||
private var keygenTimeoutCallback: ((String) -> Unit)? = null
|
||||
|
||||
companion object {
|
||||
// Polling interval for session status check (matching Electron's 2-second interval)
|
||||
private const val POLL_INTERVAL_MS = 2000L
|
||||
// Maximum wait time for keygen to start after all parties joined (5 minutes, matching Electron)
|
||||
private const val MAX_WAIT_MS = 5 * 60 * 1000L
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current party ID
|
||||
* Note: This will throw UninitializedPropertyAccessException if called before registerParty()
|
||||
|
|
@ -244,6 +255,127 @@ class TssRepository @Inject constructor(
|
|||
sessionEventCallback = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* Set keygen timeout callback (called by ViewModel)
|
||||
* This callback is invoked when the 5-minute polling timeout is reached
|
||||
* without the keygen session starting (matching Electron's checkAndTriggerKeygen timeout)
|
||||
*/
|
||||
fun setKeygenTimeoutCallback(callback: (String) -> Unit) {
|
||||
keygenTimeoutCallback = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* Start polling session status as fallback mechanism
|
||||
* This matches Electron's checkAndTriggerKeygen() polling behavior:
|
||||
* - Polls every 2 seconds
|
||||
* - Times out after 5 minutes
|
||||
* - Triggers keygen when session status is "in_progress"
|
||||
* - Calls timeout callback if keygen doesn't start within 5 minutes
|
||||
*
|
||||
* Android needs this because gRPC streams can disconnect when app goes to background,
|
||||
* causing session_started events to be missed.
|
||||
*
|
||||
* @param sessionId The session to poll
|
||||
* @param sessionType "keygen" or "sign" - determines which callback to invoke
|
||||
*/
|
||||
fun startSessionStatusPolling(sessionId: String, sessionType: String = "keygen") {
|
||||
// Cancel any existing polling job
|
||||
sessionStatusPollingJob?.cancel()
|
||||
|
||||
android.util.Log.d("TssRepository", "[POLLING] Starting session status polling for $sessionType session: $sessionId")
|
||||
|
||||
sessionStatusPollingJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
val startTime = System.currentTimeMillis()
|
||||
var pollCount = 0
|
||||
|
||||
while (isActive && (System.currentTimeMillis() - startTime) < MAX_WAIT_MS) {
|
||||
pollCount++
|
||||
android.util.Log.d("TssRepository", "[POLLING] Poll #$pollCount for session: $sessionId")
|
||||
|
||||
try {
|
||||
val statusResult = getSessionStatus(sessionId)
|
||||
|
||||
statusResult.fold(
|
||||
onSuccess = { status ->
|
||||
android.util.Log.d("TssRepository", "[POLLING] Session status: ${status.status}, completed: ${status.completedParties}/${status.totalParties}")
|
||||
|
||||
// Check if session is in_progress (all parties joined and ready)
|
||||
if (status.status == "in_progress") {
|
||||
android.util.Log.d("TssRepository", "[POLLING] Session is in_progress, triggering $sessionType via callback")
|
||||
|
||||
// Create synthetic session_started event to trigger keygen/sign
|
||||
// This matches Electron's behavior of checking status and triggering manually
|
||||
val eventData = SessionEventData(
|
||||
eventId = "polling_trigger_${System.currentTimeMillis()}",
|
||||
eventType = "session_started",
|
||||
sessionId = sessionId,
|
||||
thresholdN = status.thresholdN,
|
||||
thresholdT = status.thresholdT,
|
||||
selectedParties = status.participants.map { it.partyId },
|
||||
joinTokens = emptyMap(),
|
||||
messageHash = null
|
||||
)
|
||||
|
||||
// Invoke the session event callback on main thread
|
||||
withContext(Dispatchers.Main) {
|
||||
sessionEventCallback?.invoke(eventData)
|
||||
}
|
||||
|
||||
// Stop polling after triggering
|
||||
android.util.Log.d("TssRepository", "[POLLING] Triggered $sessionType, stopping polling")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Check if session failed or was cancelled
|
||||
if (status.status == "failed" || status.status == "cancelled") {
|
||||
android.util.Log.d("TssRepository", "[POLLING] Session ${status.status}, stopping polling")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Check if session already completed (keygen finished)
|
||||
if (status.status == "completed") {
|
||||
android.util.Log.d("TssRepository", "[POLLING] Session completed, stopping polling")
|
||||
return@launch
|
||||
}
|
||||
},
|
||||
onFailure = { e ->
|
||||
android.util.Log.w("TssRepository", "[POLLING] Failed to get session status: ${e.message}")
|
||||
// Continue polling on failure (network hiccup, etc.)
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.w("TssRepository", "[POLLING] Exception during polling: ${e.message}")
|
||||
}
|
||||
|
||||
// Wait before next poll
|
||||
delay(POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
// Timeout reached - keygen didn't start within 5 minutes
|
||||
if (isActive) {
|
||||
val elapsedSeconds = (System.currentTimeMillis() - startTime) / 1000
|
||||
android.util.Log.e("TssRepository", "[POLLING] Timeout after ${elapsedSeconds}s waiting for $sessionType to start")
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
keygenTimeoutCallback?.invoke("等待 $sessionType 启动超时(5分钟)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop session status polling
|
||||
* Called when:
|
||||
* - Session starts successfully (keygen/sign triggered)
|
||||
* - Session is cancelled
|
||||
* - User navigates away
|
||||
*/
|
||||
fun stopSessionStatusPolling() {
|
||||
sessionStatusPollingJob?.cancel()
|
||||
sessionStatusPollingJob = null
|
||||
android.util.Log.d("TssRepository", "[POLLING] Session status polling stopped")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get share count for startup check
|
||||
*/
|
||||
|
|
@ -1536,6 +1668,7 @@ class TssRepository @Inject constructor(
|
|||
tssNativeBridge.cancelSession()
|
||||
messageCollectionJob?.cancel()
|
||||
sessionEventJob?.cancel()
|
||||
stopSessionStatusPolling() // Stop polling when session is cancelled
|
||||
_currentSession.value = null
|
||||
_sessionStatus.value = SessionStatus.WAITING
|
||||
|
||||
|
|
@ -1553,6 +1686,7 @@ class TssRepository @Inject constructor(
|
|||
*/
|
||||
fun resetSessionStatus() {
|
||||
_sessionStatus.value = SessionStatus.WAITING
|
||||
stopSessionStatusPolling() // Stop polling when resetting session state
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -285,6 +285,13 @@ class MainViewModel @Inject constructor(
|
|||
*/
|
||||
private fun setupSessionEventCallback() {
|
||||
android.util.Log.d("MainViewModel", "Setting up session event callback")
|
||||
|
||||
// Setup keygen timeout callback (matching Electron's 5-minute timeout in checkAndTriggerKeygen)
|
||||
repository.setKeygenTimeoutCallback { errorMessage ->
|
||||
android.util.Log.e("MainViewModel", "Keygen timeout: $errorMessage")
|
||||
_uiState.update { it.copy(isLoading = false, error = errorMessage) }
|
||||
}
|
||||
|
||||
repository.setSessionEventCallback { event ->
|
||||
android.util.Log.d("MainViewModel", "=== MainViewModel received session event ===")
|
||||
android.util.Log.d("MainViewModel", " eventType: ${event.eventType}")
|
||||
|
|
@ -360,7 +367,32 @@ class MainViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
"all_joined" -> {
|
||||
android.util.Log.d("MainViewModel", "All parties joined, protocol will start soon")
|
||||
android.util.Log.d("MainViewModel", "All parties joined, starting session status polling as fallback")
|
||||
|
||||
// Start polling as fallback mechanism (matching Electron's checkAndTriggerKeygen)
|
||||
// This handles the case where gRPC stream disconnects (e.g., app goes to background)
|
||||
// and session_started event is missed
|
||||
val currentSessionId = _currentSessionId.value
|
||||
val joinKeygenInfo = pendingJoinKeygenInfo
|
||||
val joinSignInfo = pendingJoinSignInfo
|
||||
|
||||
when {
|
||||
// Initiator keygen session
|
||||
currentSessionId != null && event.sessionId == currentSessionId -> {
|
||||
android.util.Log.d("MainViewModel", "Starting polling for initiator keygen session: $currentSessionId")
|
||||
repository.startSessionStatusPolling(currentSessionId, "keygen")
|
||||
}
|
||||
// Joiner keygen session
|
||||
joinKeygenInfo != null && event.sessionId == joinKeygenInfo.sessionId -> {
|
||||
android.util.Log.d("MainViewModel", "Starting polling for joiner keygen session: ${joinKeygenInfo.sessionId}")
|
||||
repository.startSessionStatusPolling(joinKeygenInfo.sessionId, "keygen")
|
||||
}
|
||||
// Sign session
|
||||
joinSignInfo != null && event.sessionId == joinSignInfo.sessionId -> {
|
||||
android.util.Log.d("MainViewModel", "Starting polling for sign session: ${joinSignInfo.sessionId}")
|
||||
repository.startSessionStatusPolling(joinSignInfo.sessionId, "sign")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue