diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt index 2cb6c12da..d1e0d9721 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import chat.simplex.common.platform.Log import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.work.* @@ -21,12 +20,13 @@ import chat.simplex.common.AppLock import chat.simplex.common.helpers.requiresIgnoringBattery import chat.simplex.common.model.ChatController import chat.simplex.common.model.NotificationsMode -import chat.simplex.common.platform.androidAppContext +import chat.simplex.common.platform.* import chat.simplex.common.views.helpers.* import kotlinx.coroutines.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlin.system.exitProcess // based on: // https://robertohuertas.com/2019/06/29/android_foreground_services/ @@ -173,6 +173,11 @@ class SimplexService: Service() { // Just to make sure that after restart of the app the user will need to re-authenticate AppLock.clearAuthState() + if (appPreferences.chatStopped.get()) { + stopSelf() + exitProcess(0) + } + // If notification service isn't enabled or battery optimization isn't disabled, we shouldn't restart the service if (!SimplexApp.context.allowToStartServiceAfterAppExit()) { return 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 371c14013..d360c44b4 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 @@ -97,12 +97,14 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler { mainActivity.get()?.recreate() } else { mainActivity.get()?.apply { - window - ?.decorView - ?.findViewById(android.R.id.content) - ?.removeViewAt(0) - setContent { - AppScreen() + runOnUiThread { + window + ?.decorView + ?.findViewById(android.R.id.content) + ?.removeViewAt(0) + setContent { + AppScreen() + } } } } 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 07e091b48..fc0d09772 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 @@ -106,6 +106,7 @@ class AppPreferences { val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) val chatArchiveTime = mkDatePreference(SHARED_PREFS_CHAT_ARCHIVE_TIME, null) val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null) + val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false) val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false) val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false) val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false) @@ -273,6 +274,7 @@ class AppPreferences { private const val SHARED_PREFS_APP_LANGUAGE = "AppLanguage" private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage" private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" + private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped" private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible" private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" @@ -346,14 +348,8 @@ object ChatController { try { if (chatModel.chatRunning.value == true) return apiSetNetworkConfig(getNetCfg()) - apiSetTempFolder(coreTmpDir.absolutePath) - apiSetFilesFolder(appFilesDir.absolutePath) - if (appPlatform.isDesktop) { - apiSetRemoteHostsFolder(remoteHostsDir.absolutePath) - } - apiSetXFTPConfig(getXFTPCfg()) - apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get()) val justStarted = apiStartChat() + appPrefs.chatStopped.set(false) val users = listUsers(null) chatModel.users.clear() chatModel.users.addAll(users) @@ -365,6 +361,9 @@ object ChatController { chatModel.chatRunning.value = true startReceiver() setLocalDeviceName(appPrefs.deviceNameForRemoteAccess.get()!!) + if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) { + chatModel.setDeliveryReceipts.value = true + } Log.d(TAG, "startChat: started") } else { updatingChatsMutex.withLock { @@ -383,13 +382,6 @@ object ChatController { Log.d(TAG, "user: null") try { if (chatModel.chatRunning.value == true) return - apiSetTempFolder(coreTmpDir.absolutePath) - apiSetFilesFolder(appFilesDir.absolutePath) - if (appPlatform.isDesktop) { - apiSetRemoteHostsFolder(remoteHostsDir.absolutePath) - } - apiSetXFTPConfig(getXFTPCfg()) - apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get()) chatModel.users.clear() chatModel.currentUser.value = null chatModel.localUserCreated.value = false @@ -596,19 +588,19 @@ object ChatController { } } - private suspend fun apiSetTempFolder(tempFolder: String) { + suspend fun apiSetTempFolder(tempFolder: String) { val r = sendCmd(null, CC.SetTempFolder(tempFolder)) if (r is CR.CmdOk) return throw Error("failed to set temp folder: ${r.responseType} ${r.details}") } - private suspend fun apiSetFilesFolder(filesFolder: String) { + suspend fun apiSetFilesFolder(filesFolder: String) { val r = sendCmd(null, CC.SetFilesFolder(filesFolder)) if (r is CR.CmdOk) return throw Error("failed to set files folder: ${r.responseType} ${r.details}") } - private suspend fun apiSetRemoteHostsFolder(remoteHostsFolder: String) { + suspend fun apiSetRemoteHostsFolder(remoteHostsFolder: String) { val r = sendCmd(null, CC.SetRemoteHostsFolder(remoteHostsFolder)) if (r is CR.CmdOk) return throw Error("failed to set remote hosts folder: ${r.responseType} ${r.details}") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 07e59a55e..00a824438 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -1,8 +1,13 @@ package chat.simplex.common.platform import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.currentUser import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.helpers.DatabaseUtils.ksDatabasePassword import chat.simplex.common.views.onboarding.OnboardingStage +import chat.simplex.res.MR +import kotlinx.coroutines.* import kotlinx.serialization.decodeFromString import java.nio.ByteBuffer @@ -39,13 +44,17 @@ val chatController: ChatController = ChatController fun initChatControllerAndRunMigrations(ignoreSelfDestruct: Boolean) { if (ignoreSelfDestruct || DatabaseUtils.ksSelfDestructPassword.get() == null) { withBGApi { - initChatController() + if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) { + initChatController(startChat = ::showStartChatAfterRestartAlert) + } else { + initChatController() + } runMigrations() } } } -suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) { +suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: () -> CompletableDeferred = { CompletableDeferred(true) }) { try { chatModel.ctrlInitInProgress.value = true val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() @@ -62,45 +71,66 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat chatModel.chatDbStatus.value = res if (res != DBMigrationResult.OK) { Log.d(TAG, "Unable to migrate successfully: $res") - } else if (startChat) { - // If we migrated successfully means previous re-encryption process on database level finished successfully too - if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) - val user = chatController.apiGetActiveUser(null) - if (user == null) { - chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) - chatModel.currentUser.value = null - chatModel.users.clear() - if (appPlatform.isDesktop) { - /** - * Setting it here to null because otherwise the screen will flash in [MainScreen] after the first start - * because of default value of [OnboardingStage.OnboardingComplete] - * */ - chatModel.localUserCreated.value = null - if (chatController.listRemoteHosts()?.isEmpty() == true) { - chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - } - chatController.startChatWithoutUser() - } else { + return + } + controller.apiSetTempFolder(coreTmpDir.absolutePath) + controller.apiSetFilesFolder(appFilesDir.absolutePath) + if (appPlatform.isDesktop) { + controller.apiSetRemoteHostsFolder(remoteHostsDir.absolutePath) + } + controller.apiSetXFTPConfig(controller.getXFTPCfg()) + controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get()) + // If we migrated successfully means previous re-encryption process on database level finished successfully too + if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) + val user = chatController.apiGetActiveUser(null) + chatModel.currentUser.value = user + if (user == null) { + chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) + chatModel.currentUser.value = null + chatModel.users.clear() + if (appPlatform.isDesktop) { + /** + * Setting it here to null because otherwise the screen will flash in [MainScreen] after the first start + * because of default value of [OnboardingStage.OnboardingComplete] + * */ + chatModel.localUserCreated.value = null + if (chatController.listRemoteHosts()?.isEmpty() == true) { chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } + chatController.startChatWithoutUser() } else { - val savedOnboardingStage = appPreferences.onboardingStage.get() - val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { - OnboardingStage.Step3_CreateSimpleXAddress - } else { - savedOnboardingStage - } - if (appPreferences.onboardingStage.get() != newStage) { - appPreferences.onboardingStage.set(newStage) - } - if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) { - chatModel.setDeliveryReceipts.value = true - } - chatController.startChat(user) - platform.androidChatInitializedAndStarted() + chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } + } else if (startChat().await()) { + val savedOnboardingStage = appPreferences.onboardingStage.get() + val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { + OnboardingStage.Step3_CreateSimpleXAddress + } else { + savedOnboardingStage + } + if (appPreferences.onboardingStage.get() != newStage) { + appPreferences.onboardingStage.set(newStage) + } + chatController.startChat(user) + platform.androidChatInitializedAndStarted() + } else { + chatController.getUserChatData(null) + chatModel.localUserCreated.value = currentUser.value != null + chatModel.chatRunning.value = false } } finally { chatModel.ctrlInitInProgress.value = false } } + +fun showStartChatAfterRestartAlert(): CompletableDeferred { + val deferred = CompletableDeferred() + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.start_chat_question), + text = generalGetString(MR.strings.chat_is_stopped_you_should_transfer_database), + onConfirm = { deferred.complete(true) }, + onDismiss = { deferred.complete(false) }, + onDismissRequest = { deferred.complete(false) } + ) + return deferred +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt index 22d69de1c..0c208c06e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt @@ -44,7 +44,7 @@ fun DatabaseErrorView( fun callRunChat(confirmMigrations: MigrationConfirmation? = null) { val useKey = if (useKeychain) null else dbKey.value - runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator, appPreferences) + runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator) } fun saveAndRunChatOnClick() { @@ -190,13 +190,14 @@ private fun runChat( confirmMigrations: MigrationConfirmation? = null, chatDbStatus: State, progressIndicator: MutableState, - prefs: AppPreferences ) = CoroutineScope(Dispatchers.Default).launch { // Don't do things concurrently. Shouldn't be here concurrently, just in case if (progressIndicator.value) return@launch progressIndicator.value = true try { - initChatController(dbKey, confirmMigrations) + initChatController(dbKey, confirmMigrations, + startChat = if (appPreferences.chatStopped.get()) ::showStartChatAfterRestartAlert else { { CompletableDeferred(true) } } + ) } catch (e: Exception) { Log.d(TAG, "initializeChat ${e.stackTraceToString()}") } 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 5e1abb684..3769e0fc9 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 @@ -378,12 +378,12 @@ private fun startChat(m: ChatModel, chatLastStart: MutableState, chatD ModalManager.closeAllModalsEverywhere() return@withApi } - if (m.currentUser.value == null) { + val user = m.currentUser.value + if (user == null) { ModalManager.closeAllModalsEverywhere() return@withApi } else { - m.controller.apiStartChat() - m.chatRunning.value = true + m.controller.startChat(user) } val ts = Clock.System.now() m.controller.appPrefs.chatLastStart.set(ts) @@ -453,6 +453,7 @@ private fun stopChat(m: ChatModel) { suspend fun stopChatAsync(m: ChatModel) { m.controller.apiStopChat() m.chatRunning.value = false + controller.appPrefs.chatStopped.set(true) } suspend fun deleteChatAsync(m: ChatModel) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt index c984e1645..cc06716da 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt @@ -82,7 +82,6 @@ sealed class DBMigrationResult { @Serializable @SerialName("unknown") data class Unknown(val json: String): DBMigrationResult() } - enum class MigrationConfirmation(val value: String) { YesUp("yesUp"), YesUpDown ("yesUpDown"), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt index 040190652..b758ecdcf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt @@ -84,7 +84,7 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: ( m.chatDbChanged.value = true m.chatDbStatus.value = null try { - initChatController(startChat = true) + initChatController() } catch (e: Exception) { Log.d(TAG, "initializeChat ${e.stackTraceToString()}") } 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 bd61d9473..1cf1922c5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1112,6 +1112,8 @@ Chat is stopped You can start chat via app Settings / Database or by restarting the app. + Start chat? + Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. Chat archive