Replace ACRA with custom crash dialog
* Add uncaught exception handler that saves stacktraces to local crash log files * Show a Compose crash dialog with stacktrace toggle and share action * Register crash handler activity in a separate process with dialog theme * Remove ACRA dependencies and configuration * Add crash dialog strings and ignore `.hotswan/`
This commit is contained in:
@@ -15,3 +15,4 @@ build/
|
||||
local.properties
|
||||
.idea
|
||||
/.kotlin
|
||||
.hotswan/
|
||||
|
||||
@@ -79,9 +79,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.acra.email)
|
||||
implementation(libs.acra.dialog)
|
||||
|
||||
implementation(projects.feature.auth)
|
||||
|
||||
implementation(projects.feature.chatmaterials)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
@@ -37,6 +37,12 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="dev.meloda.fast.presentation.CrashActivity"
|
||||
android:exported="false"
|
||||
android:process=":error_handler"
|
||||
android:theme="@style/CrashDialogTheme" />
|
||||
|
||||
<service
|
||||
android:name="dev.meloda.fast.service.longpolling.LongPollingService"
|
||||
android:enabled="true"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package dev.meloda.fast.common
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.preference.PreferenceManager
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
@@ -8,14 +10,14 @@ import com.skydoves.compose.stability.runtime.ComposeStabilityAnalyzer
|
||||
import dev.meloda.fast.auth.BuildConfig
|
||||
import dev.meloda.fast.common.di.applicationModule
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import org.acra.config.dialog
|
||||
import org.acra.config.mailSender
|
||||
import org.acra.data.StringFormat
|
||||
import org.acra.ktx.initAcra
|
||||
import dev.meloda.fast.presentation.CrashActivity
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.GlobalContext.startKoin
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class AppGlobal : Application(), ImageLoaderFactory {
|
||||
|
||||
@@ -27,7 +29,7 @@ class AppGlobal : Application(), ImageLoaderFactory {
|
||||
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
|
||||
|
||||
initKoin()
|
||||
initAcra()
|
||||
initCrashHandler()
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader = get()
|
||||
@@ -40,20 +42,36 @@ class AppGlobal : Application(), ImageLoaderFactory {
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAcra() {
|
||||
initAcra {
|
||||
buildConfigClass = BuildConfig::class.java
|
||||
reportFormat = StringFormat.JSON
|
||||
|
||||
mailSender {
|
||||
mailTo = "lischenkodev@gmail.com"
|
||||
reportAsFile = true
|
||||
reportFileName = "Crash.txt"
|
||||
private fun initCrashHandler() {
|
||||
val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||
val crashLogsDirectory = File(filesDir.absolutePath + "/crashlogs")
|
||||
if (!crashLogsDirectory.exists()) {
|
||||
crashLogsDirectory.mkdirs()
|
||||
}
|
||||
|
||||
dialog {
|
||||
text = "App crashed"
|
||||
enabled = true
|
||||
val crashLogFileName = "crash_" + System.currentTimeMillis() + ".txt"
|
||||
val crashLogFile = File(crashLogsDirectory.absolutePath + "/" + crashLogFileName)
|
||||
|
||||
FileOutputStream(crashLogFile).use { stream ->
|
||||
stream.write(throwable.stackTraceToString().toByteArray())
|
||||
}
|
||||
|
||||
if (AppSettings.Debug.showAlertAfterCrash) {
|
||||
try {
|
||||
val intent = Intent(this, CrashActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
intent.putExtra("CRASH_LOG_FILE_URI", Uri.fromFile(crashLogFile))
|
||||
startActivity(intent)
|
||||
|
||||
exitProcess(0)
|
||||
} catch (e: Exception) {
|
||||
if (e !is RuntimeException) {
|
||||
defaultExceptionHandler?.uncaughtException(thread, throwable)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
defaultExceptionHandler?.uncaughtException(thread, throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package dev.meloda.fast.presentation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.components.MaterialDialog
|
||||
|
||||
@Composable
|
||||
fun AppCrashedDialog(
|
||||
stacktrace: String,
|
||||
onDismiss: () -> Unit,
|
||||
onShare: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showTrace by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
MaterialDialog(
|
||||
modifier = modifier,
|
||||
onDismissRequest = onDismiss,
|
||||
title = stringResource(R.string.title_error),
|
||||
text = if (showTrace) stacktrace else stringResource(R.string.error_occurred),
|
||||
confirmText = stringResource(R.string.action_share),
|
||||
confirmAction = onShare,
|
||||
cancelText = stringResource(if (showTrace) R.string.action_hide_stacktrace else R.string.action_show_stacktrace),
|
||||
cancelAction = { showTrace = !showTrace },
|
||||
neutralText = stringResource(R.string.action_close),
|
||||
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package dev.meloda.fast.presentation
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toFile
|
||||
import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.ui.theme.AppTheme
|
||||
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
|
||||
import org.koin.compose.koinInject
|
||||
import java.io.File
|
||||
|
||||
class CrashActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
||||
val crashLogFileUri = intent.getParcelableExtra<Uri>("CRASH_LOG_FILE_URI") ?: run {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
val crashLogFile = crashLogFileUri.toFile().takeIf(File::exists) ?: run {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
val stacktrace = crashLogFile.bufferedReader().readText()
|
||||
|
||||
setContent {
|
||||
val userSettings: UserSettings = koinInject()
|
||||
|
||||
AppTheme(
|
||||
useDarkTheme = isNeedToEnableDarkMode(darkMode = userSettings.darkMode.collectAsState().value),
|
||||
useDynamicColors = userSettings.enableDynamicColors.collectAsState().value,
|
||||
selectedColorScheme = 0,
|
||||
useAmoledBackground = userSettings.enableAmoledDark.collectAsState().value,
|
||||
useSystemFont = userSettings.useSystemFont.collectAsState().value
|
||||
) {
|
||||
AppCrashedDialog(
|
||||
stacktrace = stacktrace,
|
||||
onDismiss = { finish() },
|
||||
onShare = {
|
||||
val uri = FileProvider.getUriForFile(
|
||||
this,
|
||||
"$packageName.provider",
|
||||
crashLogFile
|
||||
)
|
||||
|
||||
val sendIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
clipData = ClipData.newRawUri(null, uri)
|
||||
}
|
||||
|
||||
val chooserIntent = Intent.createChooser(sendIntent, null)
|
||||
chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
startActivity(chooserIntent)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,4 +306,7 @@
|
||||
<string name="confirm_chat_create_empty_with_title">Are you sure you want to create chat «%s» only with yourself?</string>
|
||||
|
||||
<string name="title_edit_message">Edit message</string>
|
||||
<string name="action_close">Close</string>
|
||||
<string name="action_hide_stacktrace">Hide stacktrace</string>
|
||||
<string name="action_show_stacktrace">Show stacktrace</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,13 @@
|
||||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar" />
|
||||
|
||||
<style name="CrashDialogTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -36,13 +36,9 @@ nanokt = "1.3.0"
|
||||
androidx-navigation = "2.9.8"
|
||||
serialization = "1.11.0"
|
||||
|
||||
acra = "5.13.1"
|
||||
okhttp = "5.3.2"
|
||||
|
||||
[libraries]
|
||||
acra-email = { module = "ch.acra:acra-mail", version.ref = "acra" }
|
||||
acra-dialog = { module = "ch.acra:acra-dialog", version.ref = "acra" }
|
||||
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
|
||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
||||
|
||||
Reference in New Issue
Block a user