[优化] 新增崩溃包报告页面(因为崩溃太多了孩子们

This commit is contained in:
HapeLee
2026-04-29 20:01:19 +08:00
parent a191d80e02
commit bc670629b4
8 changed files with 209 additions and 8 deletions
+5
View File
@@ -222,6 +222,11 @@
android:name=".ui.about.AboutActivity"
android:enableOnBackInvokedCallback="true"
android:launchMode="singleTask" />
<activity
android:name=".ui.about.CrashReportActivity"
android:enableOnBackInvokedCallback="true"
android:excludeFromRecents="true"
android:launchMode="singleTask" />
<!-- 书源管理 -->
<activity
android:name=".ui.book.source.manage.BookSourceActivity"
@@ -1,11 +1,13 @@
package io.legado.app.help
import android.annotation.SuppressLint
import android.content.Intent
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Debug
import android.os.Looper
import android.os.Process
import android.webkit.WebSettings
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppLog
@@ -28,6 +30,7 @@ import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess
/**
* 异常管理类
@@ -53,8 +56,12 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
Looper.loop()
} else {
ReadAloud.stop(context)
handleException(ex)
mDefaultHandler?.uncaughtException(thread, ex)
if (handleException(ex)) {
Process.killProcess(Process.myPid())
exitProcess(10)
} else {
mDefaultHandler?.uncaughtException(thread, ex)
}
}
}
@@ -73,19 +80,39 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
/**
* 处理该异常
*/
private fun handleException(ex: Throwable?) {
if (ex == null) return
private fun handleException(ex: Throwable?): Boolean {
if (ex == null) return false
LocalConfig.appCrash = true
//保存日志文件
saveCrashInfo2File(ex)
val crashFileName = saveCrashInfo2File(ex)
if ((ex is OutOfMemoryError || ex.cause is OutOfMemoryError) && AppConfig.recordHeapDump) {
doHeapDump()
}
context.longToastOnUiLegacy(ex.stackTraceStr)
Thread.sleep(3000)
return if (startCrashReport(crashFileName)) {
LocalConfig.appCrash = false
true
} else {
context.longToastOnUiLegacy(ex.stackTraceStr)
Thread.sleep(3000)
false
}
}
private fun startCrashReport(fileName: String): Boolean {
return runCatching {
val intent = Intent()
.setClassName(context.packageName, CRASH_REPORT_ACTIVITY)
.putExtra(EXTRA_CRASH_FILE_NAME, fileName)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
}.isSuccess
}
companion object {
const val EXTRA_CRASH_FILE_NAME = "crashFileName"
private const val CRASH_REPORT_ACTIVITY = "io.legado.app.ui.about.CrashReportActivity"
/**
* 存储异常和参数信息
*/
@@ -123,7 +150,7 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
/**
* 保存错误信息到文件中
*/
fun saveCrashInfo2File(ex: Throwable) {
fun saveCrashInfo2File(ex: Throwable): String {
val sb = StringBuilder()
for ((key, value) in paramsMap) {
sb.append(key).append("=").append(value).append("\n")
@@ -165,6 +192,19 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
.writeText(crashLog)
}
}
return fileName
}
fun readCrashLog(fileName: String?): String? {
return runCatching {
val crashDir = appCtx.externalCacheDir?.resolve("crash") ?: return null
val file = fileName
?.let { crashDir.resolve(it) }
?.takeIf { it.isFile }
?: crashDir.listFiles { file -> file.isFile }
?.maxByOrNull { it.name }
file?.readText()
}.getOrNull()
}
/**
@@ -0,0 +1,35 @@
package io.legado.app.ui.about
import android.content.Intent
import androidx.compose.runtime.Composable
import io.legado.app.base.BaseComposeActivity
import io.legado.app.help.CrashHandler
import io.legado.app.ui.main.MainActivity
import io.legado.app.utils.sendToClip
class CrashReportActivity : BaseComposeActivity() {
private val crashText: String by lazy {
CrashHandler.readCrashLog(
intent.getStringExtra(CrashHandler.EXTRA_CRASH_FILE_NAME)
).orEmpty()
}
@Composable
override fun Content() {
CrashReportScreen(
errorText = crashText,
onCopy = { sendToClip(crashText) },
onRestart = { restartApp() },
onClose = { finish() }
)
}
private fun restartApp() {
startActivity(
Intent(this, MainActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
)
finish()
}
}
@@ -0,0 +1,105 @@
package io.legado.app.ui.about
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
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.unit.dp
import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun CrashReportScreen(
errorText: String,
onCopy: () -> Unit,
onRestart: () -> Unit,
onClose: () -> Unit
) {
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
val displayText = errorText.ifBlank {
stringResource(R.string.crash_report_empty)
}
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
GlassMediumFlexibleTopAppBar(
title = stringResource(R.string.crash_report),
navigationIcon = {
TopBarNavigationButton(
onClick = onClose,
imageVector = AppIcons.Close
)
},
scrollBehavior = scrollBehavior
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
AppText(
text = stringResource(R.string.crash_report_message),
style = LegadoTheme.typography.bodyMedium
)
SelectionContainer(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
Text(
text = displayText,
style = LegadoTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
OutlinedButton(
onClick = onRestart,
modifier = Modifier.weight(1f)
) {
Text(text = stringResource(R.string.restart_app))
}
Button(
onClick = onCopy,
enabled = errorText.isNotBlank(),
modifier = Modifier.weight(1f)
) {
Text(text = stringResource(R.string.copy_text))
}
}
}
}
}
@@ -1209,6 +1209,10 @@
<string name="key_page_on_long_press">按键长按翻页</string>
<string name="save_log">保存日志</string>
<string name="create_heap_dump">创建堆转储</string>
<string name="crash_report">崩溃报告</string>
<string name="crash_report_message">应用发生崩溃。报告问题时,请复制以下错误信息。</string>
<string name="crash_report_empty">未找到崩溃信息。</string>
<string name="restart_app">重启应用</string>
<string name="record_heap_dump_s">当应用发生OOM崩溃时保存堆转储</string>
<string name="record_heap_dump_t">记录堆转储</string>
<string name="font_weight_text">字重</string>
@@ -1178,6 +1178,10 @@
<string name="key_page_on_long_press">按键长按翻页</string>
<string name="save_log">保存日志</string>
<string name="create_heap_dump">创建堆转储</string>
<string name="crash_report">崩潰報告</string>
<string name="crash_report_message">應用程式發生崩潰。回報問題時,請複製以下錯誤資訊。</string>
<string name="crash_report_empty">找不到崩潰資訊。</string>
<string name="restart_app">重新啟動應用程式</string>
<string name="record_heap_dump_s">当应用发生OOM崩溃时保存堆转储</string>
<string name="record_heap_dump_t">记录堆转储</string>
<string name="font_weight_text">字重</string>
@@ -1181,6 +1181,10 @@
<string name="key_page_on_long_press">按键长按翻页</string>
<string name="save_log">保存日志</string>
<string name="create_heap_dump">创建堆转储</string>
<string name="crash_report">崩潰報告</string>
<string name="crash_report_message">應用程式發生崩潰。回報問題時,請複製以下錯誤資訊。</string>
<string name="crash_report_empty">找不到崩潰資訊。</string>
<string name="restart_app">重新啟動應用程式</string>
<string name="record_heap_dump_s">当应用发生OOM崩溃时保存堆转储</string>
<string name="record_heap_dump_t">记录堆转储</string>
<string name="font_weight_text">字重</string>
+4
View File
@@ -1212,6 +1212,10 @@
<string name="key_page_on_long_press">Press and Hold the Key to Turn Page</string>
<string name="save_log">Save logs</string>
<string name="create_heap_dump">Create Heap Dump</string>
<string name="crash_report">Crash report</string>
<string name="crash_report_message">The app crashed. Copy the error information below when reporting the issue.</string>
<string name="crash_report_empty">No crash information found.</string>
<string name="restart_app">Restart app</string>
<string name="record_heap_dump_s">Saving a heap dump when the application crashes by OOM</string>
<string name="record_heap_dump_t">Capture heap dump</string>
<string name="font_weight_text">N/B/L</string>