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:
parent
b4541129aa
commit
d83c859965
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue