From 6b47578354ee4fb48f8cf81cb545a52a1bf41675 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 27 May 2024 17:44:41 +0700 Subject: [PATCH] feat: Add CrashActivity --- app/src/main/AndroidManifest.xml | 7 +- .../yokai/presentation/crash/CrashScreen.kt | 116 ++++++++++++++++++ app/src/main/java/eu/kanade/tachiyomi/App.kt | 5 + .../tachiyomi/ui/crash/CrashActivity.kt | 34 +++++ .../ui/crash/GlobalExceptionHandler.kt | 79 ++++++++++++ .../ui/setting/SettingsAdvancedController.kt | 4 +- .../eu/kanade/tachiyomi/util/CrashLogUtil.kt | 8 +- .../util/system/ContextExtensions.kt | 6 + app/src/main/res/values/strings.xml | 5 + 9 files changed, 259 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/dev/yokai/presentation/crash/CrashScreen.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/crash/CrashActivity.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/crash/GlobalExceptionHandler.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f2493be00e..9fe83baf18 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,10 +43,15 @@ android:localeConfig="@xml/locales_config" android:theme="@style/Theme.Tachiyomi" android:networkSecurityConfig="@xml/network_security_config"> + + + diff --git a/app/src/main/java/dev/yokai/presentation/crash/CrashScreen.kt b/app/src/main/java/dev/yokai/presentation/crash/CrashScreen.kt new file mode 100644 index 0000000000..748577d45c --- /dev/null +++ b/app/src/main/java/dev/yokai/presentation/crash/CrashScreen.kt @@ -0,0 +1,116 @@ +package dev.yokai.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 dev.yokai.presentation.theme.Size +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 = Size.medium, vertical = Size.small), + verticalArrangement = Arrangement.spacedBy(Size.small), + ) { + Button( + onClick = { + scope.launch { + CrashLogUtil(context).dumpLogs() + } + }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = stringResource(id = R.string.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(horizontal = Size.medium) + .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(horizontal = Size.medium), + ) + Box( + modifier = Modifier + .padding(vertical = Size.small) + .clip(MaterialTheme.shapes.small) + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceVariant), + ) { + Text( + text = exception.toString(), + fontSize = MaterialTheme.typography.bodySmall.fontSize, + modifier = Modifier.padding(all = Size.small), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 05a217b8e8..fd3c9c267e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -43,6 +43,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.di.AppModule import eu.kanade.tachiyomi.di.PreferenceModule import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.ui.crash.CrashActivity +import eu.kanade.tachiyomi.ui.crash.GlobalExceptionHandler import eu.kanade.tachiyomi.ui.library.LibraryPresenter import eu.kanade.tachiyomi.ui.recents.RecentsPresenter import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate @@ -72,6 +74,9 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F @SuppressLint("LaunchActivityFromNotification") override fun onCreate() { super.onCreate() + + GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java) + if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) // TLS 1.3 support for Android 10 and below diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/crash/CrashActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/crash/CrashActivity.kt new file mode 100644 index 0000000000..443e8b0488 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/crash/CrashActivity.kt @@ -0,0 +1,34 @@ +package eu.kanade.tachiyomi.ui.crash + +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import dev.yokai.presentation.crash.CrashScreen +import dev.yokai.presentation.theme.YokaiTheme +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.util.system.setThemeByPref +import uy.kohesive.injekt.injectLazy + +class CrashActivity : AppCompatActivity() { + internal val preferences: PreferencesHelper by injectLazy() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setThemeByPref(preferences) + + val exception = GlobalExceptionHandler.getThrowableFromIntent(intent) + setContent { + YokaiTheme { + CrashScreen( + exception = exception, + onRestartClick = { + finishAffinity() + startActivity(Intent(this@CrashActivity, MainActivity::class.java)) + }, + ) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/crash/GlobalExceptionHandler.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/crash/GlobalExceptionHandler.kt new file mode 100644 index 0000000000..8db79e27a2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/crash/GlobalExceptionHandler.kt @@ -0,0 +1,79 @@ +package eu.kanade.tachiyomi.ui.crash + +import android.content.Context +import android.content.Intent +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 timber.log.Timber +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 { + Timber.e(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) { + Timber.e(e, "Wasn't able to retrieve throwable from intent") + null + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index e27ca05185..2faa50a5d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -110,7 +110,9 @@ class SettingsAdvancedController : SettingsController() { summaryRes = R.string.saves_error_logs onClick { - CrashLogUtil(context.localeContext).dumpLogs() + (activity as? AppCompatActivity)?.lifecycleScope?.launchIO { + CrashLogUtil(context.localeContext).dumpLogs() + } } } 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 e435966bf9..5a6a38101c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt @@ -13,6 +13,8 @@ import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.notificationBuilder import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.system.withNonCancellableContext +import eu.kanade.tachiyomi.util.system.withUIContext import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.IOException @@ -23,15 +25,15 @@ class CrashLogUtil(private val context: Context) { setSmallIcon(R.drawable.ic_tachij2k_notification) } - fun dumpLogs() { + suspend fun dumpLogs() = withNonCancellableContext { try { - val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt") + val file = context.createFileInCacheDir("yokai_crash_logs.txt") file.appendText(getDebugInfo() + "\n\n") file.appendText(getExtensionsInfo() + "\n\n") Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}") showNotification(file.getUriCompat(context)) } catch (e: IOException) { - context.toast("Failed to get logs") + withUIContext { context.toast("Failed to get logs") } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index 8e63f759b7..d1a27dfc03 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -43,6 +43,9 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.ui.main.MainActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -480,3 +483,6 @@ val Context.application: App val Context.appState: AppState get() = application.state + +suspend fun withNonCancellableContext(block: suspend CoroutineScope.() -> T) = + withContext(NonCancellable, block) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d6269888b7..7ce998d261 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1233,4 +1233,9 @@ SFW NSFW Content Type + + + An Unexpected Error Occurred + %s ran into an unexpected error. We suggest you screenshot this message, dump the crash logs, and then share it to a GitHub Issue. + Restart the application