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:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:process=":error_handler"
|
||||||
|
android:name=".crash.CrashActivity"
|
||||||
|
android:exported="true" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.DeepLinkActivity"
|
android:name=".ui.main.DeepLinkActivity"
|
||||||
android:launchMode="singleTask"
|
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.powerManager
|
||||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import rikka.sui.Sui
|
import rikka.sui.Sui
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -89,7 +90,7 @@ class SettingsAdvancedScreen : SearchableSettings {
|
|||||||
title = stringResource(R.string.pref_dump_crash_logs),
|
title = stringResource(R.string.pref_dump_crash_logs),
|
||||||
subtitle = stringResource(R.string.pref_dump_crash_logs_summary),
|
subtitle = stringResource(R.string.pref_dump_crash_logs_summary),
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launchNonCancellable {
|
scope.launch {
|
||||||
CrashLogUtil(context).dumpLogs()
|
CrashLogUtil(context).dumpLogs()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -30,6 +30,8 @@ import eu.kanade.domain.DomainModule
|
|||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.domain.ui.model.ThemeMode
|
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.DomainMangaKeyer
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
||||||
@ -74,6 +76,8 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super<Application>.onCreate()
|
super<Application>.onCreate()
|
||||||
|
|
||||||
|
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
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.R
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
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.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||||
@ -20,7 +21,7 @@ class CrashLogUtil(private val context: Context) {
|
|||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun dumpLogs() {
|
suspend fun dumpLogs() = withNonCancellableContext {
|
||||||
try {
|
try {
|
||||||
val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt")
|
val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt")
|
||||||
Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}").waitFor()
|
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="empty_screen">Well, this is awkward</string>
|
||||||
<string name="not_installed">Not installed</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 -->
|
<!-- 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_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>
|
<string name="download_insufficient_space">Couldn\'t download chapters due to low storage space</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user