Display an error screen when the app crashes

This commit is contained in:
Shinokuni 2024-08-03 19:37:21 +02:00
parent 4d9f71559f
commit 44b2858cb0
7 changed files with 216 additions and 6 deletions

View File

@ -24,7 +24,9 @@
android:resource="@xml/file_paths" />
</provider>
<receiver android:name=".sync.SyncBroadcastReceiver" android:exported="false" />
<receiver
android:name=".sync.SyncBroadcastReceiver"
android:exported="false" />
<activity
android:name=".MainActivity"
@ -38,7 +40,13 @@
</intent-filter>
</activity>
<activity
android:name=".util.CrashActivity"
android:excludeFromRecents="true"
android:exported="false"
android:finishOnTaskLaunch="true"
android:launchMode="singleInstance"
android:theme="@style/Theme.Readrops" />
</application>
</manifest>

View File

@ -3,12 +3,14 @@ package com.readrops.app
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationManagerCompat
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.disk.DiskCache
import com.readrops.api.apiModule
import com.readrops.app.util.CrashActivity
import com.readrops.db.dbModule
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
@ -16,12 +18,24 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.context.startKoin
import org.koin.core.logger.Level
import kotlin.system.exitProcess
open class ReadropsApp : Application(), KoinComponent, ImageLoaderFactory {
override fun onCreate() {
super.onCreate()
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
val intent = Intent(this, CrashActivity::class.java).apply {
putExtra(CrashActivity.THROWABLE_KEY, throwable)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
startActivity(intent)
exitProcess(0)
}
startKoin {
androidLogger(Level.ERROR)
androidContext(this@ReadropsApp)

View File

@ -0,0 +1,169 @@
package com.readrops.app.util
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
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.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.readrops.app.R
import com.readrops.app.util.theme.MediumSpacer
import com.readrops.app.util.theme.ReadropsTheme
import com.readrops.app.util.theme.ShortSpacer
import com.readrops.app.util.theme.VeryLargeSpacer
import com.readrops.app.util.theme.VeryShortSpacer
import com.readrops.app.util.theme.spacing
class CrashActivity : ComponentActivity() {
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge(statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT))
val throwable = intent.getSerializableExtra(THROWABLE_KEY) as Throwable?
setContent {
ReadropsTheme {
CrashScreen(throwable?.stackTraceToString().orEmpty())
}
}
}
companion object {
const val THROWABLE_KEY = "THROWABLE"
}
}
@Composable
fun CrashScreen(stackTrace: String) {
val uriHandler = LocalUriHandler.current
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
Surface(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.statusBarsPadding()
.padding(MaterialTheme.spacing.mediumSpacing)
) {
Column(
modifier = Modifier.fillMaxSize()
) {
VeryLargeSpacer()
Icon(
painter = painterResource(id = R.drawable.ic_bug),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(64.dp)
)
MediumSpacer()
Text(
text = stringResource(R.string.readrops_crashed),
style = MaterialTheme.typography.titleLarge
)
ShortSpacer()
Text(
text = stringResource(R.string.crash_message),
style = MaterialTheme.typography.bodyMedium,
)
MediumSpacer()
Surface(
shape = RoundedCornerShape(16.dp),
color = MaterialTheme.colorScheme.surfaceContainerLow,
modifier = Modifier
.weight(1f, fill = true)
.fillMaxWidth()
) {
Text(
text = stackTrace,
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Justify,
lineHeight = 20.sp,
modifier = Modifier
.padding(MaterialTheme.spacing.mediumSpacing)
.verticalScroll(rememberScrollState())
.horizontalScroll(rememberScrollState())
)
}
MediumSpacer()
Column {
Button(
onClick = {
uriHandler.openUri("https://github.com/readrops/Readrops/issues/new")
clipboardManager.setText(AnnotatedString(stackTrace))
displayToast(context)
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.report_error_github))
}
VeryShortSpacer()
OutlinedButton(
onClick = {
clipboardManager.setText(AnnotatedString(stackTrace))
displayToast(context)
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.copy_error_clipboard))
}
}
}
}
}
fun displayToast(context: Context) {
Toast.makeText(context, R.string.copied, Toast.LENGTH_SHORT).show()
}
@DefaultPreview
@Composable
private fun CrashScreenPreview() {
ReadropsTheme {
CrashScreen("")
}
}

