mirror of
https://github.com/Ashinch/ReadYou.git
synced 2025-02-08 08:08:40 +01:00
feat(ui): add crash report activity to handle uncaught exceptions (#576)
This commit is contained in:
parent
802b14969e
commit
3165aaceba
@ -190,7 +190,7 @@ dependencies {
|
||||
// https://developer.android.com/jetpack/androidx/releases/compose-material
|
||||
implementation "androidx.compose.material:material"
|
||||
implementation "androidx.compose.material:material-icons-extended"
|
||||
debugImplementation "androidx.compose.ui:ui-tooling"
|
||||
implementation "androidx.compose.ui:ui-tooling"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview"
|
||||
|
||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4"
|
||||
|
@ -3,7 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
@ -34,6 +34,11 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".infrastructure.android.CrashReportActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.Reader">
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
|
@ -1,8 +1,10 @@
|
||||
package me.ash.reader.infrastructure.android
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import me.ash.reader.ui.ext.getCurrentVersion
|
||||
import me.ash.reader.ui.ext.showToastLong
|
||||
import java.lang.Thread.UncaughtExceptionHandler
|
||||
|
||||
@ -21,11 +23,10 @@ class CrashHandler(private val context: Context) : UncaughtExceptionHandler {
|
||||
override fun uncaughtException(p0: Thread, p1: Throwable) {
|
||||
val causeMessage = getCauseMessage(p1)
|
||||
Log.e("RLog", "uncaughtException: $causeMessage", p1)
|
||||
Looper.myLooper() ?: Looper.prepare()
|
||||
context.showToastLong(causeMessage)
|
||||
Looper.loop()
|
||||
// android.os.Process.killProcess(android.os.Process.myPid());
|
||||
// exitProcess(1)
|
||||
context.startActivity(Intent(context, CrashReportActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
putExtra(CrashReportActivity.ERROR_REPORT_KEY, p1.stackTraceToString())
|
||||
})
|
||||
}
|
||||
|
||||
private fun getCauseMessage(e: Throwable?): String? {
|
||||
|
@ -0,0 +1,229 @@
|
||||
package me.ash.reader.infrastructure.android
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.BugReport
|
||||
import androidx.compose.material.icons.rounded.ContentCopy
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.ExperimentalTextApi
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.UrlAnnotation
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.infrastructure.preference.LocalDarkTheme
|
||||
import me.ash.reader.infrastructure.preference.LocalOpenLink
|
||||
import me.ash.reader.infrastructure.preference.LocalOpenLinkSpecificBrowser
|
||||
import me.ash.reader.infrastructure.preference.SettingsProvider
|
||||
import me.ash.reader.ui.ext.getCurrentVersion
|
||||
import me.ash.reader.ui.ext.openURL
|
||||
import me.ash.reader.ui.theme.AppTheme
|
||||
|
||||
class CrashReportActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v, insets ->
|
||||
v.setPadding(0, 0, 0, 0)
|
||||
insets
|
||||
}
|
||||
val errorMessage: String = intent.getStringExtra(ERROR_REPORT_KEY).toString()
|
||||
|
||||
setContent {
|
||||
SettingsProvider {
|
||||
AppTheme(useDarkTheme = LocalDarkTheme.current.isDarkTheme()) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val appVersion = getCurrentVersion().toString()
|
||||
val deviceModel = "${Build.MANUFACTURER} ${Build.MODEL}"
|
||||
val androidVersion =
|
||||
"Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})"
|
||||
|
||||
val errorReport =
|
||||
"Version: $appVersion\nDevice: $deviceModel\nSystem: $androidVersion\n\nStack trace: \n\n$errorMessage"
|
||||
|
||||
CrashReportPage(text = errorReport) {
|
||||
clipboardManager.setText(AnnotatedString(errorReport))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (isFinishing) finishAffinity()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ERROR_REPORT_KEY = "error_stack_trace"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalTextApi::class)
|
||||
@Composable
|
||||
@Preview(apiLevel = 33)
|
||||
fun CrashReportPage(
|
||||
text: String = "Version: 0.9.11\n" +
|
||||
"Model: Google Pixel 6 Pro\n" + "System: Android 13 (API 33)\n\n" +
|
||||
"Stack trace: \n" +
|
||||
"\nFATAL EXCEPTION: main\n" +
|
||||
"Process: me.ash.reader, PID: 29184\n" +
|
||||
"java.lang.IllegalArgumentException: performMeasureAndLayout called during measure layout\n" +
|
||||
"\tat androidx.compose.ui.node.MeasureAndLayoutDelegate.measureAndLayout(MeasureAndLayoutDelegate.kt:133)\n" +
|
||||
"\tat androidx.compose.ui.platform.AndroidComposeView.measureAndLayout(AndroidComposeView.android.kt:34)\n" +
|
||||
"\tat androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:15)\n" +
|
||||
"\tat android.view.View.draw(View.java:24193)\n" +
|
||||
"\tat android.view.View.updateDisplayListIfDirty(View.java:23056)\n" +
|
||||
"\tat android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4550)\n",
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val openLinkPreference = LocalOpenLink.current
|
||||
val openLinkSpecificBrowserPreference = LocalOpenLinkSpecificBrowser.current
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
bottomBar = {}) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(it)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.BugReport,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.surfaceTint
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.unexpected_error_title),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, top = 12.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
|
||||
val hyperLinkText = stringResource(R.string.submit_bug_report)
|
||||
val msg = stringResource(R.string.unexpected_error_msg).format(hyperLinkText)
|
||||
|
||||
val annotatedString = buildAnnotatedString {
|
||||
append(msg.format(hyperLinkText))
|
||||
val startIndex = msg.indexOf(hyperLinkText)
|
||||
val endIndex = startIndex + hyperLinkText.length
|
||||
addUrlAnnotation(
|
||||
UrlAnnotation("https://github.com/Ashinch/ReadYou/issues/new?assignees=&labels=bug&projects=&template=bug_report.md&title="),
|
||||
start = startIndex,
|
||||
end = endIndex
|
||||
)
|
||||
addStyle(
|
||||
SpanStyle(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
), start = startIndex,
|
||||
end = endIndex
|
||||
)
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = annotatedString,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
onClick = { index ->
|
||||
annotatedString.getUrlAnnotations(index, index).firstOrNull()?.let { range ->
|
||||
context.openURL(
|
||||
url = range.item.url,
|
||||
openLink = openLinkPreference,
|
||||
specificBrowser = openLinkSpecificBrowserPreference
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp, horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.padding(),
|
||||
contentPadding = ButtonDefaults.ButtonWithIconContentPadding
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.ContentCopy,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(ButtonDefaults.IconSize)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = stringResource(R.string.copy_error_report))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.large,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.padding(12.dp)
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontFamily = FontFamily.Monospace
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -409,4 +409,8 @@
|
||||
<string name="all_read">All read</string>
|
||||
<string name="read_excluding_starred">Read, excluding starred</string>
|
||||
<string name="external_links">External links</string>
|
||||
<string name="unexpected_error_title">Oops! Something went wrong…</string>
|
||||
<string name="copy_error_report">Copy error report</string>
|
||||
<string name="submit_bug_report">submit a bug report on GitHub</string>
|
||||
<string name="unexpected_error_msg">The app encountered an unexpected error and had to close.\n\nTo help us identify and fix this issue quickly, you can %1$s with the error stack trace below.</string>
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user