Add Crash activity (#8216)

* Add Crash activity

When the application crashes this sends them to a different activity with the cause message and an option to dump the crash logs

* Review changes
This commit is contained in:
Andreas 2022-10-16 22:35:20 +02:00 committed by GitHub
parent 558aad1a71
commit 4178f945c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 243 additions and 2 deletions

View file

@ -57,6 +57,12 @@
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:process=":error_handler"
android:name=".crash.CrashActivity"
android:exported="true" />
<activity
android:name=".ui.main.DeepLinkActivity"
android:launchMode="singleTask"

View file

@ -0,0 +1,119 @@
package eu.kanade.presentation.crash
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.BugReport
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.verticalPadding
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.CrashLogUtil
import kotlinx.coroutines.launch
@Composable
fun CrashScreen(
exception: Throwable?,
onRestartClick: () -> Unit,
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
Scaffold(
bottomBar = {
val strokeWidth = Dp.Hairline
val borderColor = MaterialTheme.colorScheme.outline
Column(
modifier = Modifier
.drawBehind {
drawLine(
borderColor,
Offset(0f, 0f),
Offset(size.width, 0f),
strokeWidth.value,
)
}
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
verticalArrangement = Arrangement.spacedBy(verticalPadding),
) {
Button(
onClick = {
scope.launch {
CrashLogUtil(context).dumpLogs()
}
},
modifier = Modifier.fillMaxWidth(),
) {
Text(text = stringResource(id = R.string.pref_dump_crash_logs))
}
OutlinedButton(
onClick = onRestartClick,
modifier = Modifier.fillMaxWidth(),
) {
Text(text = stringResource(R.string.crash_screen_restart_application))
}
}
},
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.padding(top = 56.dp)
.padding(horizontal = horizontalPadding)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = Icons.Outlined.BugReport,
contentDescription = null,
modifier = Modifier
.size(64.dp),
)
Text(
text = stringResource(R.string.crash_screen_title),
style = MaterialTheme.typography.titleLarge,
)
Text(
text = stringResource(R.string.crash_screen_description, stringResource(id = R.string.app_name)),
modifier = Modifier
.padding(vertical = verticalPadding),
)
Box(
modifier = Modifier
.padding(vertical = verticalPadding)
.clip(MaterialTheme.shapes.small)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant),
) {
Text(
text = exception.toString(),
modifier = Modifier
.padding(all = verticalPadding),
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}

View file

@ -59,6 +59,7 @@ import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.launch
import logcat.LogPriority
import rikka.sui.Sui
import uy.kohesive.injekt.Injekt
@ -89,7 +90,7 @@ class SettingsAdvancedScreen : SearchableSettings {
title = stringResource(R.string.pref_dump_crash_logs),
subtitle = stringResource(R.string.pref_dump_crash_logs_summary),
onClick = {
scope.launchNonCancellable {
scope.launch {
CrashLogUtil(context).dumpLogs()
}
},

View file

@ -30,6 +30,8 @@ import eu.kanade.domain.DomainModule
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.ThemeMode
import eu.kanade.tachiyomi.crash.CrashActivity
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
import eu.kanade.tachiyomi.data.coil.DomainMangaKeyer
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
@ -74,6 +76,8 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
override fun onCreate() {
super<Application>.onCreate()
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
// TLS 1.3 support for Android < 10
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Security.insertProviderAt(Conscrypt.newProvider(), 1)

View file

@ -0,0 +1,25 @@
package eu.kanade.tachiyomi.crash
import android.content.Intent
import android.os.Bundle
import eu.kanade.presentation.crash.CrashScreen
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.view.setComposeContent
class CrashActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val exception = GlobalExceptionHandler.getThrowableFromIntent(intent)
setComposeContent {
CrashScreen(
exception = exception,
onRestartClick = {
finishAffinity()
startActivity(Intent(this@CrashActivity, MainActivity::class.java))
},
)
}
}
}

View file

@ -0,0 +1,80 @@
package eu.kanade.tachiyomi.crash
import android.content.Context
import android.content.Intent
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import logcat.LogPriority
import kotlin.system.exitProcess
class GlobalExceptionHandler private constructor(
private val applicationContext: Context,
private val defaultHandler: Thread.UncaughtExceptionHandler,
private val activityToBeLaunched: Class<*>,
) : Thread.UncaughtExceptionHandler {
object ThrowableSerializer : KSerializer<Throwable> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("Throwable", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): Throwable =
Throwable(message = decoder.decodeString())
override fun serialize(encoder: Encoder, value: Throwable) =
encoder.encodeString(value.stackTraceToString())
}
override fun uncaughtException(thread: Thread, exception: Throwable) {
try {
logcat(priority = LogPriority.ERROR, throwable = exception)
launchActivity(applicationContext, activityToBeLaunched, exception)
exitProcess(0)
} catch (_: Exception) {
defaultHandler.uncaughtException(thread, exception)
}
}
private fun launchActivity(
applicationContext: Context,
activity: Class<*>,
exception: Throwable,
) {
val intent = Intent(applicationContext, activity).apply {
putExtra(INTENT_EXTRA, Json.encodeToString(ThrowableSerializer, exception))
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
applicationContext.startActivity(intent)
}
companion object {
private const val INTENT_EXTRA = "Throwable"
fun initialize(
applicationContext: Context,
activityToBeLaunched: Class<*>,
) {
val handler = GlobalExceptionHandler(
applicationContext,
Thread.getDefaultUncaughtExceptionHandler() as Thread.UncaughtExceptionHandler,
activityToBeLaunched,
)
Thread.setDefaultUncaughtExceptionHandler(handler)
}
fun getThrowableFromIntent(intent: Intent): Throwable? {
return try {
Json.decodeFromString(ThrowableSerializer, intent.getStringExtra(INTENT_EXTRA)!!)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Wasn't able to retrive throwable from intent" }
null
}
}
}
}

View file

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
@ -20,7 +21,7 @@ class CrashLogUtil(private val context: Context) {
setSmallIcon(R.drawable.ic_tachi)
}
suspend fun dumpLogs() {
suspend fun dumpLogs() = withNonCancellableContext {
try {
val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt")
Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}").waitFor()

View file

@ -781,6 +781,11 @@
<string name="empty_screen">Well, this is awkward</string>
<string name="not_installed">Not installed</string>
<!-- Crash screen -->
<string name="crash_screen_title">An Unexpected Error Occurred</string>
<string name="crash_screen_description">%s ran into an unexpected error. We suggest you screenshot this message, dump the crash logs, and then share it in our support channel on Discord.</string>
<string name="crash_screen_restart_application">Restart the application</string>
<!-- Downloads activity and service -->
<string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string>
<string name="download_insufficient_space">Couldn\'t download chapters due to low storage space</string>