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