mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
feat: Add CrashActivity
This commit is contained in:
parent
306b8c68ff
commit
6b47578354
9 changed files with 259 additions and 5 deletions
|
@ -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>
|
||||
|
|
116
app/src/main/java/dev/yokai/presentation/crash/CrashScreen.kt
Normal file
116
app/src/main/java/dev/yokai/presentation/crash/CrashScreen.kt
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -110,9 +110,11 @@ class SettingsAdvancedController : SettingsController() {
|
|||
summaryRes = R.string.saves_error_logs
|
||||
|
||||
onClick {
|
||||
(activity as? AppCompatActivity)?.lifecycleScope?.launchIO {
|
||||
CrashLogUtil(context.localeContext).dumpLogs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preference {
|
||||
key = "debug_info"
|
||||
|
|
|
@ -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") }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue