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
|
package com.durian.tssparty
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
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
|
@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
|
* Trigger reconnection with exponential backoff
|
||||||
*/
|
*/
|
||||||
private fun triggerReconnect(reason: String) {
|
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)) {
|
if (!shouldReconnect.get() || isReconnecting.getAndSet(true)) {
|
||||||
|
Log.d(TAG, "[IDLE_CRASH_DEBUG] triggerReconnect skipped (already reconnecting or disabled)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val host = currentHost
|
val host = currentHost
|
||||||
val port = currentPort
|
val port = currentPort
|
||||||
if (host == null || port == null) {
|
if (host == null || port == null) {
|
||||||
|
Log.d(TAG, "[IDLE_CRASH_DEBUG] triggerReconnect skipped (no host/port)")
|
||||||
isReconnecting.set(false)
|
isReconnecting.set(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Triggering reconnect: $reason")
|
Log.d(TAG, "[IDLE_CRASH_DEBUG] Triggering reconnect to $host:$port")
|
||||||
|
|
||||||
// Emit disconnected event
|
// Emit disconnected event
|
||||||
_connectionEvents.tryEmit(GrpcConnectionEvent.Disconnected(reason))
|
_connectionEvents.tryEmit(GrpcConnectionEvent.Disconnected(reason))
|
||||||
|
|
@ -423,15 +428,18 @@ class GrpcClient @Inject constructor() {
|
||||||
|
|
||||||
private fun handleHeartbeatFailure(reason: String) {
|
private fun handleHeartbeatFailure(reason: String) {
|
||||||
val fails = heartbeatFailCount.incrementAndGet()
|
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) {
|
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")
|
triggerReconnect("Heartbeat failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopHeartbeat() {
|
private fun stopHeartbeat() {
|
||||||
|
Log.d(TAG, "[IDLE_CRASH_DEBUG] stopHeartbeat called")
|
||||||
heartbeatJob?.cancel()
|
heartbeatJob?.cancel()
|
||||||
heartbeatJob = null
|
heartbeatJob = null
|
||||||
heartbeatFailCount.set(0)
|
heartbeatFailCount.set(0)
|
||||||
|
|
@ -475,23 +483,28 @@ class GrpcClient @Inject constructor() {
|
||||||
* Notifies the repository layer to re-establish message/event subscriptions
|
* Notifies the repository layer to re-establish message/event subscriptions
|
||||||
*/
|
*/
|
||||||
private fun reSubscribeStreams() {
|
private fun reSubscribeStreams() {
|
||||||
|
Log.d(TAG, "[IDLE_CRASH_DEBUG] reSubscribeStreams called")
|
||||||
val needsResubscribe = eventStreamSubscribed.get() || activeMessageSubscription != null
|
val needsResubscribe = eventStreamSubscribed.get() || activeMessageSubscription != null
|
||||||
|
|
||||||
if (needsResubscribe) {
|
if (needsResubscribe) {
|
||||||
Log.d(TAG, "Triggering stream re-subscription callback")
|
Log.d(TAG, "[IDLE_CRASH_DEBUG] Triggering stream re-subscription callback")
|
||||||
Log.d(TAG, " - Event stream: ${eventStreamSubscribed.get()}, partyId: $eventStreamPartyId")
|
Log.d(TAG, "[IDLE_CRASH_DEBUG] - Event stream: ${eventStreamSubscribed.get()}, partyId: $eventStreamPartyId")
|
||||||
Log.d(TAG, " - Message stream: ${activeMessageSubscription?.sessionId}")
|
Log.d(TAG, "[IDLE_CRASH_DEBUG] - Message stream: ${activeMessageSubscription?.sessionId}")
|
||||||
|
|
||||||
// Notify repository to re-establish streams
|
// Notify repository to re-establish streams
|
||||||
scope.launch {
|
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
|
// Wait for channel to be fully ready instead of fixed delay
|
||||||
if (waitForChannelReady()) {
|
if (waitForChannelReady()) {
|
||||||
|
Log.d(TAG, "[IDLE_CRASH_DEBUG] Channel ready, invoking reconnect callback")
|
||||||
try {
|
try {
|
||||||
onReconnectedCallback?.invoke()
|
onReconnectedCallback?.invoke()
|
||||||
|
Log.d(TAG, "[IDLE_CRASH_DEBUG] Reconnect callback completed")
|
||||||
// Emit reconnected event
|
// Emit reconnected event
|
||||||
_connectionEvents.tryEmit(GrpcConnectionEvent.Reconnected)
|
_connectionEvents.tryEmit(GrpcConnectionEvent.Reconnected)
|
||||||
} catch (e: Exception) {
|
} 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
|
// Don't let callback failure affect the connection state
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -548,8 +548,15 @@ class TssRepository @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
// Invoke the session event callback on main thread
|
// Invoke the session event callback on main thread
|
||||||
withContext(Dispatchers.Main) {
|
try {
|
||||||
sessionEventCallback?.invoke(eventData)
|
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
|
// Stop polling after triggering
|
||||||
|
|
@ -591,12 +598,22 @@ class TssRepository @Inject constructor(
|
||||||
// Timeout reached - keygen didn't start within 5 minutes
|
// Timeout reached - keygen didn't start within 5 minutes
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
val elapsedSeconds = (System.currentTimeMillis() - startTime) / 1000
|
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) {
|
try {
|
||||||
countdownTickCallback?.invoke(0L) // Countdown reached zero
|
withContext(Dispatchers.Main) {
|
||||||
keygenTimeoutCallback?.invoke("等待 $sessionType 启动超时(5分钟)")
|
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)
|
// Setup keygen timeout callback (matching Electron's 5-minute timeout in checkAndTriggerKeygen)
|
||||||
repository.setKeygenTimeoutCallback { errorMessage ->
|
repository.setKeygenTimeoutCallback { errorMessage ->
|
||||||
android.util.Log.e("MainViewModel", "Keygen timeout: $errorMessage")
|
android.util.Log.e("MainViewModel", "[IDLE_CRASH_DEBUG] Keygen timeout callback invoked: $errorMessage")
|
||||||
_uiState.update { it.copy(isLoading = false, error = errorMessage, countdownSeconds = -1L) }
|
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
|
// Setup countdown tick callback for UI countdown display
|
||||||
repository.setCountdownTickCallback { remainingSeconds ->
|
repository.setCountdownTickCallback { remainingSeconds ->
|
||||||
android.util.Log.d("MainViewModel", "Countdown tick: $remainingSeconds seconds remaining")
|
try {
|
||||||
_uiState.update { it.copy(countdownSeconds = remainingSeconds) }
|
_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
|
// Setup progress callback for real-time round updates from native TSS bridge
|
||||||
|
|
@ -333,14 +342,15 @@ class MainViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
repository.setSessionEventCallback { event ->
|
repository.setSessionEventCallback { event ->
|
||||||
android.util.Log.d("MainViewModel", "=== MainViewModel received session event ===")
|
try {
|
||||||
android.util.Log.d("MainViewModel", " eventType: ${event.eventType}")
|
android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] === MainViewModel received session event ===")
|
||||||
android.util.Log.d("MainViewModel", " sessionId: ${event.sessionId}")
|
android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] eventType: ${event.eventType}")
|
||||||
android.util.Log.d("MainViewModel", " _currentSessionId: ${_currentSessionId.value}")
|
android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] sessionId: ${event.sessionId}")
|
||||||
android.util.Log.d("MainViewModel", " pendingJoinKeygenInfo?.sessionId: ${pendingJoinKeygenInfo?.sessionId}")
|
android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] _currentSessionId: ${_currentSessionId.value}")
|
||||||
android.util.Log.d("MainViewModel", " pendingJoinSignInfo?.sessionId: ${pendingJoinSignInfo?.sessionId}")
|
android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] pendingJoinKeygenInfo?.sessionId: ${pendingJoinKeygenInfo?.sessionId}")
|
||||||
android.util.Log.d("MainViewModel", " _signSessionId: ${_signSessionId.value}")
|
android.util.Log.d("MainViewModel", "[IDLE_CRASH_DEBUG] pendingJoinSignInfo?.sessionId: ${pendingJoinSignInfo?.sessionId}")
|
||||||
android.util.Log.d("MainViewModel", " pendingSignInitiatorInfo?.sessionId: ${pendingSignInitiatorInfo?.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) {
|
when (event.eventType) {
|
||||||
"session_started" -> {
|
"session_started" -> {
|
||||||
|
|
@ -348,7 +358,7 @@ class MainViewModel @Inject constructor(
|
||||||
// participant_joined events from adding duplicates. This must be the
|
// participant_joined events from adding duplicates. This must be the
|
||||||
// first line before any async operations.
|
// first line before any async operations.
|
||||||
sessionStartedForSession = event.sessionId
|
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)
|
// Check if this is for keygen initiator (CreateWallet)
|
||||||
val currentSessionId = _currentSessionId.value
|
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