feat: 应用崩溃页面
This commit is contained in:
parent
d38a9d3357
commit
9861c468f8
7 changed files with 199 additions and 0 deletions
|
|
@ -24,6 +24,9 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activities.CrashActivity"
|
||||||
|
android:exported="false" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
@ -2,6 +2,7 @@ package cn.super12138.todo
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import cn.super12138.todo.logic.database.TodoDatabase
|
import cn.super12138.todo.logic.database.TodoDatabase
|
||||||
|
import cn.super12138.todo.ui.pages.crash.CrashHandler
|
||||||
|
|
||||||
class TodoApp : Application() {
|
class TodoApp : Application() {
|
||||||
private val database by lazy { TodoDatabase.getDatabase(this) }
|
private val database by lazy { TodoDatabase.getDatabase(this) }
|
||||||
|
|
@ -12,6 +13,10 @@ class TodoApp : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
db = database
|
db = database
|
||||||
|
|
||||||
|
val crashHandler = CrashHandler(applicationContext)
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,4 +22,7 @@
|
||||||
<string name="subject_others">其它</string>
|
<string name="subject_others">其它</string>
|
||||||
<string name="title_edit_task">修改待办</string>
|
<string name="title_edit_task">修改待办</string>
|
||||||
<string name="tip_remain_tasks">剩余 %s 项任务</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>
|
</resources>
|
||||||
|
|
@ -21,4 +21,7 @@
|
||||||
<string name="subject_others">Others</string>
|
<string name="subject_others">Others</string>
|
||||||
<string name="title_edit_task">Edit Task</string>
|
<string name="title_edit_task">Edit Task</string>
|
||||||
<string name="tip_remain_tasks">%s tasks remaining</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>
|
</resources>
|
||||||
Loading…
Reference in a new issue