feat: Add CrashActivity

This commit is contained in:
Ahmad Ansori Palembani 2024-05-27 17:44:41 +07:00
parent 306b8c68ff
commit 6b47578354
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
9 changed files with 259 additions and 5 deletions

View file

@ -43,10 +43,15 @@
android:localeConfig="@xml/locales_config"
android:theme="@style/Theme.Tachiyomi"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:process=":error_handler"
android:name=".ui.crash.CrashActivity"
android:exported="true" />
<activity
android:name=".ui.main.MainActivity"
android:windowSoftInputMode="adjustNothing"
android:label="@string/app_name"
android:theme="@style/Theme.Tachiyomi.SplashScreen"
android:exported="true">
<intent-filter>

View file

@ -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,
)
}
}
}
}

View file

@ -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<Application>.onCreate()
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
// TLS 1.3 support for Android 10 and below

View file

@ -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))
},
)
}
}
}
}

View file

@ -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<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 {
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
}
}
}
}

View file

@ -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()
}
}
}

View file

@ -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") }
}
}

View file

@ -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 <T> withNonCancellableContext(block: suspend CoroutineScope.() -> T) =
withContext(NonCancellable, block)

View file

@ -1233,4 +1233,9 @@
<string name="sfw">SFW</string>
<string name="nsfw">NSFW</string>
<string name="content_type">Content Type</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 to a GitHub Issue.</string>
<string name="crash_screen_restart_application">Restart the application</string>
</resources>