From 4c6d52ba751a74fc9630dcf19ec328db1aa9cf5e Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 6 Dec 2023 05:31:49 +0800 Subject: [PATCH] android, desktop: crash handler (#3516) * android, desktop: crash handler * test * rename * string * Revert "test" This reverts commit 530faf39c16e1cdf3fa8e78bac407707f4d6e528. --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/MainActivity.kt | 4 +- .../main/java/chat/simplex/app/SimplexApp.kt | 4 +- .../simplex/common/platform/UI.android.kt | 40 ++++- .../kotlin/chat/simplex/common/App.kt | 8 +- .../kotlin/chat/simplex/common/platform/UI.kt | 8 + .../common/views/helpers/AlertManager.kt | 97 +++++++---- .../commonMain/resources/MR/base/strings.xml | 1 + .../kotlin/chat/simplex/common/DesktopApp.kt | 164 +++++++++++------- .../simplex/common/platform/UI.desktop.kt | 6 + .../kotlin/chat/simplex/desktop/Main.kt | 4 - 10 files changed, 226 insertions(+), 110 deletions(-) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt index a84590fb85..cbe0ef7b16 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt @@ -41,9 +41,7 @@ class MainActivity: FragmentActivity() { ) } setContent { - SimpleXTheme { - AppScreen() - } + AppScreen() } SimplexApp.context.schedulePeriodicServiceRestartWorker() SimplexApp.context.schedulePeriodicWakeUp() diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index 90696b79a4..a345e6e48f 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -32,7 +32,9 @@ class SimplexApp: Application(), LifecycleEventObserver { override fun onCreate() { super.onCreate() if (ProcessPhoenix.isPhoenixProcess(this)) { - return; + return + } else { + registerGlobalErrorHandler() } context = this initHaskell() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt index ca497cbc5b..96bb739113 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt @@ -8,10 +8,14 @@ import android.os.Build import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.Toast +import androidx.activity.compose.setContent import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalView -import chat.simplex.common.views.helpers.KeyboardState +import chat.simplex.common.AppScreen +import chat.simplex.common.ui.theme.SimpleXTheme +import chat.simplex.common.views.helpers.* import androidx.compose.ui.platform.LocalContext as LocalContext1 +import chat.simplex.res.MR actual fun showToast(text: String, timeout: Long) = Toast.makeText(androidAppContext, text, Toast.LENGTH_SHORT).show() @@ -71,3 +75,37 @@ actual fun hideKeyboard(view: Any?) { } actual fun androidIsFinishingMainActivity(): Boolean = (mainActivity.get()?.isFinishing == true) + +actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler { + actual override fun uncaughtException(thread: Thread, e: Throwable) { + Log.e(TAG, "App crashed, thread name: " + thread.name + ", exception: " + e.stackTraceToString()) + if (ModalManager.start.hasModalsOpen()) { + ModalManager.start.closeModal() + } else if (chatModel.chatId.value != null) { + // Since no modals are open, the problem is probably in ChatView + chatModel.chatId.value = null + chatModel.chatItems.clear() + } else { + // ChatList, nothing to do. Maybe to show other view except ChatList + } + chatModel.activeCall.value?.let { + withBGApi { + chatModel.callManager.endCall(it) + } + } + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.app_was_crashed), + text = e.stackTraceToString() + ) + //mainActivity.get()?.recreate() + mainActivity.get()?.apply { + window + ?.decorView + ?.findViewById(android.R.id.content) + ?.removeViewAt(0) + setContent { + AppScreen() + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index d4062a1aae..4387adf95e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -42,9 +42,11 @@ data class SettingsViewState( @Composable fun AppScreen() { - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Surface(color = MaterialTheme.colors.background) { - MainScreen() + SimpleXTheme { + ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { + Surface(color = MaterialTheme.colors.background) { + MainScreen() + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt index c61d8564c4..28ac357c2d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt @@ -16,3 +16,11 @@ expect fun getKeyboardState(): State expect fun hideKeyboard(view: Any?) expect fun androidIsFinishingMainActivity(): Boolean + +fun registerGlobalErrorHandler() { + Thread.setDefaultUncaughtExceptionHandler(GlobalExceptionsHandler()) +} + +expect class GlobalExceptionsHandler(): Thread.UncaughtExceptionHandler { + override fun uncaughtException(thread: Thread, e: Throwable) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index 10bbae6bfc..ee9a8337a0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -1,8 +1,11 @@ package chat.simplex.common.views.helpers import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -233,53 +236,71 @@ private fun alertTitle(title: String): (@Composable () -> Unit)? { @Composable private fun AlertContent(text: String?, hostDevice: Pair?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) { - Column( - Modifier - .padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF) - ) { - if (appPlatform.isDesktop) { - HostDeviceTitle(hostDevice, extraPadding = extraPadding) - } else { - Spacer(Modifier.size(DEFAULT_PADDING_HALF)) - } - CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { - if (text != null) { - Text( - escapedHtmlToAnnotatedString(text, LocalDensity.current), - Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), - fontSize = 16.sp, - textAlign = TextAlign.Center, - color = MaterialTheme.colors.secondary - ) + BoxWithConstraints { + Column( + Modifier + .padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF) + ) { + if (appPlatform.isDesktop) { + HostDeviceTitle(hostDevice, extraPadding = extraPadding) + } else { + Spacer(Modifier.size(DEFAULT_PADDING_HALF)) } + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { + if (text != null) { + Column(Modifier.heightIn(max = this@BoxWithConstraints.maxHeight * 0.7f) + .verticalScroll(rememberScrollState()) + ) { + SelectionContainer { + Text( + escapedHtmlToAnnotatedString(text, LocalDensity.current), + Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), + fontSize = 16.sp, + textAlign = TextAlign.Center, + color = MaterialTheme.colors.secondary + ) + } + } + } + } + content() } - content() } } @Composable private fun AlertContent(text: AnnotatedString?, hostDevice: Pair?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) { - Column( - Modifier - .padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF) - ) { - if (appPlatform.isDesktop) { - HostDeviceTitle(hostDevice, extraPadding = extraPadding) - } else { - Spacer(Modifier.size(DEFAULT_PADDING_HALF)) - } - CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { - if (text != null) { - Text( - text, - Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), - fontSize = 16.sp, - textAlign = TextAlign.Center, - color = MaterialTheme.colors.secondary - ) + BoxWithConstraints { + Column( + Modifier + .verticalScroll(rememberScrollState()) + .padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF) + ) { + if (appPlatform.isDesktop) { + HostDeviceTitle(hostDevice, extraPadding = extraPadding) + } else { + Spacer(Modifier.size(DEFAULT_PADDING_HALF)) } + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { + if (text != null) { + Column( + Modifier.heightIn(max = this@BoxWithConstraints.maxHeight * 0.7f) + .verticalScroll(rememberScrollState()) + ) { + SelectionContainer { + Text( + text, + Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), + fontSize = 16.sp, + textAlign = TextAlign.Center, + color = MaterialTheme.colors.secondary + ) + } + } + } + } + content() } - content() } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 840e29fc08..e0b8f130db 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -18,6 +18,7 @@ Opening database… Invalid file path You shared an invalid file path. Report the issue to the app developers. + View crashed connected diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index d6cc9c7bbb..f1cef022d8 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -9,30 +9,74 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposeWindow import androidx.compose.ui.input.key.* import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* import chat.simplex.common.model.ChatController import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.common.views.TerminalView -import chat.simplex.common.views.helpers.FileDialogChooser -import chat.simplex.common.views.helpers.escapedHtmlToAnnotatedString +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import java.awt.event.WindowEvent import java.awt.event.WindowFocusListener import java.io.File +import kotlin.system.exitProcess val simplexWindowState = SimplexWindowState() -fun showApp() = application { +fun showApp() { + val closedByError = mutableStateOf(true) + while (closedByError.value) { + application(exitProcessOnExit = false) { + CompositionLocalProvider( + LocalWindowExceptionHandlerFactory provides WindowExceptionHandlerFactory { window -> + WindowExceptionHandler { e -> + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.app_was_crashed), + text = e.stackTraceToString() + ) + Log.e(TAG, "App crashed, thread name: " + Thread.currentThread().name + ", exception: " + e.stackTraceToString()) + window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING)) + closedByError.value = true + // If the left side of screen has open modal, it's probably caused the crash + if (ModalManager.start.hasModalsOpen()) { + ModalManager.start.closeModal() + } else if (ModalManager.start.hasModalsOpen() || ModalManager.center.hasModalsOpen() || ModalManager.end.hasModalsOpen()) { + ModalManager.start.closeModal() + ModalManager.center.closeModal() + ModalManager.end.closeModal() + // Better to not close fullscreen since it can contain passcode + } else { + // The last possible cause that can be closed + chatModel.chatId.value + chatModel.chatItems.clear() + } + chatModel.activeCall.value?.let { + withBGApi { + chatModel.callManager.endCall(it) + } + } + } + } + ) { + AppWindow(closedByError) + } + } + } + exitProcess(0) +} + +@Composable +private fun ApplicationScope.AppWindow(closedByError: MutableState) { // Creates file if not exists; comes with proper defaults val state = getStoredWindowState() - val windowState: WindowState = rememberWindowState( placement = WindowPlacement.Floating, width = state.width.dp, @@ -46,72 +90,73 @@ fun showApp() = application { windowState.size.width.value, windowState.size.height.value ) { - storeWindowState(WindowPositionSize( - x = windowState.position.x.value.toInt(), - y = windowState.position.y.value.toInt(), - width = windowState.size.width.value.toInt(), - height = windowState.size.height.value.toInt() - )) + storeWindowState( + WindowPositionSize( + x = windowState.position.x.value.toInt(), + y = windowState.position.y.value.toInt(), + width = windowState.size.width.value.toInt(), + height = windowState.size.height.value.toInt() + ) + ) } simplexWindowState.windowState = windowState // Reload all strings in all @Composable's after language change at runtime if (remember { ChatController.appPrefs.appLanguage.state }.value != "") { - Window(state = windowState, onCloseRequest = ::exitApplication, onKeyEvent = { + Window(state = windowState, onCloseRequest = { closedByError.value = false; exitApplication() }, onKeyEvent = { if (it.key == Key.Escape && it.type == KeyEventType.KeyUp) { simplexWindowState.backstack.lastOrNull()?.invoke() != null } else { false } }, title = "SimpleX") { - SimpleXTheme { - AppScreen() - if (simplexWindowState.openDialog.isAwaiting) { - FileDialogChooser( - title = "SimpleX", - isLoad = true, - params = simplexWindowState.openDialog.params, - onResult = { - simplexWindowState.openDialog.onResult(it.firstOrNull()) - } - ) - } - - if (simplexWindowState.openMultipleDialog.isAwaiting) { - FileDialogChooser( - title = "SimpleX", - isLoad = true, - params = simplexWindowState.openMultipleDialog.params, - onResult = { - simplexWindowState.openMultipleDialog.onResult(it) - } - ) - } - - if (simplexWindowState.saveDialog.isAwaiting) { - FileDialogChooser( - title = "SimpleX", - isLoad = false, - params = simplexWindowState.saveDialog.params, - onResult = { simplexWindowState.saveDialog.onResult(it.firstOrNull()) } - ) - } - val toasts = remember { simplexWindowState.toasts } - val toast = toasts.firstOrNull() - if (toast != null) { - Box(Modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.BottomCenter) { - Text( - escapedHtmlToAnnotatedString(toast.first, LocalDensity.current), - Modifier.background(MaterialTheme.colors.primary, RoundedCornerShape(100)).padding(vertical = 5.dp, horizontal = 10.dp), - color = MaterialTheme.colors.onPrimary, - style = MaterialTheme.typography.body1 - ) + simplexWindowState.window = window + AppScreen() + if (simplexWindowState.openDialog.isAwaiting) { + FileDialogChooser( + title = "SimpleX", + isLoad = true, + params = simplexWindowState.openDialog.params, + onResult = { + simplexWindowState.openDialog.onResult(it.firstOrNull()) } - // Shows toast in insertion order with preferred delay per toast. New one will be shown once previous one expires - LaunchedEffect(toast, toasts.size) { - delay(toast.second) - simplexWindowState.toasts.removeFirst() + ) + } + + if (simplexWindowState.openMultipleDialog.isAwaiting) { + FileDialogChooser( + title = "SimpleX", + isLoad = true, + params = simplexWindowState.openMultipleDialog.params, + onResult = { + simplexWindowState.openMultipleDialog.onResult(it) } + ) + } + + if (simplexWindowState.saveDialog.isAwaiting) { + FileDialogChooser( + title = "SimpleX", + isLoad = false, + params = simplexWindowState.saveDialog.params, + onResult = { simplexWindowState.saveDialog.onResult(it.firstOrNull()) } + ) + } + val toasts = remember { simplexWindowState.toasts } + val toast = toasts.firstOrNull() + if (toast != null) { + Box(Modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.BottomCenter) { + Text( + escapedHtmlToAnnotatedString(toast.first, LocalDensity.current), + Modifier.background(MaterialTheme.colors.primary, RoundedCornerShape(100)).padding(vertical = 5.dp, horizontal = 10.dp), + color = MaterialTheme.colors.onPrimary, + style = MaterialTheme.typography.body1 + ) + } + // Shows toast in insertion order with preferred delay per toast. New one will be shown once previous one expires + LaunchedEffect(toast, toasts.size) { + delay(toast.second) + simplexWindowState.toasts.removeFirst() } } var windowFocused by remember { simplexWindowState.windowFocused } @@ -160,6 +205,7 @@ class SimplexWindowState { val saveDialog = DialogState() val toasts = mutableStateListOf>() var windowFocused = mutableStateOf(true) + var window: ComposeWindow? = null } data class DialogParams( @@ -188,7 +234,5 @@ class DialogState { @Preview @Composable fun AppPreview() { - SimpleXTheme { - AppScreen() - } + AppScreen() } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt index 94d42ba792..b5a0e2e008 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt @@ -19,3 +19,9 @@ actual fun getKeyboardState(): State = remember { mutableStateOf( actual fun hideKeyboard(view: Any?) {} actual fun androidIsFinishingMainActivity(): Boolean = false + +actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler { + actual override fun uncaughtException(thread: Thread, e: Throwable) { + Log.e(TAG, "App crashed, thread name: " + thread.name + ", exception: " + e.stackTraceToString()) + } +} diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt index 787e41fd81..e32b0ae79a 100644 --- a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -3,10 +3,6 @@ package chat.simplex.desktop import chat.simplex.common.platform.* import chat.simplex.common.showApp import java.io.File -import java.nio.file.* -import java.nio.file.attribute.BasicFileAttributes -import java.nio.file.attribute.FileTime -import kotlin.io.path.setLastModifiedTime fun main() { initHaskell()