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
|
// Android gRPC streams can disconnect when app goes to background, so we poll as backup
|
||||||
private var sessionStatusPollingJob: Job? = null
|
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
|
* Get the current party ID
|
||||||
* Note: This will throw UninitializedPropertyAccessException if called before registerParty()
|
* Note: This will throw UninitializedPropertyAccessException if called before registerParty()
|
||||||
|
|
@ -244,6 +255,127 @@ class TssRepository @Inject constructor(
|
||||||
sessionEventCallback = callback
|
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
|
* Get share count for startup check
|
||||||
*/
|
*/
|
||||||
|
|
@ -1536,6 +1668,7 @@ class TssRepository @Inject constructor(
|
||||||
tssNativeBridge.cancelSession()
|
tssNativeBridge.cancelSession()
|
||||||
messageCollectionJob?.cancel()
|
messageCollectionJob?.cancel()
|
||||||
sessionEventJob?.cancel()
|
sessionEventJob?.cancel()
|
||||||
|
stopSessionStatusPolling() // Stop polling when session is cancelled
|
||||||
_currentSession.value = null
|
_currentSession.value = null
|
||||||
_sessionStatus.value = SessionStatus.WAITING
|
_sessionStatus.value = SessionStatus.WAITING
|
||||||
|
|
||||||
|
|
@ -1553,6 +1686,7 @@ class TssRepository @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun resetSessionStatus() {
|
fun resetSessionStatus() {
|
||||||
_sessionStatus.value = SessionStatus.WAITING
|
_sessionStatus.value = SessionStatus.WAITING
|
||||||
|
stopSessionStatusPolling() // Stop polling when resetting session state
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,13 @@ class MainViewModel @Inject constructor(
|
||||||
*/
|
*/
|
||||||
private fun setupSessionEventCallback() {
|
private fun setupSessionEventCallback() {
|
||||||
android.util.Log.d("MainViewModel", "Setting up session event callback")
|
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 ->
|
repository.setSessionEventCallback { event ->
|
||||||
android.util.Log.d("MainViewModel", "=== MainViewModel received session event ===")
|
android.util.Log.d("MainViewModel", "=== MainViewModel received session event ===")
|
||||||
android.util.Log.d("MainViewModel", " eventType: ${event.eventType}")
|
android.util.Log.d("MainViewModel", " eventType: ${event.eventType}")
|
||||||
|
|
@ -360,7 +367,32 @@ class MainViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"all_joined" -> {
|
"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