feat: 应用崩溃页面

This commit is contained in:
Super12138 2025-01-14 22:14:38 +08:00
parent d38a9d3357
commit 9861c468f8
7 changed files with 199 additions and 0 deletions

View file

@ -24,6 +24,9 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.activities.CrashActivity"
android:exported="false" />
</application>
</manifest>

View file

@ -2,6 +2,7 @@ package cn.super12138.todo
import android.app.Application
import cn.super12138.todo.logic.database.TodoDatabase
import cn.super12138.todo.ui.pages.crash.CrashHandler
class TodoApp : Application() {
private val database by lazy { TodoDatabase.getDatabase(this) }
@ -12,6 +13,10 @@ class TodoApp : Application() {
override fun onCreate() {
super.onCreate()
db = database
val crashHandler = CrashHandler(applicationContext)
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
}
}

View file

@ -0,0 +1,66 @@
package cn.super12138.todo.ui.activities
import android.os.Build
import android.os.Bundle
import android.view.HapticFeedbackConstants
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import cn.super12138.todo.R
import cn.super12138.todo.ui.pages.crash.CrashPage
import cn.super12138.todo.ui.theme.ToDoTheme
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class CrashActivity : ComponentActivity() {
companion object {
const val BRAND_PREFIX = "Brand: "
const val MODEL_PREFIX = "Model: "
const val DEVICE_SDK_PREFIX = "Device SDK: "
const val CRASH_TIME_PREFIX = "Crash time: "
const val BEGINNING_CRASH = "======beginning of crash======"
}
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
val crashLogs = intent.getStringExtra("crash_logs")
val deviceBrand = Build.BRAND
val deviceModel = Build.MODEL
val sdkLevel = Build.VERSION.SDK_INT
val currentDateTime = Calendar.getInstance().time
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
val formattedDateTime = formatter.format(currentDateTime)
val deviceInfo = StringBuilder().apply {
append(BRAND_PREFIX).append("").append(deviceBrand).append('\n')
append(MODEL_PREFIX).append(deviceModel).append('\n')
append(DEVICE_SDK_PREFIX).append(sdkLevel).append('\n').append('\n')
append(CRASH_TIME_PREFIX).append(formattedDateTime).append('\n').append('\n')
append(BEGINNING_CRASH).append('\n')
}
val crashLogFormatted = StringBuilder().apply {
append(deviceInfo)
append(crashLogs)
}.toString()
setContent {
ToDoTheme {
val view = LocalView.current
CrashPage(
crashLog = if (crashLogs == null) stringResource(R.string.tip_no_crash_logs) else crashLogFormatted,
exitApp = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
finishAffinity()
}
)
}
}
}
}

View file

@ -0,0 +1,28 @@
package cn.super12138.todo.ui.pages.crash
import android.content.Context
import android.content.Intent
import android.os.Process
import cn.super12138.todo.ui.activities.CrashActivity
import kotlin.system.exitProcess
class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandler {
private val defaultUEH = Thread.getDefaultUncaughtExceptionHandler()
override fun uncaughtException(thread: Thread, ex: Throwable) {
val stackTrace = ex.stackTraceToString()
// 启动新的 Activity 来显示崩溃日志
val intent = Intent(context, CrashActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
putExtra("crash_logs", stackTrace)
}
context.startActivity(intent)
// 传递异常给默认的异常处理器
defaultUEH?.uncaughtException(thread, ex)
// 杀掉崩溃的应用程序进程
Process.killProcess(Process.myPid())
exitProcess(10)
}
}

View file

@ -0,0 +1,91 @@
package cn.super12138.todo.ui.pages.crash
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ExitToApp
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import cn.super12138.todo.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CrashPage(
crashLog: String,
exitApp: () -> Unit,
modifier: Modifier = Modifier
) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
modifier = modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = {
Text(
text = stringResource(R.string.page_crash),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
scrollBehavior = scrollBehavior
)
},
floatingActionButton = {
ExtendedFloatingActionButton(
onClick = exitApp,
icon = {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ExitToApp,
contentDescription = stringResource(R.string.action_exit_app)
)
},
text = {
Text(
text = stringResource(R.string.action_exit_app)
)
},
)
},
contentWindowInsets = WindowInsets.safeContent
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.padding(horizontal = 16.dp)
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
Spacer(Modifier.height(5.dp))
SelectionContainer {
Text(
text = crashLog,
fontFamily = FontFamily.Monospace,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}

View file

@ -22,4 +22,7 @@
<string name="subject_others">其它</string>
<string name="title_edit_task">修改待办</string>
<string name="tip_remain_tasks">剩余 %s 项任务</string>
<string name="page_crash">应用程序出现错误</string>
<string name="tip_no_crash_logs">没有日志传入</string>
<string name="action_exit_app">退出应用</string>
</resources>

View file

@ -21,4 +21,7 @@
<string name="subject_others">Others</string>
<string name="title_edit_task">Edit Task</string>
<string name="tip_remain_tasks">%s tasks remaining</string>
<string name="page_crash">Oops! App went wrong</string>
<string name="tip_no_crash_logs">No crash logs</string>
<string name="action_exit_app">Exit app</string>
</resources>