mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-10 04:37:25 +01:00
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:
parent
558aad1a71
commit
4178f945c9
@ -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"
|
||||
|
119
app/src/main/java/eu/kanade/presentation/crash/CrashScreen.kt
Normal file
119
app/src/main/java/eu/kanade/presentation/crash/CrashScreen.kt
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
},
|
||||
|
@ -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)
|
||||
|
25
app/src/main/java/eu/kanade/tachiyomi/crash/CrashActivity.kt
Normal file
25
app/src/main/java/eu/kanade/tachiyomi/crash/CrashActivity.kt
Normal 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))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user