mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +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:
		
							
								
								
									
										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() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user