debug(android): 添加崩溃日志和调试信息定位待机闪退问题

- 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 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-27 09:36:00 -08:00
parent b4541129aa
commit d83c859965
4 changed files with 153 additions and 27 deletions

View File

@ -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}")
}
}

View File

@ -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 {

View File

@ -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")
}
}
}

View File

@ -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()}")
}
}
}