forked from melod1n/fast-messenger
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
|
local.properties
|
||||||
.idea
|
.idea
|
||||||
/.kotlin
|
/.kotlin
|
||||||
|
.hotswan/
|
||||||
|
|||||||
@@ -79,9 +79,6 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.acra.email)
|
|
||||||
implementation(libs.acra.dialog)
|
|
||||||
|
|
||||||
implementation(projects.feature.auth)
|
implementation(projects.feature.auth)
|
||||||
|
|
||||||
implementation(projects.feature.chatmaterials)
|
implementation(projects.feature.chatmaterials)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
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.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
@@ -37,6 +37,12 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="dev.meloda.fast.presentation.CrashActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:process=":error_handler"
|
||||||
|
android:theme="@style/CrashDialogTheme" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="dev.meloda.fast.service.longpolling.LongPollingService"
|
android:name="dev.meloda.fast.service.longpolling.LongPollingService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package dev.meloda.fast.common
|
package dev.meloda.fast.common
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
@@ -8,14 +10,14 @@ import com.skydoves.compose.stability.runtime.ComposeStabilityAnalyzer
|
|||||||
import dev.meloda.fast.auth.BuildConfig
|
import dev.meloda.fast.auth.BuildConfig
|
||||||
import dev.meloda.fast.common.di.applicationModule
|
import dev.meloda.fast.common.di.applicationModule
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import org.acra.config.dialog
|
import dev.meloda.fast.presentation.CrashActivity
|
||||||
import org.acra.config.mailSender
|
|
||||||
import org.acra.data.StringFormat
|
|
||||||
import org.acra.ktx.initAcra
|
|
||||||
import org.koin.android.ext.android.get
|
import org.koin.android.ext.android.get
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.android.ext.koin.androidLogger
|
import org.koin.android.ext.koin.androidLogger
|
||||||
import org.koin.core.context.GlobalContext.startKoin
|
import org.koin.core.context.GlobalContext.startKoin
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class AppGlobal : Application(), ImageLoaderFactory {
|
class AppGlobal : Application(), ImageLoaderFactory {
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ class AppGlobal : Application(), ImageLoaderFactory {
|
|||||||
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
|
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
|
||||||
|
|
||||||
initKoin()
|
initKoin()
|
||||||
initAcra()
|
initCrashHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader = get()
|
override fun newImageLoader(): ImageLoader = get()
|
||||||
@@ -40,20 +42,36 @@ class AppGlobal : Application(), ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initAcra() {
|
private fun initCrashHandler() {
|
||||||
initAcra {
|
val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
buildConfigClass = BuildConfig::class.java
|
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||||
reportFormat = StringFormat.JSON
|
val crashLogsDirectory = File(filesDir.absolutePath + "/crashlogs")
|
||||||
|
if (!crashLogsDirectory.exists()) {
|
||||||
mailSender {
|
crashLogsDirectory.mkdirs()
|
||||||
mailTo = "lischenkodev@gmail.com"
|
|
||||||
reportAsFile = true
|
|
||||||
reportFileName = "Crash.txt"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog {
|
val crashLogFileName = "crash_" + System.currentTimeMillis() + ".txt"
|
||||||
text = "App crashed"
|
val crashLogFile = File(crashLogsDirectory.absolutePath + "/" + crashLogFileName)
|
||||||
enabled = true
|
|
||||||
|
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="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="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>
|
</resources>
|
||||||
|
|||||||
@@ -2,4 +2,13 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar" />
|
<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>
|
</resources>
|
||||||
|
|||||||
@@ -36,13 +36,9 @@ nanokt = "1.3.0"
|
|||||||
androidx-navigation = "2.9.8"
|
androidx-navigation = "2.9.8"
|
||||||
serialization = "1.11.0"
|
serialization = "1.11.0"
|
||||||
|
|
||||||
acra = "5.13.1"
|
|
||||||
okhttp = "5.3.2"
|
okhttp = "5.3.2"
|
||||||
|
|
||||||
[libraries]
|
[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" }
|
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
|
|
||||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
||||||
|
|||||||
Reference in New Issue
Block a user