fix(android): enable real-time progress updates for keygen/sign rounds

Connect TssNativeBridge.progress Flow to UI through:
- Add progressCallback in TssRepository with startProgressCollection/stopProgressCollection
- Subscribe to native bridge progress in keygen and sign methods
- Add setProgressCallback in MainViewModel to update appropriate round state
- Progress now flows: Go Native → TssNativeBridge → TssRepository → MainViewModel → UI

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-01 19:45:31 -08:00
parent 3576af0f25
commit fd56de5c00
2 changed files with 94 additions and 0 deletions

View File

@ -67,6 +67,13 @@ class TssRepository @Inject constructor(
// Called every second with remaining seconds for UI countdown display
private var countdownTickCallback: ((Long) -> Unit)? = null
// Progress callback (set by ViewModel)
// Called when TSS protocol progress updates (round/totalRounds)
private var progressCallback: ((Int, Int) -> Unit)? = null
// Job for collecting progress from native bridge
private var progressCollectionJob: Job? = 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)
@ -294,6 +301,45 @@ class TssRepository @Inject constructor(
countdownTickCallback = callback
}
/**
* Set progress callback (called by ViewModel)
* This callback is invoked when the TSS protocol progresses through rounds
* @param callback receives (currentRound, totalRounds)
*/
fun setProgressCallback(callback: (Int, Int) -> Unit) {
progressCallback = callback
}
/**
* Start collecting progress from native bridge
* Called when keygen/sign session starts
*/
private fun startProgressCollection() {
// Cancel any existing progress collection
progressCollectionJob?.cancel()
android.util.Log.d("TssRepository", "[PROGRESS] Starting progress collection from native bridge")
progressCollectionJob = repositoryScope.launch {
tssNativeBridge.progress.collect { (round, totalRounds) ->
android.util.Log.d("TssRepository", "[PROGRESS] Round $round / $totalRounds")
withContext(Dispatchers.Main) {
progressCallback?.invoke(round, totalRounds)
}
}
}
}
/**
* Stop collecting progress from native bridge
* Called when session ends or is cancelled
*/
private fun stopProgressCollection() {
progressCollectionJob?.cancel()
progressCollectionJob = null
android.util.Log.d("TssRepository", "[PROGRESS] Progress collection stopped")
}
/**
* Start polling session status as fallback mechanism
* This matches Electron's checkAndTriggerKeygen() polling behavior:
@ -961,6 +1007,9 @@ class TssRepository @Inject constructor(
return@coroutineScope Result.failure(startResult.exceptionOrNull()!!)
}
// Start collecting progress from native bridge
startProgressCollection()
_sessionStatus.value = SessionStatus.IN_PROGRESS
// Mark ready
@ -969,6 +1018,7 @@ class TssRepository @Inject constructor(
// Wait for keygen result
val keygenResult = tssNativeBridge.waitForKeygenResult(password)
if (keygenResult.isFailure) {
stopProgressCollection()
_sessionStatus.value = SessionStatus.FAILED
return@coroutineScope Result.failure(keygenResult.exceptionOrNull()!!)
}
@ -994,6 +1044,7 @@ class TssRepository @Inject constructor(
// Report completion
grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes)
stopProgressCollection()
_sessionStatus.value = SessionStatus.COMPLETED
pendingSessionId = null // Clear pending session ID on completion
@ -1003,6 +1054,7 @@ class TssRepository @Inject constructor(
} catch (e: Exception) {
android.util.Log.e("TssRepository", "Execute keygen as joiner failed", e)
stopProgressCollection()
_sessionStatus.value = SessionStatus.FAILED
pendingSessionId = null // Clear pending session ID on failure
Result.failure(e)
@ -1156,12 +1208,16 @@ class TssRepository @Inject constructor(
return@coroutineScope Result.failure(startResult.exceptionOrNull()!!)
}
// Start collecting progress from native bridge
startProgressCollection()
// Mark ready
grpcClient.markPartyReady(sessionId, partyId)
// Wait for sign result
val signResult = tssNativeBridge.waitForSignResult()
if (signResult.isFailure) {
stopProgressCollection()
_sessionStatus.value = SessionStatus.FAILED
return@coroutineScope Result.failure(signResult.exceptionOrNull()!!)
}
@ -1172,6 +1228,7 @@ class TssRepository @Inject constructor(
val signatureBytes = android.util.Base64.decode(result.signature, android.util.Base64.NO_WRAP)
grpcClient.reportCompletion(sessionId, partyId, signature = signatureBytes)
stopProgressCollection()
_sessionStatus.value = SessionStatus.COMPLETED
pendingSessionId = null // Clear pending session ID on completion
messageCollectionJob?.cancel()
@ -1182,6 +1239,7 @@ class TssRepository @Inject constructor(
} catch (e: Exception) {
android.util.Log.e("TssRepository", "Execute sign as joiner failed", e)
stopProgressCollection()
_sessionStatus.value = SessionStatus.FAILED
pendingSessionId = null // Clear pending session ID on failure
Result.failure(e)
@ -1672,6 +1730,9 @@ class TssRepository @Inject constructor(
return@coroutineScope Result.failure(startResult.exceptionOrNull()!!)
}
// Start collecting progress from native bridge
startProgressCollection()
_sessionStatus.value = SessionStatus.IN_PROGRESS
// Mark ready
@ -1680,6 +1741,7 @@ class TssRepository @Inject constructor(
// Wait for keygen result
val keygenResult = tssNativeBridge.waitForKeygenResult(password)
if (keygenResult.isFailure) {
stopProgressCollection()
_sessionStatus.value = SessionStatus.FAILED
return@coroutineScope Result.failure(keygenResult.exceptionOrNull()!!)
}
@ -1705,6 +1767,7 @@ class TssRepository @Inject constructor(
// Report completion
grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes)
stopProgressCollection()
_sessionStatus.value = SessionStatus.COMPLETED
pendingSessionId = null // Clear pending session ID on completion
sessionEventJob?.cancel()
@ -1713,6 +1776,7 @@ class TssRepository @Inject constructor(
} catch (e: Exception) {
android.util.Log.e("TssRepository", "Start keygen as initiator failed", e)
stopProgressCollection()
_sessionStatus.value = SessionStatus.FAILED
pendingSessionId = null // Clear pending session ID on failure
Result.failure(e)
@ -2055,6 +2119,9 @@ class TssRepository @Inject constructor(
return@withContext Result.failure(startResult.exceptionOrNull()!!)
}
// Start collecting progress from native bridge
startProgressCollection()
_sessionStatus.value = SessionStatus.IN_PROGRESS
// Note: Message routing is already started in createSignSession after auto-join
@ -2068,6 +2135,7 @@ class TssRepository @Inject constructor(
Result.success(Unit)
} catch (e: Exception) {
stopProgressCollection()
_sessionStatus.value = SessionStatus.FAILED
Result.failure(e)
}
@ -2082,6 +2150,7 @@ class TssRepository @Inject constructor(
try {
val signResult = tssNativeBridge.waitForSignResult()
if (signResult.isFailure) {
stopProgressCollection()
_sessionStatus.value = SessionStatus.FAILED
return@withContext Result.failure(signResult.exceptionOrNull()!!)
}
@ -2095,6 +2164,7 @@ class TssRepository @Inject constructor(
grpcClient.reportCompletion(session.sessionId, partyId, signature = signatureBytes)
}
stopProgressCollection()
_sessionStatus.value = SessionStatus.COMPLETED
messageCollectionJob?.cancel()

View File

@ -298,6 +298,30 @@ class MainViewModel @Inject constructor(
_uiState.update { it.copy(countdownSeconds = remainingSeconds) }
}
// Setup progress callback for real-time round updates from native TSS bridge
repository.setProgressCallback { round, totalRounds ->
android.util.Log.d("MainViewModel", "Progress update: $round / $totalRounds")
// Update the appropriate round state based on which session type is active
when {
// Initiator keygen (CreateWallet)
_currentSessionId.value != null && pendingJoinKeygenInfo == null && pendingJoinSignInfo == null -> {
_currentRound.value = round
}
// Joiner keygen (JoinKeygen)
pendingJoinKeygenInfo != null -> {
_joinKeygenRound.value = round
}
// Joiner sign (CoSign/参与签名)
pendingJoinSignInfo != null -> {
_coSignRound.value = round
}
// Initiator sign (Transfer)
_signSessionId.value != null -> {
_signCurrentRound.value = round
}
}
}
repository.setSessionEventCallback { event ->
android.util.Log.d("MainViewModel", "=== MainViewModel received session event ===")
android.util.Log.d("MainViewModel", " eventType: ${event.eventType}")