diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fe692e20e..364d32422 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -57,6 +57,12 @@
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
+
+
+
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,
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt
index 58473bc51..e25f67620 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt
@@ -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()
}
},
diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt
index 4bd6bda2a..ce24e9c2b 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/App.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt
@@ -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.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)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/crash/CrashActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/crash/CrashActivity.kt
new file mode 100644
index 000000000..f0d9ce0fc
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/crash/CrashActivity.kt
@@ -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))
+ },
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/crash/GlobalExceptionHandler.kt b/app/src/main/java/eu/kanade/tachiyomi/crash/GlobalExceptionHandler.kt
new file mode 100644
index 000000000..668470071
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/crash/GlobalExceptionHandler.kt
@@ -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 {
+ 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
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt
index 39870eec2..e8b7f7969 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt
@@ -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()
diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml
index d4dd013b0..fe045cc0d 100644
--- a/i18n/src/main/res/values/strings.xml
+++ b/i18n/src/main/res/values/strings.xml
@@ -781,6 +781,11 @@
Well, this is awkward
Not installed
+
+ An Unexpected Error Occurred
+ %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.
+ Restart the application
+
Couldn\'t download chapters. You can try again in the downloads section
Couldn\'t download chapters due to low storage space