From b3b90179550138e724837a6b7251b010a2c1506d Mon Sep 17 00:00:00 2001 From: AbdallahMehiz Date: Fri, 2 Aug 2024 18:06:22 +0100 Subject: [PATCH] feat(app): add crash screen "inspired" by mihon --- app/build.gradle.kts | 11 + app/src/main/AndroidManifest.xml | 15 +- app/src/main/java/live/mehiz/mpvkt/App.kt | 3 + .../mpvkt/presentation/crash/CrashActivity.kt | 206 ++++++++++++++++++ .../crash/GlobalExceptionHandler.kt | 20 ++ app/src/main/res/values/strings.xml | 7 + app/src/main/res/xml/provider_paths.xml | 9 + 7 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/live/mehiz/mpvkt/presentation/crash/CrashActivity.kt create mode 100644 app/src/main/java/live/mehiz/mpvkt/presentation/crash/GlobalExceptionHandler.kt create mode 100644 app/src/main/res/xml/provider_paths.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 106d3c3..533e073 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,4 +1,7 @@ import io.gitlab.arturbosch.detekt.Detekt +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter plugins { alias(libs.plugins.ksp) @@ -24,6 +27,13 @@ android { vectorDrawables { useSupportLibrary = true } + + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") + buildConfigField( + "String", + "BUILD_TIME", + "\"${LocalDateTime.now(ZoneOffset.UTC).format(dateTimeFormatter)}\"", + ) } splits { abi { @@ -63,6 +73,7 @@ android { buildFeatures { compose = true viewBinding = true + buildConfig = true } composeCompiler { includeSourceInformation = true diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3c24e29..385f38a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,11 +30,11 @@ @@ -70,5 +70,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/java/live/mehiz/mpvkt/App.kt b/app/src/main/java/live/mehiz/mpvkt/App.kt index 9e380ab..1ccd7c9 100644 --- a/app/src/main/java/live/mehiz/mpvkt/App.kt +++ b/app/src/main/java/live/mehiz/mpvkt/App.kt @@ -3,12 +3,15 @@ package live.mehiz.mpvkt import android.app.Application import live.mehiz.mpvkt.di.DatabaseModule import live.mehiz.mpvkt.di.PreferencesModule +import live.mehiz.mpvkt.presentation.crash.CrashActivity +import live.mehiz.mpvkt.presentation.crash.GlobalExceptionHandler import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin class App : Application() { override fun onCreate() { super.onCreate() + Thread.setDefaultUncaughtExceptionHandler(GlobalExceptionHandler(applicationContext, CrashActivity::class.java)) startKoin { androidContext(this@App) modules( diff --git a/app/src/main/java/live/mehiz/mpvkt/presentation/crash/CrashActivity.kt b/app/src/main/java/live/mehiz/mpvkt/presentation/crash/CrashActivity.kt new file mode 100644 index 0000000..12b600e --- /dev/null +++ b/app/src/main/java/live/mehiz/mpvkt/presentation/crash/CrashActivity.kt @@ -0,0 +1,206 @@ +package live.mehiz.mpvkt.presentation.crash + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.outlined.BugReport +import androidx.compose.material3.Button +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.core.content.FileProvider +import `is`.xyz.mpv.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import live.mehiz.mpvkt.BuildConfig +import live.mehiz.mpvkt.MainActivity +import live.mehiz.mpvkt.R +import live.mehiz.mpvkt.ui.theme.MpvKtTheme +import java.io.File + +class CrashActivity : ComponentActivity() { + + private val clipboardManager by lazy { getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager } + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + setContent { + MpvKtTheme { + CrashScreen(intent.getStringExtra("exception") ?: "") + } + } + } + + private fun createLogs(): String { + return """ + App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.BUILD_TIME}) + Android version: ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT}) + Device brand: ${Build.BRAND} + Device manufacturer: ${Build.MANUFACTURER} + Device model: ${Build.MODEL} (${Build.DEVICE}) + MPV version: ${Utils.VERSIONS.mpv} + ffmpeg version: ${Utils.VERSIONS.ffmpeg} + libplacebo version: ${Utils.VERSIONS.libPlacebo} + """.trimIndent() + } + + private suspend fun dumpLogs( + exceptionString: String + ) { + withContext(NonCancellable) { + val file = File(applicationContext.cacheDir, "mptKt_logs.txt") + if (file.exists()) file.delete() + file.createNewFile() + file.appendText(createLogs()) + file.appendText("\n\n") + file.appendText(exceptionString) + val uri = FileProvider.getUriForFile(applicationContext, BuildConfig.APPLICATION_ID + ".provider", file) + val intent = Intent(Intent.ACTION_SEND) + intent.putExtra(Intent.EXTRA_STREAM, uri) + intent.clipData = ClipData.newRawUri(null, uri) + intent.type = "text/plain" + intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + this@CrashActivity.startActivity( + Intent.createChooser(intent, applicationContext.getString(R.string.crash_screen_share)) + ) + } + } + + @Composable + fun CrashScreen( + exceptionString: String, + modifier: Modifier = Modifier, + ) { + val scope = rememberCoroutineScope() + Scaffold( + modifier = modifier.fillMaxSize(), + bottomBar = { + val borderColor = MaterialTheme.colorScheme.outline + Column( + Modifier + .windowInsetsPadding(NavigationBarDefaults.windowInsets) + .drawBehind { + drawLine( + borderColor, + Offset.Zero, + Offset(size.width, 0f), + strokeWidth = Dp.Hairline.value, + ) + } + .padding(vertical = 8.dp, horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button( + onClick = { + scope.launch(Dispatchers.IO) { + dumpLogs(exceptionString) + } + }, + modifier = Modifier.weight(1f), + ) { Text(stringResource(R.string.crash_screen_share)) } + FilledIconButton( + onClick = { + clipboardManager.setPrimaryClip(ClipData.newPlainText(null, createLogs())) + }, + ) { + Icon(Icons.Default.ContentCopy, null) + } + } + OutlinedButton( + onClick = { + startActivity(Intent(this@CrashActivity, MainActivity::class.java)) + finishAndRemoveTask() + }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.crash_screen_restart)) + } + } + }, + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Spacer(Modifier.height(paddingValues.calculateTopPadding())) + Icon( + Icons.Outlined.BugReport, + null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Text( + stringResource(R.string.crash_screen_title), + style = MaterialTheme.typography.headlineLarge, + ) + Text( + stringResource(R.string.crash_screen_subtitle, stringResource(R.string.app_name)), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + stringResource(R.string.crash_screen_logs_title), + style = MaterialTheme.typography.headlineSmall, + ) + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + ) { + SelectionContainer { + Text( + text = exceptionString, + fontFamily = FontFamily.Monospace, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp), + ) + } + } + Spacer(Modifier.height(8.dp)) + } + } + } +} diff --git a/app/src/main/java/live/mehiz/mpvkt/presentation/crash/GlobalExceptionHandler.kt b/app/src/main/java/live/mehiz/mpvkt/presentation/crash/GlobalExceptionHandler.kt new file mode 100644 index 0000000..02e3156 --- /dev/null +++ b/app/src/main/java/live/mehiz/mpvkt/presentation/crash/GlobalExceptionHandler.kt @@ -0,0 +1,20 @@ +package live.mehiz.mpvkt.presentation.crash + +import android.content.Context +import android.content.Intent +import kotlin.system.exitProcess + +class GlobalExceptionHandler( + private val context: Context, + private val activity: Class<*> +) : Thread.UncaughtExceptionHandler { + + override fun uncaughtException(t: Thread, e: Throwable) { + val intent = Intent(context, activity) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + intent.putExtra("exception", e.stackTraceToString()) + context.startActivity(intent) + exitProcess(0) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d95e16..1dbb932 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ mpvKt Reset + Share Appearance Theme @@ -89,4 +90,10 @@ Fit Screen Crop Stretch + + Oops + Looks like %s experienced an unexpected error. Consider sharing the crash logs in a GitHub issue. + Crash logs: + Share crash logs + Restart the app \ No newline at end of file diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000..03f383d --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file