View File

@ -3,12 +3,12 @@ package com.readrops.app.util
import android.content.res.Configuration
import androidx.compose.ui.tooling.preview.Preview
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true
)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_NO,
showBackground = true
)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true
)
annotation class DefaultPreview

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M256,0c53,0 96,43 96,96l0,3.6c0,15.7 -12.7,28.4 -28.4,28.4l-135.1,0c-15.7,0 -28.4,-12.7 -28.4,-28.4l0,-3.6c0,-53 43,-96 96,-96zM41.4,105.4c12.5,-12.5 32.8,-12.5 45.3,0l64,64c0.7,0.7 1.3,1.4 1.9,2.1c14.2,-7.3 30.4,-11.4 47.5,-11.4l112,0c17.1,0 33.2,4.1 47.5,11.4c0.6,-0.7 1.2,-1.4 1.9,-2.1l64,-64c12.5,-12.5 32.8,-12.5 45.3,0s12.5,32.8 0,45.3l-64,64c-0.7,0.7 -1.4,1.3 -2.1,1.9c6.2,12 10.1,25.3 11.1,39.5l64.3,0c17.7,0 32,14.3 32,32s-14.3,32 -32,32l-64,0c0,24.6 -5.5,47.8 -15.4,68.6c2.2,1.3 4.2,2.9 6,4.8l64,64c12.5,12.5 12.5,32.8 0,45.3s-32.8,12.5 -45.3,0l-63.1,-63.1c-24.5,21.8 -55.8,36.2 -90.3,39.6L272,240c0,-8.8 -7.2,-16 -16,-16s-16,7.2 -16,16l0,239.2c-34.5,-3.4 -65.8,-17.8 -90.3,-39.6L86.6,502.6c-12.5,12.5 -32.8,12.5 -45.3,0s-12.5,-32.8 0,-45.3l64,-64c1.9,-1.9 3.9,-3.4 6,-4.8C101.5,367.8 96,344.6 96,320l-64,0c-17.7,0 -32,-14.3 -32,-32s14.3,-32 32,-32l64.3,0c1.1,-14.1 5,-27.5 11.1,-39.5c-0.7,-0.6 -1.4,-1.2 -2.1,-1.9l-64,-64c-12.5,-12.5 -12.5,-32.8 0,-45.3z"/>
</vector>

View File

@ -208,4 +208,9 @@
<string name="external">Externe</string>
<string name="mark_all_articles_read">Marquer les articles comme lus</string>
<string name="mark_all_articles_read_question">Voulez-vous vraiment marquer tous les articles comme lus ?</string>
<string name="readrops_crashed">Readrops a planté.</string>
<string name="crash_message">Si vous voyez ceci, cela signifie que l\'application a rencontré une erreur inattendue. Si cela est possible, merci de bien vouloir signaler cette erreur sur Github.</string>
<string name="report_error_github">Signaler l\'erreur sur Github</string>
<string name="copy_error_clipboard">Copier l\'erreur dans le presse-papier</string>
<string name="copied">Copié !</string>
</resources>

View File

@ -217,4 +217,9 @@
<string name="external">External</string>
<string name="mark_all_articles_read">Mark all articles as read</string>
<string name="mark_all_articles_read_question">Do you really want to mark all items as read?</string>
<string name="readrops_crashed">Readrops crashed.</string>
<string name="crash_message">I you see this, it means the app ran into an unexpected error. Please if you can, report the error on Github.</string>
<string name="report_error_github">Report the error on Github</string>
<string name="copy_error_clipboard">Copy error to clipboard</string>
<string name="copied">Copied!</string>
</resources>