From d83c85996563ca0fecc6e632f1f4815e54b87c7b Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 27 Jan 2026 09:36:00 -0800 Subject: [PATCH] =?UTF-8?q?debug(android):=20=E6=B7=BB=E5=8A=A0=E5=B4=A9?= =?UTF-8?q?=E6=BA=83=E6=97=A5=E5=BF=97=E5=92=8C=E8=B0=83=E8=AF=95=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=AE=9A=E4=BD=8D=E5=BE=85=E6=9C=BA=E9=97=AA=E9=80=80?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TssPartyApplication: 添加全局异常捕获,崩溃日志保存到文件 - GrpcClient: 心跳失败、重连、流重订阅添加 [IDLE_CRASH_DEBUG] 日志 - TssRepository: 轮询超时和回调调用添加调试日志 - MainViewModel: session事件回调用try-catch包装 日志筛选: adb logcat | grep "IDLE_CRASH_DEBUG" 崩溃日志: /data/data/com.durian.tssparty/files/crash_logs/ Co-Authored-By: Claude Opus 4.5 --- .../durian/tssparty/TssPartyApplication.kt | 82 ++++++++++++++++++- .../durian/tssparty/data/remote/GrpcClient.kt | 27 ++++-- .../tssparty/data/repository/TssRepository.kt | 29 +++++-- .../presentation/viewmodel/MainViewModel.kt | 42 +++++++--- 4 files changed, 153 insertions(+), 27 deletions(-) diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/TssPartyApplication.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/TssPartyApplication.kt index 142582d2..872e58cb 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/TssPartyApplication.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/TssPartyApplication.kt @@ -1,7 +1,87 @@ package com.durian.tssparty import android.app.Application +import android.util.Log import dagger.hilt.android.HiltAndroidApp +import java.io.File +import java.io.PrintWriter +import java.io.StringWriter +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale @HiltAndroidApp -class TssPartyApplication : Application() +class TssPartyApplication : Application() { + + companion object { + private const val TAG = "TssPartyApplication" + } + + private var defaultExceptionHandler: Thread.UncaughtExceptionHandler? = null + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "Application onCreate") + + // Set up global exception handler + setupCrashHandler() + } + + private fun setupCrashHandler() { + defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() + + Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> + Log.e(TAG, "=== UNCAUGHT EXCEPTION ===") + Log.e(TAG, "Thread: ${thread.name}") + Log.e(TAG, "Exception: ${throwable.javaClass.simpleName}") + Log.e(TAG, "Message: ${throwable.message}") + + // Get full stack trace + val sw = StringWriter() + throwable.printStackTrace(PrintWriter(sw)) + val stackTrace = sw.toString() + Log.e(TAG, "Stack trace:\n$stackTrace") + + // Try to save crash log to file + try { + saveCrashLog(thread, throwable, stackTrace) + } catch (e: Exception) { + Log.e(TAG, "Failed to save crash log: ${e.message}") + } + + // Call the default handler + defaultExceptionHandler?.uncaughtException(thread, throwable) + } + + Log.d(TAG, "Crash handler installed") + } + + private fun saveCrashLog(thread: Thread, throwable: Throwable, stackTrace: String) { + val crashDir = File(filesDir, "crash_logs") + if (!crashDir.exists()) { + crashDir.mkdirs() + } + + val dateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault()) + val timestamp = dateFormat.format(Date()) + val crashFile = File(crashDir, "crash_$timestamp.txt") + + crashFile.writeText(buildString { + appendLine("=== TSS Party Crash Report ===") + appendLine("Time: $timestamp") + appendLine("Thread: ${thread.name}") + appendLine("Exception: ${throwable.javaClass.name}") + appendLine("Message: ${throwable.message}") + appendLine() + appendLine("=== Stack Trace ===") + appendLine(stackTrace) + appendLine() + appendLine("=== Device Info ===") + appendLine("Android Version: ${android.os.Build.VERSION.RELEASE}") + appendLine("SDK: ${android.os.Build.VERSION.SDK_INT}") + appendLine("Device: ${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}") + }) + + Log.d(TAG, "Crash log saved to: ${crashFile.absolutePath}") + } +} diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/remote/GrpcClient.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/remote/GrpcClient.kt index 7d372fdb..86da13d5 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/remote/GrpcClient.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/remote/GrpcClient.kt @@ -332,18 +332,23 @@ class GrpcClient @Inject constructor() { * Trigger reconnection with exponential backoff */ private fun triggerReconnect(reason: String) { + Log.d(TAG, "[IDLE_CRASH_DEBUG] triggerReconnect called: $reason") + Log.d(TAG, "[IDLE_CRASH_DEBUG] shouldReconnect=${shouldReconnect.get()}, isReconnecting=${isReconnecting.get()}") + if (!shouldReconnect.get() || isReconnecting.getAndSet(true)) { + Log.d(TAG, "[IDLE_CRASH_DEBUG] triggerReconnect skipped (already reconnecting or disabled)") return } val host = currentHost val port = currentPort if (host == null || port == null) { + Log.d(TAG, "[IDLE_CRASH_DEBUG] triggerReconnect skipped (no host/port)") isReconnecting.set(false) return } - Log.d(TAG, "Triggering reconnect: $reason") + Log.d(TAG, "[IDLE_CRASH_DEBUG] Triggering reconnect to $host:$port") // Emit disconnected event _connectionEvents.tryEmit(GrpcConnectionEvent.Disconnected(reason)) @@ -423,15 +428,18 @@ class GrpcClient @Inject constructor() { private fun handleHeartbeatFailure(reason: String) { val fails = heartbeatFailCount.incrementAndGet() - Log.w(TAG, "Heartbeat failed ($fails/$MAX_HEARTBEAT_FAILS): $reason") + Log.w(TAG, "[IDLE_CRASH_DEBUG] Heartbeat failed ($fails/$MAX_HEARTBEAT_FAILS): $reason") + Log.w(TAG, "[IDLE_CRASH_DEBUG] Connection state: ${_connectionState.value}") + Log.w(TAG, "[IDLE_CRASH_DEBUG] Channel state: ${channel?.getState(false)}") if (fails >= MAX_HEARTBEAT_FAILS) { - Log.e(TAG, "Too many heartbeat failures, triggering reconnect") + Log.e(TAG, "[IDLE_CRASH_DEBUG] Too many heartbeat failures, triggering reconnect") triggerReconnect("Heartbeat failed") } } private fun stopHeartbeat() { + Log.d(TAG, "[IDLE_CRASH_DEBUG] stopHeartbeat called") heartbeatJob?.cancel() heartbeatJob = null heartbeatFailCount.set(0) @@ -475,23 +483,28 @@ class GrpcClient @Inject constructor() { * Notifies the repository layer to re-establish message/event subscriptions */ private fun reSubscribeStreams() { + Log.d(TAG, "[IDLE_CRASH_DEBUG] reSubscribeStreams called") val needsResubscribe = eventStreamSubscribed.get() || activeMessageSubscription != null if (needsResubscribe) { - Log.d(TAG, "Triggering stream re-subscription callback") - Log.d(TAG, " - Event stream: ${eventStreamSubscribed.get()}, partyId: $eventStreamPartyId") - Log.d(TAG, " - Message stream: ${activeMessageSubscription?.sessionId}") + Log.d(TAG, "[IDLE_CRASH_DEBUG] Triggering stream re-subscription callback") + Log.d(TAG, "[IDLE_CRASH_DEBUG] - Event stream: ${eventStreamSubscribed.get()}, partyId: $eventStreamPartyId") + Log.d(TAG, "[IDLE_CRASH_DEBUG] - Message stream: ${activeMessageSubscription?.sessionId}") // Notify repository to re-establish streams scope.launch { + Log.d(TAG, "[IDLE_CRASH_DEBUG] Waiting for channel to be ready...") // Wait for channel to be fully ready instead of fixed delay if (waitForChannelReady()) { + Log.d(TAG, "[IDLE_CRASH_DEBUG] Channel ready, invoking reconnect callback") try { onReconnectedCallback?.invoke() + Log.d(TAG, "[IDLE_CRASH_DEBUG] Reconnect callback completed") // Emit reconnected event _connectionEvents.tryEmit(GrpcConnectionEvent.Reconnected) } catch (e: Exception) { - Log.e(TAG, "Reconnect callback failed: ${e.message}") + Log.e(TAG, "[IDLE_CRASH_DEBUG] Reconnect callback failed: ${e.message}") + Log.e(TAG, "[IDLE_CRASH_DEBUG] Stack trace: ${e.stackTraceToString()}") // Don't let callback failure affect the connection state } } else { diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt index eeb3c81f..4cedbdca 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt @@ -548,8 +548,15 @@ class TssRepository @Inject constructor( ) // Invoke the session event callback on main thread - withContext(Dispatchers.Main) { - sessionEventCallback?.invoke(eventData) + try { + withContext(Dispatchers.Main) { + android.util.Log.d("TssRepository", "[IDLE_CRASH_DEBUG] Invoking sessionEventCallback on main thread") + sessionEventCallback?.invoke(eventData) + android.util.Log.d("TssRepository", "[IDLE_CRASH_DEBUG] sessionEventCallback completed") + } + } catch (e: Exception) { + android.util.Log.e("TssRepository", "[IDLE_CRASH_DEBUG] Exception in sessionEventCallback: ${e.message}") + android.util.Log.e("TssRepository", "[IDLE_CRASH_DEBUG] Stack: ${e.stackTraceToString()}") } // Stop polling after triggering @@ -591,12 +598,22 @@ class TssRepository @Inject constructor( // 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") + android.util.Log.e("TssRepository", "[IDLE_CRASH_DEBUG] [POLLING] Timeout after ${elapsedSeconds}s waiting for $sessionType to start") - withContext(Dispatchers.Main) { - countdownTickCallback?.invoke(0L) // Countdown reached zero - keygenTimeoutCallback?.invoke("等待 $sessionType 启动超时(5分钟)") + try { + withContext(Dispatchers.Main) { + android.util.Log.d("TssRepository", "[IDLE_CRASH_DEBUG] Invoking timeout callbacks on main thread") + countdownTickCallback?.invoke(0L) // Countdown reached zero + android.util.Log.d("TssRepository", "[IDLE_CRASH_DEBUG] countdownTickCallback completed") + keygenTimeoutCallback?.invoke("等待 $sessionType 启动超时(5分钟)") + android.util.Log.d("TssRepository", "[IDLE_CRASH_DEBUG] keygenTimeoutCallback completed") + } + } catch (e: Exception) { + android.util.Log.e("TssRepository", "[IDLE_CRASH_DEBUG] Exception in timeout callback: ${e.message}") + android.util.Log.e("TssRepository", "[IDLE_CRASH_DEBUG] Stack: ${e.stackTraceToString()}") } + } else { + android.util.Log.d("TssRepository", "[IDLE_CRASH_DEBUG] Polling job cancelled before timeout") } } } diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt index 15643434..9f4fb0cc 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt @@ -296,14 +296,23 @@ class MainViewModel @Inject constructor( // 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, countdownSeconds = -1L) } + android.util.Log.e("MainViewModel", "[IDLE_CRASH_DEBUG] Keygen timeout callback invoked: $errorMessage") + try { + _uiState.update { it.copy(isLoading = false, error = errorMessage, countdownSeconds = -1L) } + android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] Keygen timeout callback completed") + } catch (e: Exception) { + android.util.Log.e("MainViewModel", "[IDLE_CRASH_DEBUG] Exception in keygen timeout callback: ${e.message}") + android.util.Log.e("MainViewModel", "[IDLE_CRASH_DEBUG] Stack: ${e.stackTraceToString()}") + } } // Setup countdown tick callback for UI countdown display repository.setCountdownTickCallback { remainingSeconds -> - android.util.Log.d("MainViewModel", "Countdown tick: $remainingSeconds seconds remaining") - _uiState.update { it.copy(countdownSeconds = remainingSeconds) } + try { + _uiState.update { it.copy(countdownSeconds = remainingSeconds) } + } catch (e: Exception) { + android.util.Log.e("MainViewModel", "[IDLE_CRASH_DEBUG] Exception in countdown tick callback: ${e.message}") + } } // Setup progress callback for real-time round updates from native TSS bridge @@ -333,14 +342,15 @@ class MainViewModel @Inject constructor( } repository.setSessionEventCallback { event -> - android.util.Log.d("MainViewModel", "=== MainViewModel received session event ===") - android.util.Log.d("MainViewModel", " eventType: ${event.eventType}") - android.util.Log.d("MainViewModel", " sessionId: ${event.sessionId}") - android.util.Log.d("MainViewModel", " _currentSessionId: ${_currentSessionId.value}") - android.util.Log.d("MainViewModel", " pendingJoinKeygenInfo?.sessionId: ${pendingJoinKeygenInfo?.sessionId}") - android.util.Log.d("MainViewModel", " pendingJoinSignInfo?.sessionId: ${pendingJoinSignInfo?.sessionId}") - android.util.Log.d("MainViewModel", " _signSessionId: ${_signSessionId.value}") - android.util.Log.d("MainViewModel", " pendingSignInitiatorInfo?.sessionId: ${pendingSignInitiatorInfo?.sessionId}") + try { + android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] === MainViewModel received session event ===") + android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] eventType: ${event.eventType}") + android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] sessionId: ${event.sessionId}") + android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] _currentSessionId: ${_currentSessionId.value}") + android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] pendingJoinKeygenInfo?.sessionId: ${pendingJoinKeygenInfo?.sessionId}") + android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] pendingJoinSignInfo?.sessionId: ${pendingJoinSignInfo?.sessionId}") + android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] _signSessionId: ${_signSessionId.value}") + android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] pendingSignInitiatorInfo?.sessionId: ${pendingSignInitiatorInfo?.sessionId}") when (event.eventType) { "session_started" -> { @@ -348,7 +358,7 @@ class MainViewModel @Inject constructor( // participant_joined events from adding duplicates. This must be the // first line before any async operations. sessionStartedForSession = event.sessionId - android.util.Log.d("MainViewModel", "Session started flag set for: ${event.sessionId}") + android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] Session started flag set for: ${event.sessionId}") // Check if this is for keygen initiator (CreateWallet) val currentSessionId = _currentSessionId.value @@ -496,6 +506,12 @@ class MainViewModel @Inject constructor( } } } + } catch (e: Exception) { + android.util.Log.e("MainViewModel", "[IDLE_CRASH_DEBUG] Exception in session event callback!") + android.util.Log.e("MainViewModel", "[IDLE_CRASH_DEBUG] Event: ${event.eventType}, sessionId: ${event.sessionId}") + android.util.Log.e("MainViewModel", "[IDLE_CRASH_DEBUG] Exception: ${e.javaClass.simpleName}: ${e.message}") + android.util.Log.e("MainViewModel", "[IDLE_CRASH_DEBUG] Stack: ${e.stackTraceToString()}") + } } }