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 ee43da5d44..1c99b73779 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 @@ -171,6 +171,7 @@ class SimplexApp: Application(), LifecycleEventObserver { override fun androidCreateNtfChannelsMaybeShowAlert() = NtfManager.createNtfChannelsMaybeShowAlert() override fun cancelCallNotification() = NtfManager.cancelCallNotification() override fun cancelAllNotifications() = NtfManager.cancelAllNotifications() + override fun showMessage(title: String, text: String) = NtfManager.showMessage(title, text) } platform = object : PlatformInterface { override suspend fun androidServiceStart() { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt index 916f40df13..7158b82ea6 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt @@ -208,6 +208,38 @@ object NtfManager { } } + fun showMessage(title: String, text: String) { + val builder = NotificationCompat.Builder(context, MessageChannel) + .setContentTitle(title) + .setContentText(text) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setGroup(MessageGroup) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) + .setSmallIcon(R.drawable.ntf_icon) + .setLargeIcon(null) + .setColor(0x88FFFF) + .setAutoCancel(true) + .setVibrate(null) + .setContentIntent(chatPendingIntent(ShowChatsAction, null, null)) + .setSilent(false) + + val summary = NotificationCompat.Builder(context, MessageChannel) + .setSmallIcon(R.drawable.ntf_icon) + .setColor(0x88FFFF) + .setGroup(MessageGroup) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) + .setGroupSummary(true) + .setContentIntent(chatPendingIntent(ShowChatsAction, null)) + .build() + + with(NotificationManagerCompat.from(context)) { + if (ActivityCompat.checkSelfPermission(SimplexApp.context, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { + notify("MESSAGE".hashCode(), builder.build()) + notify(0, summary) + } + } + } + fun cancelCallNotification() { manager.cancel(CallNotificationId) } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseView.android.kt new file mode 100644 index 0000000000..e392c0999f --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseView.android.kt @@ -0,0 +1,7 @@ +package chat.simplex.common.views.database + +import chat.simplex.common.views.usersettings.restartApp + +actual fun restartChatOrApp() { + restartApp() +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt index 49a29cf141..9b376bb9d3 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt @@ -28,7 +28,7 @@ actual fun SettingsSectionApp( } -private fun restartApp() { +fun restartApp() { ProcessPhoenix.triggerRebirth(androidAppContext) shutdownApp() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 708bbb9073..5f1cf5783c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -122,6 +122,9 @@ object ChatModel { val remoteHostPairing = mutableStateOf?>(null) val remoteCtrlSession = mutableStateOf(null) + val processedCriticalError: ProcessedErrors = ProcessedErrors(60_000) + val processedInternalError: ProcessedErrors = ProcessedErrors(20_000) + fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) { currentUser.value } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 38f47b8b27..012fce705f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -108,6 +108,7 @@ class AppPreferences { val chatArchiveTime = mkDatePreference(SHARED_PREFS_CHAT_ARCHIVE_TIME, null) val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null) val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false) + val showInternalErrors = mkBoolPreference(SHARED_PREFS_SHOW_INTERNAL_ERRORS, false) val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false) val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false) val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050") @@ -276,6 +277,7 @@ class AppPreferences { private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage" private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" + private const val SHARED_PREFS_SHOW_INTERNAL_ERRORS = "ShowInternalErrors" private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible" private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort" @@ -1920,6 +1922,14 @@ object ChatController { } } } + is CR.ChatCmdError -> when { + r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> { + chatModel.processedCriticalError.newError(r.chatError.agentError, r.chatError.agentError.offerRestart) + } + r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.INTERNAL && appPrefs.showInternalErrors.get() -> { + chatModel.processedInternalError.newError(r.chatError.agentError, false) + } + } else -> Log.d(TAG , "unsupported event: ${r.responseType}") } @@ -4710,6 +4720,7 @@ sealed class AgentErrorType { is AGENT -> "AGENT ${agentErr.string}" is INTERNAL -> "INTERNAL $internalErr" is INACTIVE -> "INACTIVE" + is CRITICAL -> "CRITICAL $offerRestart $criticalErr" } @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType() @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType() @@ -4721,6 +4732,7 @@ sealed class AgentErrorType { @Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType() @Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType() @Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType() + @Serializable @SerialName("CRITICAL") data class CRITICAL(val offerRestart: Boolean, val criticalErr: String): AgentErrorType() } @Serializable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt index 6ca065086f..5c57a48c84 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -99,6 +99,7 @@ abstract class NtfManager { abstract fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List Unit>> = emptyList()) abstract fun cancelCallNotification() abstract fun cancelAllNotifications() + abstract fun showMessage(title: String, text: String) // Android only abstract fun androidCreateNtfChannelsMaybeShowAlert() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 224317f949..2251d890fd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -4,7 +4,6 @@ import SectionBottomSpacer import SectionDividerSpaced import SectionTextFooter import SectionItemView -import SectionSpacer import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* @@ -367,7 +366,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String { return stringResource(if (chatArchiveTime < chatLastStart) MR.strings.old_database_archive else MR.strings.new_database_archive) } -private fun startChat(m: ChatModel, chatLastStart: MutableState, chatDbChanged: MutableState) { +fun startChat(m: ChatModel, chatLastStart: MutableState, chatDbChanged: MutableState) { withApi { try { if (chatDbChanged.value) { @@ -407,6 +406,8 @@ private fun stopChatAlert(m: ChatModel) { ) } +expect fun restartChatOrApp() + private fun exportProhibitedAlert() { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.set_password_to_export), @@ -414,7 +415,7 @@ private fun exportProhibitedAlert() { ) } -private fun authStopChat(m: ChatModel) { +fun authStopChat(m: ChatModel, onStop: (() -> Unit)? = null) { if (m.controller.appPrefs.performLA.get()) { authenticate( generalGetString(MR.strings.auth_stop_chat), @@ -422,7 +423,7 @@ private fun authStopChat(m: ChatModel) { completed = { laResult -> when (laResult) { LAResult.Success, is LAResult.Unavailable -> { - stopChat(m) + stopChat(m, onStop) } is LAResult.Error -> { m.chatRunning.value = true @@ -434,15 +435,16 @@ private fun authStopChat(m: ChatModel) { } ) } else { - stopChat(m) + stopChat(m, onStop) } } -private fun stopChat(m: ChatModel) { +private fun stopChat(m: ChatModel, onStop: (() -> Unit)? = null) { withApi { try { stopChatAsync(m) platform.androidChatStopped() + onStop?.invoke() } catch (e: Error) { m.chatRunning.value = true AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_stopping_chat), e.toString()) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt new file mode 100644 index 0000000000..4b44777a3a --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt @@ -0,0 +1,64 @@ +package chat.simplex.common.views.helpers + +import chat.simplex.common.model.AgentErrorType +import chat.simplex.common.platform.Log +import chat.simplex.common.platform.TAG +import chat.simplex.common.platform.ntfManager +import chat.simplex.common.views.database.restartChatOrApp +import chat.simplex.res.MR +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay + +class ProcessedErrors (val interval: Long) { + private var lastShownTimestamp: Long = -1 + private var lastShownOfferRestart: Boolean = false + private var timer: Job = Job() + + fun newError(error: T, offerRestart: Boolean) { + timer.cancel() + timer = withBGApi { + val delayBeforeNext = (lastShownTimestamp + interval) - System.currentTimeMillis() + if ((lastShownOfferRestart || !offerRestart) && delayBeforeNext >= 0) { + delay(delayBeforeNext) + } + lastShownTimestamp = System.currentTimeMillis() + lastShownOfferRestart = offerRestart + AlertManager.shared.hideAllAlerts() + showMessage(error, offerRestart) + } + } + + private fun showMessage(error: T, offerRestart: Boolean) { + when (error) { + is AgentErrorType.CRITICAL -> { + val title = generalGetString(MR.strings.agent_critical_error_title) + val text = generalGetString(MR.strings.agent_critical_error_desc).format(error.criticalErr) + try { + ntfManager.showMessage(title, text) + } catch (e: Throwable) { + Log.e(TAG, e.stackTraceToString()) + } + if (offerRestart) { + AlertManager.shared.showAlertDialog( + title = title, + text = text, + confirmText = generalGetString(MR.strings.restart_chat_button), + onConfirm = { + withApi { restartChatOrApp() } + }) + } else { + AlertManager.shared.showAlertMsg( + title = title, + text = text, + ) + } + } + is AgentErrorType.INTERNAL -> { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.agent_internal_error_title), + text = generalGetString(MR.strings.agent_internal_error_desc).format(error.internalErr), + ) + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index a6ac8c14ed..969a6d9d9f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -10,10 +10,11 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler +import chat.simplex.common.model.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.appPlatform +import chat.simplex.common.platform.appPreferences import chat.simplex.common.views.TerminalView import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @@ -44,6 +45,7 @@ fun DeveloperView( m.controller.appPrefs.terminalAlwaysVisible.set(false) } } + SettingsPreferenceItem(painterResource(MR.images.ic_report), stringResource(MR.strings.show_internal_errors), appPreferences.showInternalErrors) } } SectionTextFooter( 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 4ad40d7a62..d13a7c65fb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -662,6 +662,7 @@ Hide: Show developer options Database IDs and Transport isolation option. + Show internal errors Shutdown? Notifications will stop working until you re-launch the app @@ -1724,4 +1725,11 @@ You are already joining the group via this link. You are already in group %1$s. Connect via link? + + + Critical error + Please report it to the developers: \n%s\n\nIt is recommended to restart the app. + Internal error + Please report it to the developers: \n%s + Restart chat \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg new file mode 100644 index 0000000000..8695857b97 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt index cb34bdb3b0..1892b0c7f9 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt @@ -47,6 +47,10 @@ object NtfManager { } } + fun showMessage(title: String, text: String) { + displayNotificationViaLib("MESSAGE", title, text, null, emptyList()) {} + } + fun hasNotificationsForChat(chatId: ChatId) = false//prevNtfs.any { it.first == chatId } fun cancelNotificationsForChat(chatId: ChatId) { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt index 92111f162a..c4dc974da7 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt @@ -23,6 +23,7 @@ fun initApp() { override fun androidCreateNtfChannelsMaybeShowAlert() {} override fun cancelCallNotification() {} override fun cancelAllNotifications() = chat.simplex.common.model.NtfManager.cancelAllNotifications() + override fun showMessage(title: String, text: String) = chat.simplex.common.model.NtfManager.showMessage(title, text) } applyAppLocale() withBGApi { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseView.desktop.kt new file mode 100644 index 0000000000..10e9f9f3c3 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseView.desktop.kt @@ -0,0 +1,23 @@ +package chat.simplex.common.views.database + +import androidx.compose.runtime.mutableStateOf +import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.helpers.withApi +import kotlinx.coroutines.delay +import kotlinx.datetime.Instant + +actual fun restartChatOrApp() { + if (chatModel.chatRunning.value == false) { + chatModel.chatDbChanged.value = true + startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged) + } else { + authStopChat(chatModel) { + withApi { + // adding delay in order to prevent locked database by previous initialization + delay(1000) + chatModel.chatDbChanged.value = true + startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged) + } + } + } +}