From 4a4d470859e86b44ebf61447a505957c54ffcf5e Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 21 Dec 2023 02:00:44 +0800 Subject: [PATCH] android, desktop: try-catch composables (#3575) * android, desktop: try-catch composables * test * better catching on Android * more try-catch'es * Revert "test" This reverts commit adaf92b116fd8453d44cd401055fb0904f41f23c. * more try-catch'es * unneeded imports --- .../main/java/chat/simplex/app/SimplexApp.kt | 17 ++++++ .../simplex/common/platform/UI.android.kt | 34 ++++++----- .../kotlin/chat/simplex/common/App.kt | 8 ++- .../simplex/common/views/chat/ChatView.kt | 6 +- .../views/chat/item/CIBrokenComposableView.kt | 18 ++++++ .../views/chatlist/ChatListNavLinkView.kt | 60 ++++++++++++++++--- .../common/views/chatlist/ChatListView.kt | 26 ++++++-- .../common/views/chatlist/ShareListView.kt | 10 ++-- .../simplex/common/views/helpers/Utils.kt | 22 +++++++ .../commonMain/resources/MR/base/strings.xml | 3 + .../kotlin/chat/simplex/common/DesktopApp.kt | 1 + 11 files changed, 169 insertions(+), 36 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIBrokenComposableView.kt 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 a345e6e48..e3f4e69bd 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 @@ -1,6 +1,8 @@ package chat.simplex.app import android.app.Application +import android.os.Handler +import android.os.Looper import chat.simplex.common.platform.Log import androidx.lifecycle.* import androidx.work.* @@ -35,6 +37,21 @@ class SimplexApp: Application(), LifecycleEventObserver { return } else { registerGlobalErrorHandler() + Handler(Looper.getMainLooper()).post { + while (true) { + try { + Looper.loop() + } catch (e: Throwable) { + if (e.message != null && e.message!!.startsWith("Unable to start activity")) { + android.os.Process.killProcess(android.os.Process.myPid()) + break + } else { + // Send it to our exception handled because it will not get the exception otherwise + Thread.getDefaultUncaughtExceptionHandler()?.uncaughtException(Looper.getMainLooper().thread, e) + } + } + } + } } 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 96bb73911..371c14013 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 @@ -4,7 +4,7 @@ import android.app.Activity import android.content.Context import android.content.pm.ActivityInfo import android.graphics.Rect -import android.os.Build +import android.os.* import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.Toast @@ -12,7 +12,6 @@ import androidx.activity.compose.setContent import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalView 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 @@ -79,6 +78,7 @@ actual fun androidIsFinishingMainActivity(): Boolean = (mainActivity.get()?.isFi 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()) + includeMoreFailedComposables() if (ModalManager.start.hasModalsOpen()) { ModalManager.start.closeModal() } else if (chatModel.chatId.value != null) { @@ -93,19 +93,25 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler { 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() + if (thread.name == "main") { + mainActivity.get()?.recreate() + } else { + mainActivity.get()?.apply { + window + ?.decorView + ?.findViewById(android.R.id.content) + ?.removeViewAt(0) + setContent { + AppScreen() + } } } + // Wait until activity recreates to prevent showing two alerts (in case `main` was crashed) + Handler(Looper.getMainLooper()).post { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.app_was_crashed), + text = e.stackTraceToString() + ) + } } } 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 0082972c7..d457eb57a 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 @@ -332,9 +332,11 @@ fun DesktopScreen(settingsState: SettingsViewState) { ) } VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH)) - UserPicker(chatModel, userPickerState) { - scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } - userPickerState.value = AnimatedViewState.GONE + tryOrShowError("UserPicker", error = {}) { + UserPicker(chatModel, userPickerState) { + scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } + userPickerState.value = AnimatedViewState.GONE + } } ModalManager.fullscreen.showInView() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 8eee43035..ebec780df 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -900,7 +900,11 @@ fun BoxWithConstraintsScope.ChatItemsList( @Composable fun ChatItemViewShortHand(cItem: ChatItem, range: IntRange?) { - ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools) + tryOrShowError("${cItem.id}ChatItem", error = { + CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) + }) { + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools) + } } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIBrokenComposableView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIBrokenComposableView.kt new file mode 100644 index 000000000..d49f8526d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIBrokenComposableView.kt @@ -0,0 +1,18 @@ +package chat.simplex.common.views.chat.item + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.dp +import chat.simplex.res.MR + +@Composable +fun CIBrokenComposableView(alignment: Alignment) { + Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp), contentAlignment = alignment) { + Text(stringResource(MR.strings.error_showing_message), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 9ae0da2a3..8d5446aa5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -61,9 +62,17 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { is ChatInfo.Direct -> { val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact) ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode, inProgress = false, progressByTimeout = false) }, + chatLinkPreview = { + tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { + ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode, inProgress = false, progressByTimeout = false) + } + }, click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) }, - dropdownMenuItems = { ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu, showMarkRead) }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { + ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu, showMarkRead) + } + }, showMenu, stopped, selectedChat @@ -71,25 +80,45 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { } is ChatInfo.Group -> ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode, inProgress.value, progressByTimeout) }, + chatLinkPreview = { + tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { + ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode, inProgress.value, progressByTimeout) + } + }, click = { if (!inProgress.value) groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) }, - dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead) }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { + GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead) + } + }, showMenu, stopped, selectedChat ) is ChatInfo.ContactRequest -> ChatListNavLinkLayout( - chatLinkPreview = { ContactRequestView(chat.chatInfo) }, + chatLinkPreview = { + tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { + ContactRequestView(chat.chatInfo) + } + }, click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) }, - dropdownMenuItems = { ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { + ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) + } + }, showMenu, stopped, selectedChat ) is ChatInfo.ContactConnection -> ChatListNavLinkLayout( - chatLinkPreview = { ContactConnectionView(chat.chatInfo.contactConnection) }, + chatLinkPreview = { + tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { + ContactConnectionView(chat.chatInfo.contactConnection) + } + }, click = { ModalManager.center.closeModals() ModalManager.end.closeModals() @@ -97,7 +126,11 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { ContactConnectionInfoView(chatModel, chat.remoteHostId, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close) } }, - dropdownMenuItems = { ContactConnectionMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { + ContactConnectionMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) + } + }, showMenu, stopped, selectedChat @@ -105,7 +138,9 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { is ChatInfo.InvalidJSON -> ChatListNavLinkLayout( chatLinkPreview = { - InvalidDataView() + tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { + InvalidDataView() + } }, click = { ModalManager.end.closeModals() @@ -119,6 +154,13 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { } } +@Composable +private fun ErrorChatListItem() { + Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) { + Text(stringResource(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic) + } +} + fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) { when { contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 18252d0e2..cf12727d7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* +import androidx.compose.ui.text.font.FontStyle import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight @@ -64,7 +65,11 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf val (userPickerState, scaffoldState ) = settingsState Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } } }, scaffoldState = scaffoldState, - drawerContent = { SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) }, + drawerContent = { + tryOrShowError("Settings", error = { ErrorSettingsView() }) { + SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) + } + }, drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f), drawerGesturesEnabled = appPlatform.isAndroid, floatingActionButton = { @@ -111,12 +116,16 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf if (searchInList.isEmpty()) { DesktopActiveCallOverlayLayout(newChatSheetState) // TODO disable this button and sheet for the duration of the switch - NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet) + tryOrShowError("NewChatSheet", error = {}) { + NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet) + } } if (appPlatform.isAndroid) { - UserPicker(chatModel, userPickerState) { - scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } - userPickerState.value = AnimatedViewState.GONE + tryOrShowError("UserPicker", error = {}) { + UserPicker(chatModel, userPickerState) { + scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } + userPickerState.value = AnimatedViewState.GONE + } } } } @@ -303,6 +312,13 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) { } } +@Composable +private fun ErrorSettingsView() { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(generalGetString(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic) + } +} + private var lazyListState = 0 to 0 @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index 8338d2960..ac8331007 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -47,10 +47,12 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe } } if (appPlatform.isAndroid) { - UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = { - chatModel.sharedContent.value = null - userPickerState.value = AnimatedViewState.GONE - }) + tryOrShowError("UserPicker", error = {}) { + UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = { + chatModel.sharedContent.value = null + userPickerState.value = AnimatedViewState.GONE + }) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 0a0ef17c4..9a81b9f9d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -390,6 +390,28 @@ fun IntSize.Companion.Saver(): Saver = Saver( restore = { IntSize(it.first, it.second) } ) +private var lastExecutedComposables = HashSet() +private val failedComposables = HashSet() + +@Composable +fun tryOrShowError(key: Any = Exception().stackTraceToString().lines()[2], error: @Composable () -> Unit = {}, content: @Composable () -> Unit) { + if (!failedComposables.contains(key)) { + lastExecutedComposables.add(key) + content() + lastExecutedComposables.remove(key) + } else { + error() + } +} + +fun includeMoreFailedComposables() { + lastExecutedComposables.forEach { + failedComposables.add(it) + Log.i(TAG, "Added composable key as failed: $it") + } + lastExecutedComposables.clear() +} + @Composable fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) { DisposableEffect(Unit) { 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 e0b8f130d..7ee86c2f5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -45,6 +45,9 @@ moderated invalid chat invalid data + error showing message + error showing content + Decryption error Encryption re-negotiation error 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 12bead366..57371e25a 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 @@ -45,6 +45,7 @@ fun showApp() { Log.e(TAG, "App crashed, thread name: " + Thread.currentThread().name + ", exception: " + e.stackTraceToString()) window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING)) closedByError.value = true + includeMoreFailedComposables() // If the left side of screen has open modal, it's probably caused the crash if (ModalManager.start.hasModalsOpen()) { ModalManager.start.closeModal()